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