feat: implement trash auto-purge, settings layout update, larger folder icons, and legacy version cleanup
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m18s

This commit is contained in:
Elijah 2026-05-22 18:41:16 -07:00
parent c3b1fe8de1
commit 71ba029fbc
8 changed files with 256 additions and 103 deletions

View file

@ -1,6 +1,6 @@
'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Folder, File, Search, Pin, Trash2, HardDrive, Settings,
@ -25,6 +25,7 @@ interface FileItem {
is_pinned: boolean;
is_trashed: boolean;
mod_time: string;
trashed_at?: string;
checksum?: string;
}
@ -96,12 +97,14 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
const [gridSize, setGridSize] = useState('200');
const [theme, setTheme] = useState('dark');
const [trashAutoPurgeDays, setTrashAutoPurgeDays] = useState(30);
useEffect(() => {
api.getSettings().then((data) => {
const s = data?.settings || data;
if (s) {
if (s.grid_size) setGridSize(s.grid_size);
if (s.trash_auto_purge_days) setTrashAutoPurgeDays(parseInt(s.trash_auto_purge_days, 10));
const fetchedTheme = s.theme || 'dark';
setTheme(fetchedTheme);
if (fetchedTheme === 'light') {
@ -228,6 +231,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
setFiles(data.items?.map((item: any) => ({
...item,
is_trashed: true,
trashed_at: item.trashed_at,
})) || []);
} catch {
setFiles([]);
@ -470,7 +474,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
if (file.is_dir) {
if (large) {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-accent)" className="w-20 h-20 opacity-90 drop-shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-accent)" className="w-28 h-28 opacity-90 drop-shadow-sm">
<path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z" />
</svg>
);
@ -554,6 +558,60 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
return sortAsc ? cmp : -cmp;
});
const groupedFiles = useMemo(() => {
const baseFiles = displayFiles.filter(f => activeSection !== 'files' || !f.is_pinned);
if (activeSection !== 'trash' || trashAutoPurgeDays <= 0) {
const defaultName = activeSection === 'files' && displayFiles.some(f => f.is_pinned) ? 'All Files' : '';
return [{ name: defaultName, files: baseFiles }];
}
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
const groupsMap: { [key: string]: FileItem[] } = {};
const groupSortValue: { [key: string]: number } = {};
baseFiles.forEach(f => {
if (!f.trashed_at) {
if (!groupsMap['Unknown']) groupsMap['Unknown'] = [];
groupsMap['Unknown'].push(f);
groupSortValue['Unknown'] = 999;
return;
}
let tStr = f.trashed_at;
if (tStr.includes(' ')) tStr = tStr.replace(' ', 'T');
const trashedDate = new Date(tStr);
if (isNaN(trashedDate.getTime())) {
if (!groupsMap['Unknown']) groupsMap['Unknown'] = [];
groupsMap['Unknown'].push(f);
groupSortValue['Unknown'] = 999;
return;
}
const trashedDay = new Date(trashedDate.getFullYear(), trashedDate.getMonth(), trashedDate.getDate()).getTime();
if (trashedDay === today) {
if (!groupsMap['Deleted Today']) groupsMap['Deleted Today'] = [];
groupsMap['Deleted Today'].push(f);
groupSortValue['Deleted Today'] = -1;
} else {
const expirationDate = trashedDate.getTime() + (trashAutoPurgeDays * 24 * 60 * 60 * 1000);
const daysRemaining = Math.ceil((expirationDate - now.getTime()) / (1000 * 60 * 60 * 24));
let groupName = `Deletes in ${daysRemaining} days`;
if (daysRemaining === 1) groupName = `Deletes in 1 day`;
else if (daysRemaining <= 0) groupName = `Pending Deletion`;
if (!groupsMap[groupName]) groupsMap[groupName] = [];
groupsMap[groupName].push(f);
groupSortValue[groupName] = daysRemaining;
}
});
const sortedGroupNames = Object.keys(groupsMap).sort((a, b) => groupSortValue[a] - groupSortValue[b]);
return sortedGroupNames.map(name => ({ name, files: groupsMap[name] }));
}, [displayFiles, activeSection, trashAutoPurgeDays]);
return (
<div className="flex h-screen" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
{/* === SIDEBAR === */}
@ -602,33 +660,21 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
</button>
))}
<div className="pt-4">
<div className="h-px mx-2 mb-4" style={{ backgroundColor: 'var(--color-border-subtle)' }} />
<button
id="nav-settings"
onClick={() => handleSectionChange('settings')}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-150"
style={{
backgroundColor: activeSection === 'settings' ? 'var(--color-accent-subtle)' : 'transparent',
color: activeSection === 'settings' ? 'var(--color-accent)' : 'var(--color-text-secondary)',
}}
>
<Settings className="w-4.5 h-4.5" />
Settings
</button>
</div>
</nav>
{/* Logout */}
{/* Settings Footer */}
<div className="px-3 py-4 border-t" style={{ borderColor: 'var(--color-border-subtle)' }}>
<button
id="logout-button"
onClick={onLogout}
id="nav-settings"
onClick={() => handleSectionChange('settings')}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-150 hover:opacity-80"
style={{ color: 'var(--color-danger)' }}
style={{
backgroundColor: activeSection === 'settings' ? 'var(--color-accent-subtle)' : 'transparent',
color: activeSection === 'settings' ? 'var(--color-accent)' : 'var(--color-text-secondary)',
}}
>
<LogOut className="w-4.5 h-4.5" />
Sign Out
<Settings className="w-4.5 h-4.5" />
Settings
</button>
</div>
</aside>
@ -674,30 +720,32 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
</div>
{/* Search Bar */}
<div className="relative w-72">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
<input
id="search-input"
type="text"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search files..."
className="w-full pl-9 pr-4 py-2 rounded-lg text-sm outline-none transition-all"
style={{
backgroundColor: 'var(--color-bg-tertiary)',
border: '1px solid var(--color-border)',
color: 'var(--color-text-primary)',
}}
/>
{searchQuery && (
<button
onClick={() => { setSearchQuery(''); setSearchResults(null); }}
className="absolute right-3 top-1/2 -translate-y-1/2"
>
<X className="w-3.5 h-3.5" style={{ color: 'var(--color-text-tertiary)' }} />
</button>
)}
</div>
{activeSection !== 'settings' && (
<div className="relative w-72">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
<input
id="search-input"
type="text"
value={searchQuery}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search files..."
className="w-full pl-9 pr-4 py-2 rounded-lg text-sm outline-none transition-all"
style={{
backgroundColor: 'var(--color-bg-tertiary)',
border: '1px solid var(--color-border)',
color: 'var(--color-text-primary)',
}}
/>
{searchQuery && (
<button
onClick={() => { setSearchQuery(''); setSearchResults(null); }}
className="absolute right-3 top-1/2 -translate-y-1/2"
>
<X className="w-3.5 h-3.5" style={{ color: 'var(--color-text-tertiary)' }} />
</button>
)}
</div>
)}
{/* Action Buttons */}
<div className="flex items-center gap-2">
@ -805,7 +853,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
{/* Dynamic Content Area */}
{activeSection === 'settings' ? (
<SettingsPage />
<SettingsPage onLogout={onLogout} />
) : activeSection === 'storage' ? (
<StorageDashboard />
) : (
@ -974,11 +1022,13 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
)}
<div>
{activeSection === 'files' && displayFiles.some(f => f.is_pinned) && (
<h3 className="text-xs font-semibold uppercase tracking-wider mb-3 px-1" style={{ color: 'var(--color-text-tertiary)' }}>All Files</h3>
)}
<div className="grid gap-3" style={{ gridTemplateColumns: `repeat(auto-fill, minmax(${gridSize}px, 1fr))` }}>
{displayFiles.filter(f => activeSection !== 'files' || !f.is_pinned).map((file, idx) => (
{groupedFiles.map((group, groupIdx) => (
<div key={group.name} className={groupIdx > 0 || (activeSection === 'files' && displayFiles.some(f => f.is_pinned)) ? 'mt-8' : ''}>
{group.name && (
<h3 className="text-xs font-semibold uppercase tracking-wider mb-3 px-1" style={{ color: 'var(--color-text-tertiary)' }}>{group.name}</h3>
)}
<div className="grid gap-3" style={{ gridTemplateColumns: `repeat(auto-fill, minmax(${gridSize}px, 1fr))` }}>
{group.files.map((file, idx) => (
<motion.div
key={file.path}
draggable
@ -1047,8 +1097,10 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
)}
</div>
</motion.div>
))}
</div>
))}
</div>
</div>
))}
</div>
</div>
) : (
@ -1118,14 +1170,19 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
)}
<div>
<div className="space-y-0.5">
<div className="grid grid-cols-[1fr_100px_140px_40px] gap-4 px-4 py-2 text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--color-text-tertiary)' }}>
<span>{activeSection === 'files' && displayFiles.some(f => f.is_pinned) ? 'All Files' : 'Name'}</span>
<span>Size</span>
<span>Modified</span>
<span></span>
</div>
{displayFiles.filter(f => activeSection !== 'files' || !f.is_pinned).map((file, idx) => (
{groupedFiles.map((group, groupIdx) => (
<div key={group.name} className={groupIdx > 0 || (activeSection === 'files' && displayFiles.some(f => f.is_pinned)) ? 'mt-8' : ''}>
{group.name && (
<h3 className="text-xs font-semibold uppercase tracking-wider mb-2 px-4" style={{ color: 'var(--color-text-tertiary)' }}>{group.name}</h3>
)}
<div className="space-y-0.5">
<div className="grid grid-cols-[1fr_100px_140px_40px] gap-4 px-4 py-2 text-xs font-medium uppercase tracking-wider" style={{ color: 'var(--color-text-tertiary)' }}>
<span>Name</span>
<span>Size</span>
<span>Modified</span>
<span></span>
</div>
{group.files.map((file, idx) => (
<motion.div
key={file.path}
draggable
@ -1179,11 +1236,13 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
<MoreVertical className="w-4 h-4" style={{ color: 'var(--color-text-tertiary)' }} />
</button>
</motion.div>
))}
</div>
))}
</div>
</div>
))}
</div>
</div>
)}
)}
</div>
{/* Bulk Actions Bar */}