Update.
This commit is contained in:
parent
2fc18801a7
commit
62eb1060d9
src/snek
@ -10,7 +10,7 @@
|
||||
import { Schedule } from "./schedule.js";
|
||||
import { EventHandler } from "./event-handler.js";
|
||||
import { Socket } from "./socket.js";
|
||||
|
||||
import { Njet } from "./njet.js";
|
||||
export class RESTClient {
|
||||
debug = false;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { app } from "../app.js";
|
||||
|
||||
class ChatInputComponent extends HTMLElement {
|
||||
import { app } from "./app.js";
|
||||
import { NjetComponent } from "./njet.js";
|
||||
import { FileUploadGrid } from "./file-upload-grid.js";
|
||||
class ChatInputComponent extends NjetComponent {
|
||||
autoCompletions = {
|
||||
"example 1": () => {
|
||||
},
|
||||
@ -161,6 +162,11 @@ class ChatInputComponent extends HTMLElement {
|
||||
|
||||
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("rows", "2");
|
||||
|
||||
@ -174,7 +180,12 @@ class ChatInputComponent extends HTMLElement {
|
||||
this.uploadButton.addEventListener("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.textarea.addEventListener("blur", () => {
|
||||
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" />
|
||||
<title>Snek</title>
|
||||
<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="/push.js" type="module"></script>
|
||||
<script src="/fancy-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 main %}
|
||||
|
||||
{% include "channel.html" %}
|
||||
<section class="chat-area">
|
||||
<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>
|
||||
@ -30,6 +30,7 @@
|
||||
{{ message.html }}
|
||||
{% endautoescape %}
|
||||
{% endfor %}
|
||||
|
||||
<div class="message-list-bottom"></div>
|
||||
</message-list>
|
||||
<chat-input live-type="true" channel="{{ channel.uid.value }}"></chat-input>
|
||||
|
Loading…
Reference in New Issue
Block a user