import { useState, useEffect, useRef, useCallback } from 'react'; import { api, S } from '../lib.js'; import { useConfirm } from '../confirm.jsx'; import { UploadIcon, DownloadIcon, TrashIcon, SearchIcon, ChevronIcon } from '../icons.jsx'; const fmtSize = b => b<1024?`${b} B`:b<1024*1024?`${(b/1024).toFixed(1)} KB`:`${(b/(1024*1024)).toFixed(1)} MB`; const fmtDate = s => { if (!s) return ''; const d = new Date(String(s).replace(' ','T')); return isNaN(d)?'':d.toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric'}); }; const extIcon = name => { const e = name.split('.').pop().toLowerCase(); if (['jpg','jpeg','png','gif','webp'].includes(e)) return 'πŸ–Ό'; if (e==='pdf') return 'πŸ“„'; if (['zip','rar','7z'].includes(e)) return 'πŸ“¦'; if (['mp4','mov','avi'].includes(e)) return '🎬'; if (['mp3','wav'].includes(e)) return '🎡'; if (['stl','3mf'].includes(e)) return 'πŸ–¨'; if (['txt','md'].includes(e)) return 'πŸ“'; if (['xlsx','csv'].includes(e)) return 'πŸ“Š'; if (['docx','doc'].includes(e)) return 'πŸ“ƒ'; return 'πŸ“Ž'; }; const IconBtn = ({ onClick, color, title, children, stop }) => ( ); // ── Γ–ffentlicher Link-Share Modal ───────────────────────────────────────────── function PublicShareModal({ item, itemType, onClose, toast }) { const [password, setPassword] = useState(() => genPw()); const [expiresH, setExpiresH] = useState(1); const [label, setLabel] = useState(''); const [shares, setShares] = useState([]); const [creating, setCreating] = useState(false); const [newLink, setNewLink] = useState(null); const [newPw, setNewPw] = useState(''); const isFolder = itemType === 'folder'; const base = '/tools/dateien/public-shares'; function genPw() { const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; const lower = 'abcdefghjkmnpqrstuvwxyz'; const digits = '23456789'; const special = '!@#$%&*-+?'; const all = upper + lower + digits + special; // Mindestens je 1 aus jeder Kategorie, Rest zufΓ€llig const pick = s => s[Math.floor(Math.random()*s.length)]; const base = [pick(upper), pick(lower), pick(digits), pick(special)]; const rest = Array.from({length:8}, () => pick(all)); return [...base, ...rest].sort(() => Math.random()-0.5).join(''); } useEffect(() => { api(base).then(d => setShares((d.shares||[]).filter(s => isFolder ? s.folder_id === item.id : s.file_id === item.id ))).catch(()=>{}); }, []); async function createShare() { if (!password.trim()) { toast('Passwort ist Pflicht'); return; } setCreating(true); try { const body = { [isFolder ? 'folder_id' : 'file_id']: item.id, password: password.trim(), expires_hours: expiresH, label: label || undefined, }; const d = await api(base, { body }); const link = `${window.location.origin}/s/${d.token}`; setNewLink(link); setNewPw(password.trim()); setShares(p => [d.share, ...p]); navigator.clipboard?.writeText(link).catch(()=>{}); toast('Link erstellt & kopiert βœ“'); setPassword(genPw()); } catch(e) { toast('Fehler: ' + e.message); } finally { setCreating(false); } } async function deleteShare(id) { await api(`${base}/${id}`, { method:'DELETE' }); setShares(p => p.filter(s => s.id !== id)); toast('Link gelΓΆscht'); } function copyLink(token) { const link = `${window.location.origin}/s/${token}`; navigator.clipboard?.writeText(link).then(() => toast('Link kopiert βœ“')).catch(() => toast(link)); } function fmtExp(exp) { if (!exp) return 'Kein Ablauf'; const d = new Date(exp * 1000); return d.toLocaleDateString('de-DE') + ' ' + d.toLocaleTimeString('de-DE', {hour:'2-digit',minute:'2-digit'}); } const expiryOptions = [ { label:'1 Stunde', h:1 }, { label:'24 Stunden', h:24 }, { label:'7 Tage', h:168 }, { label:'30 Tage', h:720 }, ]; return (
e.target===e.currentTarget&&onClose()}>
πŸ”— Γ–ffentlicher Link
{isFolder ? 'πŸ“' : 'πŸ“„'} {item.name}
{/* Neuer Link */}
NEUEN LINK ERSTELLEN
setLabel(e.target.value)} placeholder="Bezeichnung (optional)" style={{...S.inp, width:'100%', boxSizing:'border-box', marginBottom:8}} /> {/* Passwort Pflicht + Generator */}
setPassword(e.target.value)} placeholder="Passwort (Pflicht)" style={{...S.inp, width:'100%', boxSizing:'border-box', fontFamily:'monospace', letterSpacing:1}} />
{/* Ablauf – kein "Kein Ablauf" */}
{expiryOptions.map(o => ( ))}
{newLink && (
LINK (in Zwischenablage)
{newLink}
PASSWORT
{newPw}
)}
{/* Bestehende Links */} {shares.length > 0 && (
AKTIVE LINKS
{shares.map(s => (
{s.label || 'β€”'} πŸ”’
Ablauf: {fmtExp(s.expires_at)} Β· {s.download_count} Abrufe
))}
)}
); } // ── Share Modal ─────────────────────────────────────────────────────────────── function ShareModal({ item, type, onClose, toast }) { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [shares, setShares] = useState([]); const base = type==='folder' ? `/tools/dateien/folders/${item.id}` : `/tools/dateien/${item.id}`; useEffect(() => { api(`${base}/shares`).then(setShares).catch(()=>{}); }, [base]); const share = async () => { if (!username.trim()) return; try { await api(`${base}/share`, { body: { username: username.trim(), password: password.trim() || undefined } }); toast('Geteilt βœ“'); setUsername(''); setPassword(''); api(`${base}/shares`).then(setShares); } catch(e) { toast(e.message,'error'); } }; const unshare = async uid => { try { await api(`${base}/share/${uid}`,{method:'DELETE'}); setShares(p=>p.filter(s=>s.id!==uid)); toast('Freigabe entfernt'); } catch(e) { toast(e.message,'error'); } }; const isMobile = window.innerWidth<768; return (
e.target===e.currentTarget&&onClose()}>
{isMobile&&
}
{type==='folder'?'πŸ“':'πŸ“Ž'} {item.name||item.originalname}
{/* Teilen-Formular */} {type==='file' && (
setUsername(e.target.value)} onKeyDown={e=>e.key==='Enter'&&share()} placeholder="Benutzername" autoCapitalize="none" style={{...S.inp,flex:1,fontSize:15}}/>
setPassword(e.target.value)} placeholder="Passwort (optional)" type="text" autoCapitalize="none" style={{...S.inp,fontSize:13,padding:'7px 10px'}}/> {password &&
⚠ Teile das Passwort separat mit dem EmpfÀnger
}
)} {type==='folder' && (
setUsername(e.target.value)} onKeyDown={e=>e.key==='Enter'&&share()} placeholder="Benutzername" autoCapitalize="none" style={{...S.inp,flex:1,fontSize:15}}/>
)} {/* Bestehende Shares */} {shares.length>0 && (
GETEILT MIT
{shares.map(s=>(
{s.username} {s.has_password ? πŸ”‘ : null}
{s.shared_at && `geteilt ${fmtDate(s.shared_at)}`} {s.accessed_at ? βœ“ zugegriffen {fmtDate(s.accessed_at)} : β—‹ noch nicht geΓΆffnet }
))}
)} {shares.length===0 &&
Noch mit niemandem geteilt.
}
); } // ── Haupt-Komponente ────────────────────────────────────────────────────────── // ── Passwort-Prompt Modal (ersetzt window.prompt – kein 1Password-Trigger) ──── function PasswordPromptModal({ filename, onSubmit, onCancel }) { const [pw, setPw] = useState(''); const isMobile = window.innerWidth < 768; return (
e.target===e.currentTarget&&onCancel()}>
{isMobile&&
}
πŸ”’ PasswortgeschΓΌtzte Datei
{filename}
setPw(e.target.value)} onKeyDown={e=>{ if(e.key==='Enter'&&pw)onSubmit(pw); if(e.key==='Escape')onCancel(); }} placeholder="Passwort eingeben…" style={{...S.inp,marginBottom:12,fontSize:14}} />
); } // ── Upload-Ordner & Share-Verwaltung ───────────────────────────────────────── function UploadShareManager({ toast }) { const [shares, setShares] = useState([]); const [folder, setFolder] = useState(null); const [logs, setLogs] = useState([]); const [showCreate, setShowCreate] = useState(false); const [showLogs, setShowLogs] = useState(false); 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 const load = () => api('/upload-shares').then(d=>{ setShares(d.shares||[]); setFolder(d.folder); }).catch(()=>{}); useEffect(()=>{ load(); },[]); const genPw = () => { const chars='ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#$%'; setForm(p=>({...p, password: Array.from({length:12},()=>chars[Math.floor(Math.random()*chars.length)]).join('')})); }; const create = async () => { if (!form.password.trim()) { toast('Passwort erforderlich','error'); return; } setBusy(true); 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:1,max_size_mb:10}); load(); } catch(e) { toast(e.message,'error'); } setBusy(false); }; const deactivate = async id => { await api(`/upload-shares/${id}`, { method:'DELETE' }); load(); toast('Link deaktiviert'); }; const removeShare = async id => { await api(`/upload-shares/${id}/remove`, { method:'DELETE' }); setShares(p => p.filter(s => s.id !== id)); toast('Entfernt'); }; const shorten = async (shareId, token) => { setShortening(shareId); const url = `${window.location.origin}/u/${token}`; try { // is.gd – kostenloser URL-Shortener, kein API-Key nΓΆtig const r = await fetch(`https://is.gd/create.php?format=simple&url=${encodeURIComponent(url)}`); const short = await r.text(); if (short.startsWith('http')) { setShortUrls(p=>({...p,[shareId]:short.trim()})); navigator.clipboard?.writeText(short.trim()).catch(()=>{}); toast('Kurz-URL erstellt & kopiert βœ“'); } else { toast('Shortener nicht verfΓΌgbar','error'); } } catch { toast('Shortener nicht erreichbar','error'); } setShortening(null); }; const copyLink = (token) => { const url = `${window.location.origin}/u/${token}`; if (navigator.clipboard) { navigator.clipboard.writeText(url).catch(() => { const ta = document.createElement('textarea'); ta.value = url; ta.style.cssText='position:fixed;opacity:0.01'; document.body.appendChild(ta); ta.focus(); ta.select(); try { document.execCommand('copy'); } catch {} document.body.removeChild(ta); }); } setCopied(token); setTimeout(()=>setCopied(null), 2500); }; const loadLogs = () => { api('/upload-shares/logs').then(setLogs).catch(()=>{}); setShowLogs(true); }; const fmtDate = d => 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 fmtSize = b => b>1024*1024 ? (b/1024/1024).toFixed(1)+' MB' : (b/1024).toFixed(0)+' KB'; const DURATION_OPTS = [ [1,'1 Stunde'],[2,'2 Stunden'],[6,'6 Stunden'],[12,'12 Stunden'], [24,'1 Tag'],[48,'2 Tage'],[72,'3 Tage'],[168,'1 Woche'],[336,'2 Wochen'], ]; const SIZE_OPTS = [1,2,5,10,20,50,100,250]; return (
πŸ“€ Upload-Freigabe
{folder &&
Dateien landen in: {folderIcon(folder)} {folder.name}
}
{/* Erstellen-Formular */} {showCreate && (
UPLOAD-LINK KONFIGURIEREN
GÜLTIGKEITSDAUER
MAX. DATEIGRΓ–SSE
PASSWORT
setForm(p=>({...p,password:e.target.value}))} placeholder="Passwort vergeben" style={{...S.inp,flex:1,fontFamily:'monospace'}}/>
{form.password &&
Passwort: {form.password} – notiere es jetzt
}
)} {/* Aktive Links */} {shares.length===0 && !showCreate && (
πŸ“€
Noch kein Upload-Link. Erstelle einen Link um anderen das Hochladen zu erlauben.
)} {shares.map(s => { const expired = new Date(s.expires_at) < new Date(); return (
{!s.is_active?(s.deactivated_reason==='too_many_attempts'?'⚠ Gesperrt (Fehlversuche)':'Deaktiviert'):expired?'Abgelaufen':'Aktiv'} bis {fmtDate(s.expires_at)} · max {s.max_size_mb} MB/Datei · {s.upload_count||0} Upload(s)
/u/{s.token}
{!!s.is_active && !expired && ( <> {sharePws[s.id] && ( )} )} {!!s.is_active && ( )} {!s.is_active && ( )}
); })} {/* Log-Ansicht */} {showLogs && (
UPLOAD-PROTOKOLL
{logs.length===0 ?
Noch keine Uploads ΓΌber Freigabe-Links.
: logs.map(l=>(
πŸ“„
{l.originalname}
{fmtDate(l.uploaded_at)} Β· {fmtSize(l.size)}
)) }
)}
); } function folderIcon(folder) { if (folder?.is_upload_folder) return 'πŸ“€'; if (folder?.name === '3D-Modelle' || folder?.name?.startsWith('3D')) return 'πŸ“¦'; return 'πŸ“'; } export default function Dateien({ toast, mobile }) { const [data, setData] = useState({own:[],shared:[],sharedByMe:[]}); const [folders, setFolders] = useState({own:[],shared:[],sharedByMe:[]}); const [loading, setLoading] = useState(true); const [currentFolderId, setCurrentFolderId] = useState(null); const [folderPath, setFolderPath] = useState([]); const [tab, setTab] = useState('own'); const [search, setSearch] = useState(''); const [shareItem, setShareItem] = useState(null); const [publicShareItem, setPublicShareItem] = useState(null); const [publicShares, setPublicShares] = useState([]); const [showPublicLinks, setShowPublicLinks] = useState(false); const [pwPrompt, setPwPrompt] = useState(null); // {file} const [uploading, setUploading] = useState(false); const [dragOver, setDragOver] = useState(false); const [newFolder, setNewFolder] = useState(false); const [folderName, setFolderName] = useState(''); const [storage, setStorage] = useState(null); const fileRef = useRef(null); const { confirm, ConfirmDialog } = useConfirm(); const load = useCallback(() => { setLoading(true); api('/tools/dateien/public-shares').then(d => setPublicShares(d.shares||[])).catch(()=>{}); const fq = currentFolderId ? `?folder_id=${currentFolderId}` : ''; const pq = currentFolderId ? `?parent_id=${currentFolderId}` : ''; Promise.all([api(`/tools/dateien${fq}`), api(`/tools/dateien/folders${pq}`)]) .then(([d,f]) => { setData({own:d.own||[],shared:d.shared||[],sharedByMe:d.sharedByMe||[]}); setFolders({own:f.own||[],shared:f.shared||[],sharedByMe:f.sharedByMe||[]}); }) .catch(e=>toast(e.message,'error')) .finally(()=>setLoading(false)); api('/tools/dateien/storage').then(setStorage).catch(()=>{}); }, [currentFolderId]); useEffect(()=>{load();},[load]); // "3D-Modelle" Ordner beim ersten Laden automatisch anlegen useEffect(()=>{ api('/tools/dateien/ensure-folder',{body:{name:'3D-Modelle'}}).catch(()=>{}); },[]); const doUpload = async file => { if (!file) return; setUploading(true); try { const form = new FormData(); form.append('file', file); if (currentFolderId) form.append('folder_id', currentFolderId); await api('/tools/dateien',{method:'POST',body:form,isFile:true}); toast(`${file.name} hochgeladen βœ“`); load(); } catch(e) { toast(e.message,'error'); } finally { setUploading(false); } }; const onFileInput = e => { doUpload(e.target.files?.[0]); e.target.value=''; }; const onDrop = e => { e.preventDefault(); setDragOver(false); doUpload(e.dataTransfer.files?.[0]); }; const download = async (file, pw) => { try { const url = `/api/tools/dateien/${file.id}/download${pw ? `?password=${encodeURIComponent(pw)}` : ''}`; const res = await fetch(url, { headers:{Authorization:`Bearer ${localStorage.getItem('sk_token')}`} }); if (!res.ok) { const err = await res.json().catch(()=>({error:'Fehler'})); if (err.passwordRequired) { setPwPrompt(file); return; } if (pw && res.status===403) { toast('Falsches Passwort', 'error'); return; } toast(err.error,'error'); return; } const blob = await res.blob(); const a = Object.assign(document.createElement('a'), { href: URL.createObjectURL(blob), download: file.originalname, }); document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(()=>URL.revokeObjectURL(a.href),1000); } catch(e){toast(e.message,'error');} }; const delFile = async file => { if (!await confirm(`"${file.originalname}" lΓΆschen?`)) return; try{await api(`/tools/dateien/${file.id}`,{method:'DELETE'});toast('GelΓΆscht');load();} catch(e){toast(e.message,'error');} }; const delFolder = async folder => { if (!await confirm(`Ordner "${folder.name}" und alle Inhalte lΓΆschen?`)) return; try{await api(`/tools/dateien/folders/${folder.id}`,{method:'DELETE'});toast('GelΓΆscht');load();} catch(e){toast(e.message,'error');} }; const createFolder = async () => { if (!folderName.trim()) return; try { const body={name:folderName.trim()}; if (currentFolderId) body.parent_id=currentFolderId; await api('/tools/dateien/folders',{body}); toast('Ordner erstellt'); setFolderName(''); setNewFolder(false); load(); } catch(e){toast(e.message,'error');} }; const enterFolder = (folder, isShared=false) => { setCurrentFolderId(folder.id); setFolderPath(p=>[...p,{id:folder.id,name:folder.name,isShared}]); setSearch(''); }; const navigateTo = idx => { if (idx<0){setCurrentFolderId(null);setFolderPath([]);} else{const t=folderPath[idx];setCurrentFolderId(t.id);setFolderPath(p=>p.slice(0,idx+1));} setSearch(''); }; const sq = search.toLowerCase(); const getFolders = () => tab==='sharedByMe'?(folders.sharedByMe||[]):tab==='shared'?(folders.shared||[]):folders.own; const getFiles = () => tab==='sharedByMe'?(data.sharedByMe||[]):tab==='shared'?data.shared:data.own; const filtFolders = sq ? getFolders().filter(f=>f.name.toLowerCase().includes(sq)) : getFolders(); const filtFiles = sq ? getFiles().filter(f=>f.originalname.toLowerCase().includes(sq)) : getFiles(); const totalSize = data.own.reduce((s,f)=>s+f.size,0); const inSharedFolder = folderPath.some(p=>p.isShared); const isOwn = tab==='own' && !inSharedFolder; const sharedCnt = data.shared.length + (folders.shared||[]).length; const byMeCnt = (data.sharedByMe||[]).length + (folders.sharedByMe||[]).length; return (
{/* ── Speicherplatz ─────────────────────────────────────────────────── */} {storage && (
{fmtSize(storage.used)} / {storage.totalLimitMb} MB belegt max. {storage.maxSizeMb} MB pro Datei Β· {storage.count} Dateien
0.85 ? '#ff6b9d' : storage.used/(storage.totalLimitMb*1024*1024) > 0.6 ? '#ffe66d' : '#4ecdc4', transition:'width 0.4s', }}/>
)} {shareItem && {setShareItem(null);load();}} toast={toast}/>} {publicShareItem && {setPublicShareItem(null);api('/tools/dateien/public-shares').then(d=>setPublicShares(d.shares||[])).catch(()=>{});}} toast={toast}/>} {/* ── Aktive ΓΆffentliche Links ──────────────────────────────────────── */} {publicShares.length > 0 && tab === 'own' && (
setShowPublicLinks(v=>!v)}>
πŸ”— Aktive ΓΆffentliche Links {publicShares.length}
{showPublicLinks ? 'β–²' : 'β–Ό'}
{showPublicLinks && publicShares.map(s => { const name = s.file_name || s.folder_name || s.label || 'β€”'; const isExp = s.expires_at && s.expires_at < Math.floor(Date.now()/1000); const fmtExp = exp => { if (!exp) return '∞'; const d = new Date(exp*1000); return d.toLocaleDateString('de-DE')+' '+d.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'}); }; return (
{s.file_id ? 'πŸ“„' : 'πŸ“'}
{name} {s.label && s.label !== name && ({s.label})}
{isExp?'⚠ Abgelaufen':'Ablauf: '}{!isExp&&fmtExp(s.expires_at)} Β· {s.download_count} Abrufe Β· πŸ”’
); })}
)} {pwPrompt && { setPwPrompt(null); download(pwPrompt, pw); }} onCancel={()=>setPwPrompt(null)} />} {/* ── Seitentitel ──────────────────────────────────────────────────── */}

Dateien

{data.own.length} Dateien Β· {fmtSize(totalSize)}
{/* ── Tabs + Neu-Ordner (ΓΌberall gleich) ───────────────────────────── */}
{[ ['own','Dateien', null], ['shared','Geteilt mit mir', sharedCnt||null], ['sharedByMe','Geteilt von mir', byMeCnt||null], ['upload','πŸ“€ Upload-Freigabe', null], ].map(([k,l,cnt])=>( ))} {/* Ordner-Button – immer am Ende der Tab-Reihe wenn own-Tab aktiv */} {isOwn && !newFolder && ( )}
{/* ── Breadcrumb ────────────────────────────────────────────────────── */} {folderPath.length>0 && (
{folderPath.map((p,i)=>( ))}
)} {/* ── Neuer Ordner Formular ─────────────────────────────────────────── */} {newFolder && (
πŸ“ setFolderName(e.target.value)} onKeyDown={e=>{if(e.key==='Enter')createFolder();if(e.key==='Escape'){setNewFolder(false);setFolderName('');}}} placeholder="Ordnername…" autoFocus style={{...S.inp,flex:1,fontSize:15}}/>
)} {/* ── Upload-Ordner Tab ─────────────────────────────────────────────── */} {tab==='upload' && } {/* ── Suche + Upload-Zone ───────────────────────────────────────────── */} {tab!=='upload' &&
setSearch(e.target.value)} placeholder="Suchen…" style={{...S.inp,paddingLeft:34,fontSize:15}}/> {search&&}
{/* Drop-Zone */}
{e.preventDefault();setDragOver(true);}} onDragLeave={()=>setDragOver(false)} onDrop={onDrop} onClick={()=>fileRef.current?.click()} style={{ display:'flex',alignItems:'center',justifyContent:'center',gap:8, padding:'0 18px',height:44,borderRadius:10,cursor:'pointer', border:`2px dashed ${dragOver?'#4ecdc4':'rgba(255,255,255,0.15)'}`, background:dragOver?'rgba(78,205,196,0.08)':'transparent', transition:'all 0.15s',flexShrink:0, minWidth:mobile?'100%':200, }}> {uploading?'Hochladen…':dragOver?'Loslassen':mobile?'Datei hochladen':'Hier ablegen oder klicken'}
} {loading&&tab!=='upload'&&
LΓ€dt…
} {/* ── Ordner ────────────────────────────────────────────────────────── */} {tab!=='upload' && filtFolders.map(folder => { const canManage = !folder.owner || tab==='sharedByMe'; return (
{folderIcon(folder)} {/* Info – klickbar zum Navigieren */}
enterFolder(folder,!!folder.owner)}>
{folder.name}
{[ folder.owner&&tab!=='sharedByMe'?`von ${folder.owner}`:null, folder.subCount>0?`${folder.subCount} Ordner`:null, folder.fileCount>0?`${folder.fileCount} Datei${folder.fileCount!==1?'en':''}`:null, folder.fileCount>0&&folder.totalSize>0?fmtSize(folder.totalSize):null, folder.subCount===0&&folder.fileCount===0?'leer':null, tab==='sharedByMe'&&folder.shared_with_names?`β†’ ${folder.shared_with_names}`:null, ].filter(Boolean).join(' Β· ')}
{/* Aktionen – feste Breite, symmetrisch */}
{canManage && ( <>setShareItem({item:folder,type:'folder'})} color="#ffe66d" title="Teilen" stop>🀝 {e.stopPropagation();setPublicShareItem({item:folder,type:'folder'});}} color="#4ade80" title="Γ–ffentlicher Link" stop>πŸ”— )} {canManage&&tab==='own' && ( delFolder(folder)} color="#ff6b9d" title="LΓΆschen" stop> )}
); })} {/* ── Dateien ───────────────────────────────────────────────────────── */} {tab!=='upload' && filtFiles.map(file => { const canManage = isOwn || tab==='sharedByMe'; return (
{extIcon(file.originalname)}
{file.originalname} {file.has_password ? πŸ”’ : null}
{fmtSize(file.size)} Β· {fmtDate(file.created_at)} {tab==='shared'?` Β· von ${file.owner}`:''} {tab==='sharedByMe'&&file.shared_with_names?( {` Β· β†’ ${file.shared_with_names}`} {file.accessed_at ? Β· βœ“ {fmtDate(file.accessed_at)} : Β· β—‹ nicht geΓΆffnet } ):''}
{/* Aktionen – gleiche Breite wie bei Ordnern */}
download(file)} color="#4ecdc4" title="Herunterladen"> {canManage && ( <>setShareItem({item:file,type:'file'})} color="#ffe66d" title="Teilen">🀝 setPublicShareItem({item:file,type:'file'})} color="#4ade80" title="Γ–ffentlicher Link">πŸ”— )} {isOwn && ( delFile(file)} color="#ff6b9d" title="LΓΆschen"> )} {/* Platzhalter damit Dateien-Zeile gleich breit wie Ordner-Zeile */}
); })} {!loading&&filtFolders.length===0&&filtFiles.length===0&&(
πŸ“‚
{search?'Keine Treffer.':tab==='own'?'Noch leer – Datei hochladen oder Ordner anlegen.':tab==='upload'?'':' Noch nichts geteilt.'}
)}
); } // ── Datei-Einstellungen (Admin) ─────────────────────────────────────────────── export function DateiSettings({ toast }) { const [s, setS] = useState({file_max_size_mb:'50',file_total_size_mb:'500',file_allowed_ext:'.pdf,.jpg,.jpeg,.png,.gif,.zip,.txt,.docx,.xlsx,.mp4,.stl,.3mf'}); const [busy, setBusy] = useState(false); useEffect(()=>{api('/tools/dateien/settings').then(d=>setS(prev=>({...prev,...d}))).catch(()=>{});},[]); const save = async()=>{ setBusy(true); try{await api('/tools/dateien/settings',{method:'PUT',body:s});toast('Gespeichert βœ“');} catch(e){toast(e.message,'error');}finally{setBusy(false);} }; return (
setS(p=>({...p,file_max_size_mb:e.target.value}))} style={{...S.inp,fontSize:15}} min="1"/>
setS(p=>({...p,file_total_size_mb:e.target.value}))} style={{...S.inp,fontSize:15}} min="1"/>
setS(p=>({...p,file_allowed_ext:e.target.value}))} autoCapitalize="none" style={{...S.inp,fontSize:13}}/>
); }