feat: drag&drop upload, default 1h links, Media-Tool mit Kino-Stub
This commit is contained in:
@@ -2862,6 +2862,7 @@ function PublicUpload({ token }) {
|
|||||||
const [done, setDone] = useState([]);
|
const [done, setDone] = useState([]);
|
||||||
const [msg, setMsg] = useState(null);
|
const [msg, setMsg] = useState(null);
|
||||||
const [attLeft, setAttLeft] = useState(null);
|
const [attLeft, setAttLeft] = useState(null);
|
||||||
|
const [dragging, setDragging] = useState(false);
|
||||||
const fileRef = useRef(null);
|
const fileRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -2892,8 +2893,8 @@ function PublicUpload({ token }) {
|
|||||||
} catch(e) { setMsg({text:'Netzwerkfehler: '+e.message,type:'error'}); }
|
} catch(e) { setMsg({text:'Netzwerkfehler: '+e.message,type:'error'}); }
|
||||||
};
|
};
|
||||||
|
|
||||||
const doUpload = async () => {
|
const doUpload = async (droppedFiles) => {
|
||||||
const files = fileRef.current?.files;
|
const files = droppedFiles || fileRef.current?.files;
|
||||||
if (!files?.length) { fileRef.current?.click(); return; }
|
if (!files?.length) { fileRef.current?.click(); return; }
|
||||||
setUploading(true); setMsg(null);
|
setUploading(true); setMsg(null);
|
||||||
const newDone = [];
|
const newDone = [];
|
||||||
@@ -2919,6 +2920,22 @@ function PublicUpload({ token }) {
|
|||||||
setUploading(false);
|
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 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 = {
|
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},
|
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},
|
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'})}),
|
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 (
|
return (
|
||||||
@@ -2952,8 +2970,21 @@ function PublicUpload({ token }) {
|
|||||||
|
|
||||||
{phase==='upload' && info && <>
|
{phase==='upload' && info && <>
|
||||||
<div style={C.infoBox}>✓ Zugang OK · max {info.max_size_mb} MB pro Datei · bis {fmtDate(info.expires_at)}</div>
|
<div style={C.infoBox}>✓ Zugang OK · max {info.max_size_mb} MB pro Datei · bis {fmtDate(info.expires_at)}</div>
|
||||||
<input type="file" ref={fileRef} multiple style={{...C.inp,padding:8,cursor:'pointer'}}/>
|
<div
|
||||||
<button onClick={doUpload} disabled={uploading} style={{...C.btn,opacity:uploading?0.5:1}}>
|
style={C.dropZone(dragging)}
|
||||||
|
onDragOver={onDragOver}
|
||||||
|
onDragLeave={onDragLeave}
|
||||||
|
onDrop={onDrop}
|
||||||
|
onClick={()=>fileRef.current?.click()}
|
||||||
|
>
|
||||||
|
<div style={{fontSize:28,marginBottom:6}}>{dragging?'📂':'📁'}</div>
|
||||||
|
<div style={{color:dragging?'#4ecdc4':'rgba(255,255,255,0.5)',fontSize:13}}>
|
||||||
|
{dragging?'Loslassen zum Hochladen':'Dateien hierher ziehen oder klicken'}
|
||||||
|
</div>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.25)',fontSize:11,marginTop:4}}>max {info.max_size_mb} MB pro Datei</div>
|
||||||
|
</div>
|
||||||
|
<input type="file" ref={fileRef} multiple onChange={()=>doUpload()} style={{display:'none'}}/>
|
||||||
|
<button onClick={()=>doUpload()} disabled={uploading} style={{...C.btn,opacity:uploading?0.5:1}}>
|
||||||
{uploading?'⏳ Lädt hoch…':'⬆ Hochladen'}
|
{uploading?'⏳ Lädt hoch…':'⬆ Hochladen'}
|
||||||
</button>
|
</button>
|
||||||
{msg && <div style={C.msg(msg.type)}>{msg.text}</div>}
|
{msg && <div style={C.msg(msg.type)}>{msg.text}</div>}
|
||||||
|
|||||||
@@ -153,3 +153,13 @@ export const SkizzeIcon = ({size=20,color='currentColor',sw}) => base(<>
|
|||||||
export const WrenchIcon = ({size=20,color='currentColor',sw}) => base(<>
|
export const WrenchIcon = ({size=20,color='currentColor',sw}) => base(<>
|
||||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" stroke={color} strokeWidth={sw||1.5} fill="none" strokeLinecap="round" strokeLinejoin="round"/>
|
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" stroke={color} strokeWidth={sw||1.5} fill="none" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
</>, size, color, sw);
|
</>, size, color, sw);
|
||||||
|
export const FilmIcon = ({size=20,color='currentColor',sw}) => base(<>
|
||||||
|
<rect x="2" y="2" width="20" height="20" rx="2.18" stroke={color} strokeWidth={sw||1.5} fill="none" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<line x1="7" y1="2" x2="7" y2="22" stroke={color} strokeWidth={sw||1.5} strokeLinecap="round"/>
|
||||||
|
<line x1="17" y1="2" x2="17" y2="22" stroke={color} strokeWidth={sw||1.5} strokeLinecap="round"/>
|
||||||
|
<line x1="2" y1="12" x2="22" y2="12" stroke={color} strokeWidth={sw||1.5} strokeLinecap="round"/>
|
||||||
|
<line x1="2" y1="7" x2="7" y2="7" stroke={color} strokeWidth={sw||1.5} strokeLinecap="round"/>
|
||||||
|
<line x1="2" y1="17" x2="7" y2="17" stroke={color} strokeWidth={sw||1.5} strokeLinecap="round"/>
|
||||||
|
<line x1="17" y1="17" x2="22" y2="17" stroke={color} strokeWidth={sw||1.5} strokeLinecap="round"/>
|
||||||
|
<line x1="17" y1="7" x2="22" y2="7" stroke={color} strokeWidth={sw||1.5} strokeLinecap="round"/>
|
||||||
|
</>, size, color, sw);
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import Linkliste from './tools/linkliste.jsx';
|
|||||||
import CodeSchnipsel from './tools/codeschnipsel.jsx';
|
import CodeSchnipsel from './tools/codeschnipsel.jsx';
|
||||||
import Skizze from './tools/skizze.jsx';
|
import Skizze from './tools/skizze.jsx';
|
||||||
import DevTools from './tools/devtools.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 = [
|
export const TOOLS = [
|
||||||
{
|
{
|
||||||
@@ -81,6 +82,14 @@ export const TOOLS = [
|
|||||||
group: 'Werkzeuge',
|
group: 'Werkzeuge',
|
||||||
component: DevTools,
|
component: DevTools,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'media',
|
||||||
|
Icon: FilmIcon,
|
||||||
|
label: 'Media',
|
||||||
|
navLabel: 'Media',
|
||||||
|
group: 'Freizeit',
|
||||||
|
component: Media,
|
||||||
|
},
|
||||||
// Neues Tool: { id:'...', Icon:..., label:'...', navLabel:'...', group:'...', component:... },
|
// Neues Tool: { id:'...', Icon:..., label:'...', navLabel:'...', group:'...', component:... },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ function UploadShareManager({ toast }) {
|
|||||||
const [logs, setLogs] = useState([]);
|
const [logs, setLogs] = useState([]);
|
||||||
const [showCreate, setShowCreate] = useState(false);
|
const [showCreate, setShowCreate] = useState(false);
|
||||||
const [showLogs, setShowLogs] = 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 [busy, setBusy] = useState(false);
|
||||||
const [copied, setCopied] = useState(null);
|
const [copied, setCopied] = useState(null);
|
||||||
const [sharePws, setSharePws] = useState({}); // {shareId: password} für Passwort-Kopieren
|
const [sharePws, setSharePws] = useState({}); // {shareId: password} für Passwort-Kopieren
|
||||||
@@ -185,7 +185,7 @@ function UploadShareManager({ toast }) {
|
|||||||
try {
|
try {
|
||||||
const r = await api('/upload-shares', { body: form });
|
const r = await api('/upload-shares', { body: form });
|
||||||
setSharePws(p=>({...p, [r.id]: form.password}));
|
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'); }
|
} catch(e) { toast(e.message,'error'); }
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
};
|
};
|
||||||
|
|||||||
60
frontend/src/tools/media.jsx
Normal file
60
frontend/src/tools/media.jsx
Normal file
@@ -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 (
|
||||||
|
<div style={{ padding: '0 0 40px' }}>
|
||||||
|
{/* Tab-Bar */}
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginBottom: 24, flexWrap: 'wrap' }}>
|
||||||
|
{TABS.map(t => (
|
||||||
|
<button
|
||||||
|
key={t.id}
|
||||||
|
onClick={() => setTab(t.id)}
|
||||||
|
style={{
|
||||||
|
padding: '7px 16px',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: tab === t.id ? '1px solid #4ecdc4' : '1px solid rgba(255,255,255,0.12)',
|
||||||
|
background: tab === t.id ? 'rgba(78,205,196,0.15)' : 'rgba(255,255,255,0.04)',
|
||||||
|
color: tab === t.id ? '#4ecdc4' : 'rgba(255,255,255,0.6)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontFamily: "'Courier New', monospace",
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: tab === t.id ? 700 : 400,
|
||||||
|
transition: 'all 0.15s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tab === 'kino' && <KinoTab S={S} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function KinoTab({ S }) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
background: 'rgba(255,255,255,0.03)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.08)',
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: '32px 24px',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'rgba(255,255,255,0.35)',
|
||||||
|
fontFamily: "'Courier New', monospace",
|
||||||
|
fontSize: 14,
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: 40, marginBottom: 12 }}>🎬</div>
|
||||||
|
<div style={{ fontSize: 15, color: 'rgba(255,255,255,0.5)', marginBottom: 8 }}>Kino-Feature</div>
|
||||||
|
<div style={{ fontSize: 12 }}>Wird implementiert – TMDb API-Key erforderlich</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user