Update.
This commit is contained in:
		
							parent
							
								
									2fc18801a7
								
							
						
					
					
						commit
						62eb1060d9
					
				@ -10,7 +10,7 @@
 | 
				
			|||||||
import { Schedule } from "./schedule.js";
 | 
					import { Schedule } from "./schedule.js";
 | 
				
			||||||
import { EventHandler } from "./event-handler.js";
 | 
					import { EventHandler } from "./event-handler.js";
 | 
				
			||||||
import { Socket } from "./socket.js";
 | 
					import { Socket } from "./socket.js";
 | 
				
			||||||
 | 
					import { Njet } from "./njet.js";
 | 
				
			||||||
export class RESTClient {
 | 
					export class RESTClient {
 | 
				
			||||||
  debug = false;
 | 
					  debug = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
import { app } from "../app.js";
 | 
					import { app } from "./app.js";
 | 
				
			||||||
 | 
					import { NjetComponent } from "./njet.js";
 | 
				
			||||||
class ChatInputComponent extends HTMLElement {
 | 
					import { FileUploadGrid } from "./file-upload-grid.js";
 | 
				
			||||||
 | 
					class ChatInputComponent extends NjetComponent {
 | 
				
			||||||
  autoCompletions = {
 | 
					  autoCompletions = {
 | 
				
			||||||
    "example 1": () => {
 | 
					    "example 1": () => {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -161,6 +162,11 @@ class ChatInputComponent extends HTMLElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    this.classList.add("chat-input");
 | 
					    this.classList.add("chat-input");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.fileUploadGrid = new FileUploadGrid();
 | 
				
			||||||
 | 
					    this.fileUploadGrid.setAttribute("channel", this.channelUid);
 | 
				
			||||||
 | 
					    this.fileUploadGrid.style.display = "none";
 | 
				
			||||||
 | 
					    this.appendChild(this.fileUploadGrid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.textarea.setAttribute("placeholder", "Type a message...");
 | 
					    this.textarea.setAttribute("placeholder", "Type a message...");
 | 
				
			||||||
    this.textarea.setAttribute("rows", "2");
 | 
					    this.textarea.setAttribute("rows", "2");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -174,7 +180,12 @@ class ChatInputComponent extends HTMLElement {
 | 
				
			|||||||
    this.uploadButton.addEventListener("uploaded", (e) => {
 | 
					    this.uploadButton.addEventListener("uploaded", (e) => {
 | 
				
			||||||
      this.dispatchEvent(new CustomEvent("uploaded", e));
 | 
					      this.dispatchEvent(new CustomEvent("uploaded", e));
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    this.subscribe("file-uploading", (e) => {
 | 
				
			||||||
 | 
					        this.fileUploadGrid.style.display = "block";   
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.uploadButton.style.display = "none";
 | 
				
			||||||
 | 
					        this.textarea.style.display = "none";
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
    this.appendChild(this.uploadButton);
 | 
					    this.appendChild(this.uploadButton);
 | 
				
			||||||
    this.textarea.addEventListener("blur", () => {
 | 
					    this.textarea.addEventListener("blur", () => {
 | 
				
			||||||
      this.updateFromInput("");
 | 
					      this.updateFromInput("");
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										59
									
								
								src/snek/static/file-upload-grid.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/snek/static/file-upload-grid.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,59 @@
 | 
				
			|||||||
 | 
					.fug-root {
 | 
				
			||||||
 | 
					    background: #181818;
 | 
				
			||||||
 | 
					    color: white;
 | 
				
			||||||
 | 
					    font-family: sans-serif;
 | 
				
			||||||
 | 
					    /*min-height: 100vh;*/
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.fug-grid {
 | 
				
			||||||
 | 
					    display: grid;
 | 
				
			||||||
 | 
					    grid-template-columns: repeat(6, 150px);
 | 
				
			||||||
 | 
					    gap: 20px;
 | 
				
			||||||
 | 
					    margin: 30px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.fug-tile {
 | 
				
			||||||
 | 
					    background: #111;
 | 
				
			||||||
 | 
					    border: 2px solid #ff6600;
 | 
				
			||||||
 | 
					    border-radius: 12px;
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    padding: 16px 8px 8px 8px;
 | 
				
			||||||
 | 
					    box-shadow: 0 0 4px #333;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.fug-icon {
 | 
				
			||||||
 | 
					    width: 48px; height: 48px;
 | 
				
			||||||
 | 
					    margin-bottom: 8px;
 | 
				
			||||||
 | 
					    object-fit: cover;
 | 
				
			||||||
 | 
					    background: #222;
 | 
				
			||||||
 | 
					    border-radius: 5px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.fug-filename {
 | 
				
			||||||
 | 
					    word-break: break-all;
 | 
				
			||||||
 | 
					    margin-bottom: 8px;
 | 
				
			||||||
 | 
					    font-size: 0.95em;
 | 
				
			||||||
 | 
					    text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.fug-progressbar {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 7px;
 | 
				
			||||||
 | 
					    background: #333;
 | 
				
			||||||
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					    margin-top: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.fug-progress {
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    background: #ffb200;
 | 
				
			||||||
 | 
					    transition: width 0.15s;
 | 
				
			||||||
 | 
					    width: 0%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.fug-tile.fug-done {
 | 
				
			||||||
 | 
					    border-color: #0f0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.fug-fileinput {
 | 
				
			||||||
 | 
					    margin: 24px;
 | 
				
			||||||
 | 
					    font-size: 1.1em;
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										172
									
								
								src/snek/static/file-upload-grid.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								src/snek/static/file-upload-grid.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,172 @@
 | 
				
			|||||||
 | 
					import { NjetComponent, NjetDialog } from './njet.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const FUG_ICONS = {
 | 
				
			||||||
 | 
					    file: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><rect x="8" y="8" width="32" height="40" rx="5" fill="white" stroke="%234af" stroke-width="2"/></svg>'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FileUploadGrid extends NjetComponent {
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        super();
 | 
				
			||||||
 | 
					        this._grid = null;
 | 
				
			||||||
 | 
					        this._fileInput = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openFileDialog() {
 | 
				
			||||||
 | 
					        if(this.isBusy){
 | 
				
			||||||
 | 
					            const dialog = new NjetDialog({
 | 
				
			||||||
 | 
					                title: 'Upload in progress',
 | 
				
			||||||
 | 
					                content: 'Please wait for the current upload to complete.',
 | 
				
			||||||
 | 
					                primaryButton: {
 | 
				
			||||||
 | 
					                    text: 'OK',
 | 
				
			||||||
 | 
					                    handler: function () {
 | 
				
			||||||
 | 
					                        this.closest('njet-dialog').remove();
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            return false 
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this._fileInput) {
 | 
				
			||||||
 | 
					            this._fileInput.value = ''; // Allow same file selection twice
 | 
				
			||||||
 | 
					            this._fileInput.click();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    connectedCallback() {
 | 
				
			||||||
 | 
					        this.render();
 | 
				
			||||||
 | 
					        this._fileInput.addEventListener('change', e => this.handleFiles(e.target.files));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    render() {
 | 
				
			||||||
 | 
					        // Root wrapper for styling
 | 
				
			||||||
 | 
					        this.classList.add('fug-root');
 | 
				
			||||||
 | 
					        // Clear previous (if rerendered)
 | 
				
			||||||
 | 
					        this.innerHTML = '';
 | 
				
			||||||
 | 
					        // Input
 | 
				
			||||||
 | 
					        this._fileInput = document.createElement('input');
 | 
				
			||||||
 | 
					        this._fileInput.type = 'file';
 | 
				
			||||||
 | 
					        this._fileInput.className = 'fug-fileinput';
 | 
				
			||||||
 | 
					        this._fileInput.multiple = true;
 | 
				
			||||||
 | 
					        // Grid
 | 
				
			||||||
 | 
					        this._grid = document.createElement('div');
 | 
				
			||||||
 | 
					        this._grid.className = 'fug-grid';
 | 
				
			||||||
 | 
					        this.appendChild(this._fileInput);
 | 
				
			||||||
 | 
					        this.appendChild(this._grid);
 | 
				
			||||||
 | 
					        this.uploadsDone = 0;
 | 
				
			||||||
 | 
					        this.uploadsStarted = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reset(){
 | 
				
			||||||
 | 
					        this.uploadsDone = 0;
 | 
				
			||||||
 | 
					        this.uploadsStarted = 0;
 | 
				
			||||||
 | 
					        this._grid.innerHTML = '';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    get isBusy(){
 | 
				
			||||||
 | 
					        return this.uploadsDone != this.uploadsStarted;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    handleFiles(files) {
 | 
				
			||||||
 | 
					        this.reset()
 | 
				
			||||||
 | 
					        this.uploadsDone = 0;
 | 
				
			||||||
 | 
					        this.uploadsStarted = files.length;
 | 
				
			||||||
 | 
					        [...files].forEach(file => this.createTile(file));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    createTile(file) {
 | 
				
			||||||
 | 
					        const tile = document.createElement('div');
 | 
				
			||||||
 | 
					        tile.className = 'fug-tile';
 | 
				
			||||||
 | 
					        // Icon/Thumbnail
 | 
				
			||||||
 | 
					        let icon;
 | 
				
			||||||
 | 
					        if (file.type.startsWith('image/')) {
 | 
				
			||||||
 | 
					            icon = document.createElement('img');
 | 
				
			||||||
 | 
					            icon.className = 'fug-icon';
 | 
				
			||||||
 | 
					            icon.src = URL.createObjectURL(file);
 | 
				
			||||||
 | 
					            icon.onload = () => URL.revokeObjectURL(icon.src);
 | 
				
			||||||
 | 
					        } else if (file.type.startsWith('video/')) {
 | 
				
			||||||
 | 
					            icon = document.createElement('video');
 | 
				
			||||||
 | 
					            icon.className = 'fug-icon';
 | 
				
			||||||
 | 
					            icon.src = URL.createObjectURL(file);
 | 
				
			||||||
 | 
					            icon.muted = true;
 | 
				
			||||||
 | 
					            icon.playsInline = true;
 | 
				
			||||||
 | 
					            icon.controls = false;
 | 
				
			||||||
 | 
					            icon.preload = 'metadata';
 | 
				
			||||||
 | 
					            icon.onloadeddata = () => {
 | 
				
			||||||
 | 
					                icon.currentTime = 0.5;
 | 
				
			||||||
 | 
					                URL.revokeObjectURL(icon.src);
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            icon = document.createElement('img');
 | 
				
			||||||
 | 
					            icon.className = 'fug-icon';
 | 
				
			||||||
 | 
					            icon.src = FUG_ICONS.file;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // Filename
 | 
				
			||||||
 | 
					        const name = document.createElement('div');
 | 
				
			||||||
 | 
					        name.className = 'fug-filename';
 | 
				
			||||||
 | 
					        name.textContent = file.name;
 | 
				
			||||||
 | 
					        // Progressbar
 | 
				
			||||||
 | 
					        const progressbar = document.createElement('div');
 | 
				
			||||||
 | 
					        progressbar.className = 'fug-progressbar';
 | 
				
			||||||
 | 
					        const progress = document.createElement('div');
 | 
				
			||||||
 | 
					        progress.className = 'fug-progress';
 | 
				
			||||||
 | 
					        progressbar.appendChild(progress);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Tile composition
 | 
				
			||||||
 | 
					        tile.appendChild(icon);
 | 
				
			||||||
 | 
					        tile.appendChild(name);
 | 
				
			||||||
 | 
					        tile.appendChild(progressbar);
 | 
				
			||||||
 | 
					        this._grid.appendChild(tile);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Start upload
 | 
				
			||||||
 | 
					        this.startUpload(file, tile, progress);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    startUpload(file, tile, progress) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.publish('file-uploading', {file: file, tile: tile, progress: progress});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const ws = new WebSocket(`ws://${location.host}/ws`);
 | 
				
			||||||
 | 
					        ws.binaryType = 'arraybuffer';
 | 
				
			||||||
 | 
					        let sent = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ws.onopen = async () => {
 | 
				
			||||||
 | 
					            ws.send(JSON.stringify({type: 'start', filename: file.name}));
 | 
				
			||||||
 | 
					            const chunkSize = 64*1024;
 | 
				
			||||||
 | 
					            while (sent < file.size) {
 | 
				
			||||||
 | 
					                const chunk = file.slice(sent, sent + chunkSize);
 | 
				
			||||||
 | 
					                ws.send(await chunk.arrayBuffer());
 | 
				
			||||||
 | 
					                sent += chunkSize;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            ws.send(JSON.stringify({type: 'end', filename: file.name}));
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ws.onmessage = (event) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const data = JSON.parse(event.data);
 | 
				
			||||||
 | 
					            if (data.type === 'progress') {
 | 
				
			||||||
 | 
					                const pct = Math.min(100, Math.round(100 * data.bytes / file.size));
 | 
				
			||||||
 | 
					                progress.style.width = pct + '%';
 | 
				
			||||||
 | 
					                this.publish('file-uploading', {file: file, tile: tile, progress: progress});
 | 
				
			||||||
 | 
					            } else if (data.type === 'done') {
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                this.uploadsDone += 1;
 | 
				
			||||||
 | 
					                this.publish('file-uploaded', {file: file, tile: tile, progress: progress});
 | 
				
			||||||
 | 
					                progress.style.width = '100%';
 | 
				
			||||||
 | 
					                tile.classList.add('fug-done');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                ws.close();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                this.reset()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					customElements.define('file-upload-grid', FileUploadGrid);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { FileUploadGrid };
 | 
				
			||||||
@ -6,7 +6,12 @@
 | 
				
			|||||||
    <link rel="manifest" href="/manifest.json" />
 | 
					    <link rel="manifest" href="/manifest.json" />
 | 
				
			||||||
  <title>Snek</title>
 | 
					  <title>Snek</title>
 | 
				
			||||||
  <style>{{highlight_styles}}</style>
 | 
					  <style>{{highlight_styles}}</style>
 | 
				
			||||||
 | 
					  <link rel="stylesheet" href="/file-upload-grid.css">
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  <script src="/njet.js" type="module"></script>
 | 
				
			||||||
 | 
					  <script src="/file-upload-grid.js" type="module"></script>
 | 
				
			||||||
  <script src="/polyfills/Promise.withResolvers.js" type="module"></script>
 | 
					  <script src="/polyfills/Promise.withResolvers.js" type="module"></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <script src="/push.js" type="module"></script>
 | 
					  <script src="/push.js" type="module"></script>
 | 
				
			||||||
  <script src="/fancy-button.js" type="module"></script>
 | 
					  <script src="/fancy-button.js" type="module"></script>
 | 
				
			||||||
  <script src="/upload-button.js" type="module"></script>
 | 
					  <script src="/upload-button.js" type="module"></script>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										18
									
								
								src/snek/templates/channel.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/snek/templates/channel.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					<script type="module">
 | 
				
			||||||
 | 
					    import { NjetDialog } from "./njet.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function deleteChannel(channelUid){
 | 
				
			||||||
 | 
					        const dialog = new NjetDialog({
 | 
				
			||||||
 | 
					            title: "Delete channel?",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            buttons: [{
 | 
				
			||||||
 | 
					                text: "No"
 | 
				
			||||||
 | 
					            },{
 | 
				
			||||||
 | 
					                text: "Yes"
 | 
				
			||||||
 | 
					            }]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
@ -3,7 +3,7 @@
 | 
				
			|||||||
{% block header_text %}<h2 style="color:#fff">{{ name }}</h2>{% endblock %} 
 | 
					{% block header_text %}<h2 style="color:#fff">{{ name }}</h2>{% endblock %} 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block main %}
 | 
					{% block main %}
 | 
				
			||||||
 | 
					{% include "channel.html" %}
 | 
				
			||||||
<section class="chat-area">
 | 
					<section class="chat-area">
 | 
				
			||||||
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css">
 | 
					    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css">
 | 
				
			||||||
    <script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
 | 
					    <script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.js"></script>
 | 
				
			||||||
@ -30,6 +30,7 @@
 | 
				
			|||||||
            {{ message.html }}
 | 
					            {{ message.html }}
 | 
				
			||||||
            {% endautoescape %}
 | 
					            {% endautoescape %}
 | 
				
			||||||
        {% endfor %}
 | 
					        {% endfor %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="message-list-bottom"></div>
 | 
					        <div class="message-list-bottom"></div>
 | 
				
			||||||
    </message-list>
 | 
					    </message-list>
 | 
				
			||||||
    <chat-input live-type="true" channel="{{ channel.uid.value }}"></chat-input>
 | 
					    <chat-input live-type="true" channel="{{ channel.uid.value }}"></chat-input>
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user