Speicherplatz in Dateien, Smilies in Nachrichten
This commit is contained in:
@@ -279,6 +279,22 @@ router.delete('/:id/share/:userId', authenticate, (req, res) => {
|
|||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get('/storage', authenticate, (req, res) => {
|
||||||
|
const uid = req.user.id;
|
||||||
|
const used = db.prepare('SELECT COALESCE(SUM(size),0) AS s FROM files WHERE user_id=?').get(uid).s;
|
||||||
|
const max = parseInt(getSetting('file_max_size_mb') || '50') *
|
||||||
|
parseInt(getSetting('file_max_count') || '20') * 1024 * 1024;
|
||||||
|
// Disk space via Node
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
let diskFree = null;
|
||||||
|
try {
|
||||||
|
const out = execSync(`df -k "${process.env.UPLOAD_DIR || '/data/uploads'}" | tail -1`).toString();
|
||||||
|
const parts = out.trim().split(/\s+/);
|
||||||
|
diskFree = parseInt(parts[3]) * 1024; // available bytes
|
||||||
|
} catch {}
|
||||||
|
res.json({ used, max, diskFree });
|
||||||
|
});
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
router.get('/settings', authenticate, requireAdmin, (req, res) => {
|
router.get('/settings', authenticate, requireAdmin, (req, res) => {
|
||||||
const s = db.prepare('SELECT * FROM admin_settings').all();
|
const s = db.prepare('SELECT * FROM admin_settings').all();
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ export default function Dateien({ toast, mobile }) {
|
|||||||
const [dragOver, setDragOver] = useState(false);
|
const [dragOver, setDragOver] = useState(false);
|
||||||
const [newFolder, setNewFolder] = useState(false);
|
const [newFolder, setNewFolder] = useState(false);
|
||||||
const [folderName, setFolderName] = useState('');
|
const [folderName, setFolderName] = useState('');
|
||||||
|
const [storage, setStorage] = useState(null);
|
||||||
const fileRef = useRef(null);
|
const fileRef = useRef(null);
|
||||||
const { confirm, ConfirmDialog } = useConfirm();
|
const { confirm, ConfirmDialog } = useConfirm();
|
||||||
|
|
||||||
@@ -186,6 +187,7 @@ export default function Dateien({ toast, mobile }) {
|
|||||||
})
|
})
|
||||||
.catch(e=>toast(e.message,'error'))
|
.catch(e=>toast(e.message,'error'))
|
||||||
.finally(()=>setLoading(false));
|
.finally(()=>setLoading(false));
|
||||||
|
api('/tools/dateien/storage').then(setStorage).catch(()=>{});
|
||||||
}, [currentFolderId]);
|
}, [currentFolderId]);
|
||||||
|
|
||||||
useEffect(()=>{load();},[load]);
|
useEffect(()=>{load();},[load]);
|
||||||
@@ -276,7 +278,28 @@ export default function Dateien({ toast, mobile }) {
|
|||||||
return (
|
return (
|
||||||
<div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:960}}>
|
<div style={{padding:mobile?'14px 14px 90px':'36px 44px',maxWidth:960}}>
|
||||||
<ConfirmDialog/>
|
<ConfirmDialog/>
|
||||||
{shareItem && <ShareModal item={shareItem.item} type={shareItem.type} onClose={()=>{setShareItem(null);load();}} toast={toast}/>}
|
{/* ── Speicherplatz ─────────────────────────────────────────────────── */}
|
||||||
|
{storage && (
|
||||||
|
<div style={{marginBottom:14}}>
|
||||||
|
<div style={{display:'flex',justifyContent:'space-between',
|
||||||
|
color:'rgba(255,255,255,0.35)',fontFamily:'monospace',fontSize:9,marginBottom:4}}>
|
||||||
|
<span>{fmtSize(storage.used)} belegt</span>
|
||||||
|
<span>{storage.diskFree!=null ? `${fmtSize(storage.diskFree)} frei auf Disk` : ''}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{height:4,borderRadius:4,background:'rgba(255,255,255,0.06)',overflow:'hidden'}}>
|
||||||
|
<div style={{
|
||||||
|
height:'100%', borderRadius:4,
|
||||||
|
width:`${Math.min(100,(storage.used/storage.max)*100)}%`,
|
||||||
|
background: storage.used/storage.max > 0.85 ? '#ff6b9d' :
|
||||||
|
storage.used/storage.max > 0.6 ? '#ffe66d' : '#4ecdc4',
|
||||||
|
transition:'width 0.4s',
|
||||||
|
}}/>
|
||||||
|
</div>
|
||||||
|
<div style={{color:'rgba(255,255,255,0.2)',fontFamily:'monospace',fontSize:8,marginTop:3,textAlign:'right'}}>
|
||||||
|
Limit: {fmtSize(storage.max)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)} item={shareItem.item} type={shareItem.type} onClose={()=>{setShareItem(null);load();}} toast={toast}/>}
|
||||||
{pwPrompt && <PasswordPromptModal
|
{pwPrompt && <PasswordPromptModal
|
||||||
filename={pwPrompt.originalname}
|
filename={pwPrompt.originalname}
|
||||||
onSubmit={pw=>{ setPwPrompt(null); download(pwPrompt, pw); }}
|
onSubmit={pw=>{ setPwPrompt(null); download(pwPrompt, pw); }}
|
||||||
|
|||||||
@@ -25,12 +25,10 @@ const EMOJI_MAP = {
|
|||||||
|
|
||||||
// Ersetze Kürzel am Ende des Textes (nach Leerzeichen oder Satzanfang)
|
// Ersetze Kürzel am Ende des Textes (nach Leerzeichen oder Satzanfang)
|
||||||
function convertEmojis(text) {
|
function convertEmojis(text) {
|
||||||
let result = text;
|
// Letztes "Wort" vor dem Leerzeichen/Newline prüfen
|
||||||
for (const [code, emoji] of Object.entries(EMOJI_MAP)) {
|
return text.replace(/(\S+)(\s)$/, (_, word, space) => {
|
||||||
const escaped = code.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
return (EMOJI_MAP[word] ?? word) + space;
|
||||||
result = result.replace(new RegExp(`(^|\\s)${escaped}(?=\\s|$)`, 'g'), `$1${emoji}`);
|
});
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Emoji-Picker Daten ────────────────────────────────────────────────────────
|
// ── Emoji-Picker Daten ────────────────────────────────────────────────────────
|
||||||
@@ -86,6 +84,17 @@ export default function Nachrichten({ toast, mobile }) {
|
|||||||
const [showPicker, setShowPicker] = useState(false);
|
const [showPicker, setShowPicker] = useState(false);
|
||||||
const bottomRef = useRef(null);
|
const bottomRef = useRef(null);
|
||||||
const pollRef = useRef(null);
|
const pollRef = useRef(null);
|
||||||
|
const pickerRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showPicker) return;
|
||||||
|
const handler = e => {
|
||||||
|
if (pickerRef.current && !pickerRef.current.contains(e.target)) setShowPicker(false);
|
||||||
|
};
|
||||||
|
document.addEventListener('mousedown', handler);
|
||||||
|
document.addEventListener('touchstart', handler);
|
||||||
|
return () => { document.removeEventListener('mousedown', handler); document.removeEventListener('touchstart', handler); };
|
||||||
|
}, [showPicker]);
|
||||||
|
|
||||||
// ── Initialisierung ──────────────────────────────────────────────────────────
|
// ── Initialisierung ──────────────────────────────────────────────────────────
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -430,7 +439,7 @@ export default function Nachrichten({ toast, mobile }) {
|
|||||||
Senden nicht möglich
|
Senden nicht möglich
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display:'flex', gap:8, alignItems:'flex-end', position:'relative' }}>
|
<div style={{ display:'flex', gap:8, alignItems:'flex-end', position:'relative' }} ref={pickerRef}>
|
||||||
{showPicker && (
|
{showPicker && (
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
onSelect={em => { setText(t => t + em); setShowPicker(false); }}
|
onSelect={em => { setText(t => t + em); setShowPicker(false); }}
|
||||||
|
|||||||
Reference in New Issue
Block a user