From 83b6dad9e3a7849923d143c9bac53c3d2836dc89 Mon Sep 17 00:00:00 2001 From: Dicken Date: Sat, 6 Jun 2026 15:35:35 +0200 Subject: [PATCH] feat: drag&drop upload, default 1h links, Media-Tool mit Kino-Stub --- frontend/src/App.jsx | 39 +++++++++++++++++++--- frontend/src/icons.jsx | 10 ++++++ frontend/src/toolRegistry.js | 11 ++++++- frontend/src/tools/dateien.jsx | 4 +-- frontend/src/tools/media.jsx | 60 ++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 frontend/src/tools/media.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8213b8d..2c23f07 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -2862,6 +2862,7 @@ function PublicUpload({ token }) { const [done, setDone] = useState([]); const [msg, setMsg] = useState(null); const [attLeft, setAttLeft] = useState(null); + const [dragging, setDragging] = useState(false); const fileRef = useRef(null); useEffect(() => { @@ -2892,8 +2893,8 @@ function PublicUpload({ token }) { } catch(e) { setMsg({text:'Netzwerkfehler: '+e.message,type:'error'}); } }; - const doUpload = async () => { - const files = fileRef.current?.files; + const doUpload = async (droppedFiles) => { + const files = droppedFiles || fileRef.current?.files; if (!files?.length) { fileRef.current?.click(); return; } setUploading(true); setMsg(null); const newDone = []; @@ -2919,6 +2920,22 @@ function PublicUpload({ token }) { setUploading(false); }; + const onDragOver = e => { e.preventDefault(); setDragging(true); }; + const onDragLeave = e => { if (!e.currentTarget.contains(e.relatedTarget)) setDragging(false); }; + const onDrop = e => { + e.preventDefault(); setDragging(false); + if (!fileRef.current) return; + // DataTransfer.files is read-only – assign via input to trigger React update + const dt = e.dataTransfer; + if (!dt.files.length) return; + // Assign dropped files to the file input + Object.defineProperty(fileRef.current, 'files', { value: dt.files, writable: true }); + fileRef.current.files = dt.files; + // Trigger display update by reading back + setMsg({text:`${dt.files.length} Datei${dt.files.length!==1?'en':''} ausgewählt`,type:'ok'}); + doUpload(dt.files); + }; + const fmtDate = d => new Date(d).toLocaleString('de-DE',{timeZone:'Europe/Berlin',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}); const C = { @@ -2928,6 +2945,7 @@ function PublicUpload({ token }) { infoBox: {background:'rgba(78,205,196,0.08)',border:'1px solid rgba(78,205,196,0.2)',borderRadius:8,padding:'10px 12px',fontSize:12,color:'rgba(255,255,255,0.6)',marginBottom:14}, btn: {width:'100%',background:'#4ecdc4',border:'none',borderRadius:8,color:'#0d0d0f',cursor:'pointer',fontFamily:"'Courier New',monospace",fontSize:14,fontWeight:700,padding:12,marginTop:12}, msg: t=>({borderRadius:8,padding:'10px 12px',fontSize:13,marginTop:10,...(t==='ok'?{background:'rgba(78,205,196,0.12)',border:'1px solid rgba(78,205,196,0.3)',color:'#4ecdc4'}:t==='warn'?{background:'rgba(255,230,109,0.12)',border:'1px solid rgba(255,230,109,0.3)',color:'#ffe66d'}:{background:'rgba(255,107,157,0.12)',border:'1px solid rgba(255,107,157,0.3)',color:'#ff6b9d'})}), + dropZone: active=>({border:`2px dashed ${active?'#4ecdc4':'rgba(255,255,255,0.18)'}`,borderRadius:12,padding:'28px 16px',textAlign:'center',cursor:'pointer',transition:'border-color 0.15s, background 0.15s',background:active?'rgba(78,205,196,0.07)':'rgba(255,255,255,0.02)',marginBottom:12}), }; return ( @@ -2952,8 +2970,21 @@ function PublicUpload({ token }) { {phase==='upload' && info && <>
✓ Zugang OK · max {info.max_size_mb} MB pro Datei · bis {fmtDate(info.expires_at)}
- - {msg &&
{msg.text}
} diff --git a/frontend/src/icons.jsx b/frontend/src/icons.jsx index c41b695..aeb2261 100644 --- a/frontend/src/icons.jsx +++ b/frontend/src/icons.jsx @@ -153,3 +153,13 @@ export const SkizzeIcon = ({size=20,color='currentColor',sw}) => base(<> export const WrenchIcon = ({size=20,color='currentColor',sw}) => base(<> , size, color, sw); +export const FilmIcon = ({size=20,color='currentColor',sw}) => base(<> + + + + + + + + +, size, color, sw); diff --git a/frontend/src/toolRegistry.js b/frontend/src/toolRegistry.js index 439866f..c713d5a 100644 --- a/frontend/src/toolRegistry.js +++ b/frontend/src/toolRegistry.js @@ -6,7 +6,8 @@ import Linkliste from './tools/linkliste.jsx'; import CodeSchnipsel from './tools/codeschnipsel.jsx'; import Skizze from './tools/skizze.jsx'; import DevTools from './tools/devtools.jsx'; -import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon, MessageIcon, LinkIcon, CodeIcon, SkizzeIcon, WrenchIcon } from './icons.jsx'; +import Media from './tools/media.jsx'; +import { CalculatorIcon, ArchiveIcon, DatabaseIcon, AppsIcon, MessageIcon, LinkIcon, CodeIcon, SkizzeIcon, WrenchIcon, FilmIcon } from './icons.jsx'; export const TOOLS = [ { @@ -81,6 +82,14 @@ export const TOOLS = [ group: 'Werkzeuge', component: DevTools, }, + { + id: 'media', + Icon: FilmIcon, + label: 'Media', + navLabel: 'Media', + group: 'Freizeit', + component: Media, + }, // Neues Tool: { id:'...', Icon:..., label:'...', navLabel:'...', group:'...', component:... }, ]; diff --git a/frontend/src/tools/dateien.jsx b/frontend/src/tools/dateien.jsx index 9263b24..61a7de7 100644 --- a/frontend/src/tools/dateien.jsx +++ b/frontend/src/tools/dateien.jsx @@ -166,7 +166,7 @@ function UploadShareManager({ toast }) { const [logs, setLogs] = useState([]); const [showCreate, setShowCreate] = useState(false); const [showLogs, setShowLogs] = useState(false); - const [form, setForm] = useState({ password:'', expires_hours:24, max_size_mb:10 }); + const [form, setForm] = useState({ password:'', expires_hours:1, max_size_mb:10 }); const [busy, setBusy] = useState(false); const [copied, setCopied] = useState(null); const [sharePws, setSharePws] = useState({}); // {shareId: password} für Passwort-Kopieren @@ -185,7 +185,7 @@ function UploadShareManager({ toast }) { try { const r = await api('/upload-shares', { body: form }); setSharePws(p=>({...p, [r.id]: form.password})); - toast('Upload-Link erstellt ✓'); setShowCreate(false); setForm({password:'',expires_hours:24,max_size_mb:10}); load(); + toast('Upload-Link erstellt ✓'); setShowCreate(false); setForm({password:'',expires_hours:1,max_size_mb:10}); load(); } catch(e) { toast(e.message,'error'); } setBusy(false); }; diff --git a/frontend/src/tools/media.jsx b/frontend/src/tools/media.jsx new file mode 100644 index 0000000..de05ec2 --- /dev/null +++ b/frontend/src/tools/media.jsx @@ -0,0 +1,60 @@ +import { useState } from 'react'; +import { useS } from '../lib.js'; + +const TABS = [ + { id: 'kino', label: '🎬 Kino' }, +]; + +export default function Media() { + const S = useS(); + const [tab, setTab] = useState('kino'); + + return ( +
+ {/* Tab-Bar */} +
+ {TABS.map(t => ( + + ))} +
+ + {tab === 'kino' && } +
+ ); +} + +function KinoTab({ S }) { + return ( +
+
🎬
+
Kino-Feature
+
Wird implementiert – TMDb API-Key erforderlich
+
+ ); +}