545 lines
30 KiB
JavaScript
545 lines
30 KiB
JavaScript
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 }) => (
|
||
<button onClick={stop ? e=>{e.stopPropagation();onClick(e);} : onClick}
|
||
title={title}
|
||
style={{ background:`${color}14`, border:`1px solid ${color}35`,
|
||
borderRadius:7, padding:'6px 8px', cursor:'pointer',
|
||
display:'flex', alignItems:'center', justifyContent:'center', gap:4,
|
||
color, fontFamily:'monospace', fontSize:11, flexShrink:0 }}>
|
||
{children}
|
||
</button>
|
||
);
|
||
|
||
// ── 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 (
|
||
<div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.8)',zIndex:6000,
|
||
display:'flex',alignItems:isMobile?'flex-end':'center',justifyContent:'center',padding:isMobile?0:24}}
|
||
onClick={e=>e.target===e.currentTarget&&onClose()}>
|
||
<div style={{background:'#1a1a1e',borderRadius:isMobile?'16px 16px 0 0':16,
|
||
width:'100%',maxWidth:440,padding:'20px 20px 28px',border:'1px solid rgba(255,255,255,0.12)'}}>
|
||
{isMobile&&<div style={{width:36,height:4,background:'rgba(255,255,255,0.15)',borderRadius:2,margin:'0 auto 14px'}}/>}
|
||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:14}}>
|
||
<div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:13,fontWeight:700}}>
|
||
{type==='folder'?'📁':'📎'} {item.name||item.originalname}
|
||
</div>
|
||
<button onClick={onClose} style={{background:'transparent',border:'none',color:'rgba(255,255,255,0.4)',cursor:'pointer',fontSize:18,padding:'0 4px'}}>✕</button>
|
||
</div>
|
||
|
||
{/* Teilen-Formular */}
|
||
{type==='file' && (
|
||
<div style={{marginBottom:10}}>
|
||
<div style={{display:'flex',gap:8,marginBottom:6}}>
|
||
<input value={username} onChange={e=>setUsername(e.target.value)} onKeyDown={e=>e.key==='Enter'&&share()}
|
||
placeholder="Benutzername" autoCapitalize="none" style={{...S.inp,flex:1,fontSize:15}}/>
|
||
<button onClick={share} style={S.btn('#4ecdc4')}>Teilen</button>
|
||
</div>
|
||
<input value={password} onChange={e=>setPassword(e.target.value)}
|
||
placeholder="Passwort (optional)" type="text" autoCapitalize="none"
|
||
style={{...S.inp,fontSize:13,padding:'7px 10px'}}/>
|
||
{password && <div style={{color:'rgba(255,230,109,0.6)',fontSize:9,fontFamily:'monospace',marginTop:4}}>
|
||
⚠ Teile das Passwort separat mit dem Empfänger
|
||
</div>}
|
||
</div>
|
||
)}
|
||
{type==='folder' && (
|
||
<div style={{display:'flex',gap:8,marginBottom:14}}>
|
||
<input value={username} onChange={e=>setUsername(e.target.value)} onKeyDown={e=>e.key==='Enter'&&share()}
|
||
placeholder="Benutzername" autoCapitalize="none" style={{...S.inp,flex:1,fontSize:15}}/>
|
||
<button onClick={share} style={S.btn('#4ecdc4')}>Teilen</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Bestehende Shares */}
|
||
{shares.length>0 && (
|
||
<div>
|
||
<div style={{...S.head,marginBottom:6,marginTop:4}}>GETEILT MIT</div>
|
||
{shares.map(s=>(
|
||
<div key={s.id} style={{display:'flex',justifyContent:'space-between',alignItems:'center',
|
||
padding:'8px 0',borderBottom:'1px solid rgba(255,255,255,0.05)'}}>
|
||
<div>
|
||
<div style={{display:'flex',alignItems:'center',gap:6}}>
|
||
<span style={{color:'rgba(255,255,255,0.8)',fontFamily:'monospace',fontSize:13}}>{s.username}</span>
|
||
{s.has_password ? <span title="Passwortgeschützt" style={{fontSize:10}}>🔑</span> : null}
|
||
</div>
|
||
<div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
|
||
{s.shared_at && `geteilt ${fmtDate(s.shared_at)}`}
|
||
{s.accessed_at
|
||
? <span style={{color:'rgba(78,205,196,0.7)',marginLeft:8}}>✓ zugegriffen {fmtDate(s.accessed_at)}</span>
|
||
: <span style={{color:'rgba(255,255,255,0.25)',marginLeft:8}}>○ noch nicht geöffnet</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
<button onClick={()=>unshare(s.id)} style={S.btn('#ff6b9d',true)}>✕</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
{shares.length===0 && <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:11,marginTop:4}}>Noch mit niemandem geteilt.</div>}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── 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 (
|
||
<div style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.8)',zIndex:7000,
|
||
display:'flex',alignItems:isMobile?'flex-end':'center',justifyContent:'center',padding:isMobile?0:24}}
|
||
onClick={e=>e.target===e.currentTarget&&onCancel()}>
|
||
<div style={{background:'#1a1a1e',borderRadius:isMobile?'16px 16px 0 0':16,
|
||
width:'100%',maxWidth:400,padding:'22px 20px 28px',border:'1px solid rgba(255,255,255,0.12)'}}>
|
||
{isMobile&&<div style={{width:36,height:4,background:'rgba(255,255,255,0.15)',borderRadius:2,margin:'0 auto 14px'}}/>}
|
||
<div style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:13,fontWeight:700,marginBottom:6}}>
|
||
🔒 Passwortgeschützte Datei
|
||
</div>
|
||
<div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:11,marginBottom:14,lineHeight:1.6}}>
|
||
{filename}
|
||
</div>
|
||
<input
|
||
autoFocus
|
||
type="text"
|
||
autoComplete="off"
|
||
data-1p-ignore
|
||
data-lpignore="true"
|
||
value={pw}
|
||
onChange={e=>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}}
|
||
/>
|
||
<div style={{display:'flex',gap:8}}>
|
||
<button onClick={()=>{if(pw)onSubmit(pw);}} disabled={!pw}
|
||
style={{...S.btn('#4ecdc4'),flex:1,textAlign:'center',padding:'10px 0',opacity:pw?1:0.4}}>
|
||
↓ Herunterladen
|
||
</button>
|
||
<button onClick={onCancel} style={{...S.btn('#ff6b9d',true),padding:'0 16px'}}>Abbrechen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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 [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);
|
||
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 (
|
||
<div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:960}}>
|
||
<ConfirmDialog/>
|
||
{/* ── Speicherplatz ─────────────────────────────────────────────────── */}
|
||
{storage && (
|
||
<div style={{marginBottom:14,padding:'10px 12px',background:'rgba(255,255,255,0.02)',
|
||
border:'1px solid rgba(255,255,255,0.06)',borderRadius:8}}>
|
||
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',
|
||
color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10}}>
|
||
<span>{fmtSize(storage.used)} / {storage.totalLimitMb} MB belegt</span>
|
||
<span style={{fontSize:9,color:'rgba(255,255,255,0.25)'}}>max. {storage.maxSizeMb} MB pro Datei · {storage.count} Dateien</span>
|
||
</div>
|
||
<div style={{height:3,borderRadius:3,background:'rgba(255,255,255,0.06)',overflow:'hidden',marginTop:7}}>
|
||
<div style={{
|
||
height:'100%', borderRadius:3,
|
||
width:`${Math.min(100,(storage.used/(storage.totalLimitMb*1024*1024))*100)}%`,
|
||
background: storage.used/(storage.totalLimitMb*1024*1024) > 0.85 ? '#ff6b9d' :
|
||
storage.used/(storage.totalLimitMb*1024*1024) > 0.6 ? '#ffe66d' : '#4ecdc4',
|
||
transition:'width 0.4s',
|
||
}}/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{shareItem && <ShareModal item={shareItem.item} type={shareItem.type} onClose={()=>{setShareItem(null);load();}} toast={toast}/>}
|
||
{pwPrompt && <PasswordPromptModal
|
||
filename={pwPrompt.originalname}
|
||
onSubmit={pw=>{ setPwPrompt(null); download(pwPrompt, pw); }}
|
||
onCancel={()=>setPwPrompt(null)}
|
||
/>}
|
||
<input ref={fileRef} type="file" onChange={onFileInput} style={{display:'none'}}/>
|
||
|
||
{/* ── Seitentitel ──────────────────────────────────────────────────── */}
|
||
<div style={{marginBottom:16,display:'flex',alignItems:'flex-start',justifyContent:'space-between'}}>
|
||
<div>
|
||
<h1 style={{color:'#fff',fontFamily:"'Space Mono',monospace",fontSize:mobile?18:22,margin:0}}>Dateien</h1>
|
||
<div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10,marginTop:3}}>
|
||
{data.own.length} Dateien · {fmtSize(totalSize)}
|
||
</div>
|
||
</div>
|
||
<button onClick={load} title="Neu laden"
|
||
style={{background:'transparent',border:'1px solid rgba(255,255,255,0.1)',borderRadius:8,
|
||
color:'rgba(255,255,255,0.4)',cursor:'pointer',padding:'6px 10px',fontSize:14,fontFamily:'monospace',marginTop:2}}>↺</button>
|
||
</div>
|
||
{/* ── Tabs + Neu-Ordner (überall gleich) ───────────────────────────── */}
|
||
<div style={{display:'flex',gap:6,marginBottom:12,alignItems:'center',flexWrap:'wrap'}}>
|
||
{[
|
||
['own','Dateien', null],
|
||
['shared','Geteilt mit mir', sharedCnt||null],
|
||
['sharedByMe','Geteilt von mir', byMeCnt||null],
|
||
].map(([k,l,cnt])=>(
|
||
<button key={k} onClick={()=>{setTab(k);setSearch('');setCurrentFolderId(null);setFolderPath([]);}} style={{
|
||
padding:'6px 14px',borderRadius:20,fontFamily:'monospace',fontSize:11,cursor:'pointer',
|
||
border:`1px solid ${tab===k?'rgba(78,205,196,0.5)':'rgba(255,255,255,0.12)'}`,
|
||
background:tab===k?'rgba(78,205,196,0.12)':'transparent',
|
||
color:tab===k?'#4ecdc4':'rgba(255,255,255,0.55)',
|
||
}}>{l}{cnt?` (${cnt})`:''}</button>
|
||
))}
|
||
{/* Ordner-Button – immer am Ende der Tab-Reihe wenn own-Tab aktiv */}
|
||
{isOwn && !newFolder && (
|
||
<button onClick={()=>setNewFolder(true)} style={{
|
||
marginLeft:'auto',padding:'6px 12px',borderRadius:20,fontFamily:'monospace',fontSize:11,
|
||
cursor:'pointer',border:'1px solid rgba(255,255,255,0.15)',
|
||
background:'rgba(255,255,255,0.05)',color:'rgba(255,255,255,0.65)',
|
||
display:'flex',alignItems:'center',gap:5,
|
||
}}>📁 + Ordner</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* ── Breadcrumb ────────────────────────────────────────────────────── */}
|
||
{folderPath.length>0 && (
|
||
<div style={{display:'flex',alignItems:'center',gap:4,marginBottom:12,flexWrap:'wrap'}}>
|
||
<button onClick={()=>navigateTo(-1)} style={{background:'transparent',border:'none',
|
||
color:'rgba(255,255,255,0.5)',cursor:'pointer',fontFamily:'monospace',fontSize:11,padding:'2px 6px'}}>
|
||
Dateien
|
||
</button>
|
||
{folderPath.map((p,i)=>(
|
||
<span key={p.id} style={{display:'flex',alignItems:'center',gap:4}}>
|
||
<ChevronIcon size={11} color="rgba(255,255,255,0.25)" dir="right"/>
|
||
<button onClick={()=>navigateTo(i)} style={{
|
||
background:i===folderPath.length-1?'rgba(78,205,196,0.1)':'transparent',
|
||
border:i===folderPath.length-1?'1px solid rgba(78,205,196,0.2)':'none',
|
||
borderRadius:6,color:i===folderPath.length-1?'#4ecdc4':'rgba(255,255,255,0.5)',
|
||
cursor:'pointer',fontFamily:'monospace',fontSize:11,padding:'2px 8px'}}>
|
||
{p.name}
|
||
</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* ── Neuer Ordner Formular ─────────────────────────────────────────── */}
|
||
{newFolder && (
|
||
<div style={{...S.card,marginBottom:10,display:'flex',gap:8,alignItems:'center'}}>
|
||
<span style={{fontSize:18}}>📁</span>
|
||
<input value={folderName} onChange={e=>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}}/>
|
||
<button onClick={createFolder} style={S.btn('#4ecdc4')}>✓</button>
|
||
<button onClick={()=>{setNewFolder(false);setFolderName('');}} style={S.btn('#ff6b9d',true)}>✕</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* ── Suche + Upload-Zone ───────────────────────────────────────────── */}
|
||
<div style={{display:mobile?'block':'flex',gap:8,marginBottom:12}}>
|
||
<div style={{position:'relative',flex:1,marginBottom:mobile?8:0}}>
|
||
<span style={{position:'absolute',left:12,top:'50%',transform:'translateY(-50%)'}}>
|
||
<SearchIcon size={14} color="rgba(255,255,255,0.3)"/>
|
||
</span>
|
||
<input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Suchen…"
|
||
style={{...S.inp,paddingLeft:34,fontSize:15}}/>
|
||
{search&&<button onClick={()=>setSearch('')} style={{position:'absolute',right:10,top:'50%',
|
||
transform:'translateY(-50%)',background:'transparent',border:'none',
|
||
color:'rgba(255,255,255,0.3)',cursor:'pointer',fontSize:16}}>✕</button>}
|
||
</div>
|
||
{/* Drop-Zone */}
|
||
<div onDragOver={e=>{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,
|
||
}}>
|
||
<UploadIcon size={14} color={dragOver?'#4ecdc4':'rgba(255,255,255,0.4)'}/>
|
||
<span style={{color:dragOver?'#4ecdc4':'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:11}}>
|
||
{uploading?'Hochladen…':dragOver?'Loslassen':mobile?'Datei hochladen':'Hier ablegen oder klicken'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{loading&&<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',padding:'8px 0'}}>Lädt…</div>}
|
||
|
||
{/* ── Ordner ────────────────────────────────────────────────────────── */}
|
||
{filtFolders.map(folder => {
|
||
const canManage = !folder.owner || tab==='sharedByMe';
|
||
return (
|
||
<div key={folder.id} style={{...S.card,marginBottom:6,padding:'10px 14px',
|
||
display:'flex',alignItems:'center',gap:10}}>
|
||
<span style={{fontSize:22,flexShrink:0}}>📁</span>
|
||
{/* Info – klickbar zum Navigieren */}
|
||
<div style={{flex:1,minWidth:0,cursor:'pointer'}} onClick={()=>enterFolder(folder,!!folder.owner)}>
|
||
<div style={{color:'#fff',fontFamily:'monospace',fontSize:13,fontWeight:700}}>{folder.name}</div>
|
||
<div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
|
||
{[
|
||
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(' · ')}
|
||
</div>
|
||
</div>
|
||
{/* Aktionen – feste Breite, symmetrisch */}
|
||
<div style={{display:'flex',gap:5,flexShrink:0,alignItems:'center'}}>
|
||
{canManage && (
|
||
<IconBtn onClick={()=>setShareItem({item:folder,type:'folder'})} color="#ffe66d" title="Teilen" stop>🤝</IconBtn>
|
||
)}
|
||
{canManage&&tab==='own' && (
|
||
<IconBtn onClick={()=>delFolder(folder)} color="#ff6b9d" title="Löschen" stop>
|
||
<TrashIcon size={13} color="#ff6b9d"/>
|
||
</IconBtn>
|
||
)}
|
||
<div style={{width:24,display:'flex',alignItems:'center',justifyContent:'center'}}>
|
||
<ChevronIcon size={14} color="rgba(255,255,255,0.3)" dir="right"/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
|
||
{/* ── Dateien ───────────────────────────────────────────────────────── */}
|
||
{filtFiles.map(file => {
|
||
const canManage = isOwn || tab==='sharedByMe';
|
||
return (
|
||
<div key={file.id} style={{...S.card,marginBottom:6,padding:'10px 14px',
|
||
display:'flex',alignItems:'center',gap:10}}>
|
||
<span style={{fontSize:22,flexShrink:0}}>{extIcon(file.originalname)}</span>
|
||
<div style={{flex:1,minWidth:0}}>
|
||
<div style={{color:'#fff',fontFamily:'monospace',fontSize:13,fontWeight:700,
|
||
overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',display:'flex',alignItems:'center',gap:5}}>
|
||
<span style={{overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{file.originalname}</span>
|
||
{file.has_password ? <span title="Passwortgeschützt" style={{fontSize:11,flexShrink:0}}>🔒</span> : null}
|
||
</div>
|
||
<div style={{color:'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:10,marginTop:2}}>
|
||
{fmtSize(file.size)} · {fmtDate(file.created_at)}
|
||
{tab==='shared'?` · von ${file.owner}`:''}
|
||
{tab==='sharedByMe'&&file.shared_with_names?(
|
||
<span>
|
||
{` · → ${file.shared_with_names}`}
|
||
{file.accessed_at
|
||
? <span style={{color:'rgba(78,205,196,0.8)'}}> · ✓ {fmtDate(file.accessed_at)}</span>
|
||
: <span style={{color:'rgba(255,255,255,0.25)'}}> · ○ nicht geöffnet</span>
|
||
}
|
||
</span>
|
||
):''}
|
||
</div>
|
||
</div>
|
||
{/* Aktionen – gleiche Breite wie bei Ordnern */}
|
||
<div style={{display:'flex',gap:5,flexShrink:0,alignItems:'center'}}>
|
||
<IconBtn onClick={()=>download(file)} color="#4ecdc4" title="Herunterladen">
|
||
<DownloadIcon size={13} color="#4ecdc4"/>
|
||
</IconBtn>
|
||
{canManage && (
|
||
<IconBtn onClick={()=>setShareItem({item:file,type:'file'})} color="#ffe66d" title="Teilen">🤝</IconBtn>
|
||
)}
|
||
{isOwn && (
|
||
<IconBtn onClick={()=>delFile(file)} color="#ff6b9d" title="Löschen">
|
||
<TrashIcon size={13} color="#ff6b9d"/>
|
||
</IconBtn>
|
||
)}
|
||
{/* Platzhalter damit Dateien-Zeile gleich breit wie Ordner-Zeile */}
|
||
<div style={{width:24}}/>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
|
||
{!loading&&filtFolders.length===0&&filtFiles.length===0&&(
|
||
<div style={{...S.card,textAlign:'center',padding:40}}>
|
||
<div style={{fontSize:28,marginBottom:10}}>📂</div>
|
||
<div style={{color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:12}}>
|
||
{search?'Keine Treffer.':tab==='own'?'Noch leer – Datei hochladen oder Ordner anlegen.':'Noch nichts geteilt.'}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── 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 (
|
||
<div>
|
||
<div style={{marginBottom:10}}>
|
||
<label style={{...S.head,display:'block',marginBottom:4}}>MAX. DATEIGRÖSSE PRO UPLOAD (MB)</label>
|
||
<input type="number" value={s.file_max_size_mb} onChange={e=>setS(p=>({...p,file_max_size_mb:e.target.value}))} style={{...S.inp,fontSize:15}} min="1"/>
|
||
</div>
|
||
<div style={{marginBottom:10}}>
|
||
<label style={{...S.head,display:'block',marginBottom:4}}>GESAMTLIMIT PRO BENUTZER (MB)</label>
|
||
<input type="number" value={s.file_total_size_mb} onChange={e=>setS(p=>({...p,file_total_size_mb:e.target.value}))} style={{...S.inp,fontSize:15}} min="1"/>
|
||
</div>
|
||
<div style={{marginBottom:14}}>
|
||
<label style={{...S.head,display:'block',marginBottom:4}}>ERLAUBTE ENDUNGEN (kommagetrennt)</label>
|
||
<input value={s.file_allowed_ext} onChange={e=>setS(p=>({...p,file_allowed_ext:e.target.value}))} autoCapitalize="none" style={{...S.inp,fontSize:13}}/>
|
||
</div>
|
||
<button onClick={save} disabled={busy} style={{...S.btn('#4ecdc4'),width:'100%',textAlign:'center',padding:'10px 0'}}>
|
||
{busy?'…':'✓ Speichern'}
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|