This commit is contained in:
retoor 2025-06-10 11:28:46 +02:00
parent 2fc18801a7
commit 62eb1060d9
7 changed files with 271 additions and 5 deletions

View File

@ -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;

View File

@ -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("");

View 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;
}

View 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 };

View File

@ -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>

View 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>

View File

@ -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>