Implement UI enhancements, change password settings, theme toggle, and custom grid size slider
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m19s

This commit is contained in:
Elijah 2026-05-22 18:22:21 -07:00
parent 425598c278
commit c3b1fe8de1
5 changed files with 226 additions and 26 deletions

View file

@ -6,7 +6,8 @@ import {
Folder, File, Search, Pin, Trash2, HardDrive, Settings,
ChevronRight, Grid3X3, List, Plus, Upload, LogOut,
MoreVertical, Star, Download, Pencil, Copy, Move,
FolderPlus, ArrowUp, X, Check, RefreshCw, Info, Link as LinkIcon, Image as ImageIcon, Film as FilmIcon
FolderPlus, ArrowUp, X, Check, RefreshCw, Info, Link as LinkIcon, Image as ImageIcon, Film as FilmIcon,
Sun, Moon
} from 'lucide-react';
import api from '@/lib/api';
import FilePreview from './FilePreview';
@ -93,11 +94,17 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
const searchTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
const fileInputRef = useRef<HTMLInputElement>(null);
const [gridSize, setGridSize] = useState('200');
const [theme, setTheme] = useState('dark');
useEffect(() => {
api.getSettings().then((data) => {
const theme = data?.settings?.theme || data?.theme;
if (theme) {
if (theme === 'light') {
const s = data?.settings || data;
if (s) {
if (s.grid_size) setGridSize(s.grid_size);
const fetchedTheme = s.theme || 'dark';
setTheme(fetchedTheme);
if (fetchedTheme === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
document.documentElement.classList.remove('dark');
document.documentElement.style.setProperty('color-scheme', 'light');
@ -106,11 +113,33 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
document.documentElement.classList.add('dark');
document.documentElement.style.setProperty('color-scheme', 'dark');
}
localStorage.setItem('theme', theme);
localStorage.setItem('theme', fetchedTheme);
}
}).catch(console.error);
}, []);
async function toggleTheme() {
const newTheme = theme === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
if (newTheme === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
document.documentElement.classList.remove('dark');
document.documentElement.style.setProperty('color-scheme', 'light');
} else {
document.documentElement.setAttribute('data-theme', 'dark');
document.documentElement.classList.add('dark');
document.documentElement.style.setProperty('color-scheme', 'dark');
}
localStorage.setItem('theme', newTheme);
try {
const data = await api.getSettings();
const s = data?.settings || data || {};
await api.updateSettings({ ...s, theme: newTheme });
} catch (err) {
console.error('Failed to save theme', err);
}
}
// Load files on path change
const loadFiles = useCallback(async (path?: string) => {
const targetPath = path ?? currentPath;
@ -438,9 +467,18 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
}
function getFileIcon(file: FileItem, large = false) {
const sizeClass = large ? "w-12 h-12" : "w-5 h-5";
if (file.is_dir) return <Folder className={sizeClass} style={{ color: 'var(--color-accent)' }} />;
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">
<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>
);
}
return <Folder className="w-5 h-5" style={{ color: 'var(--color-accent)' }} />;
}
const sizeClass = large ? "w-20 h-20" : "w-5 h-5";
const ext = file.name.split('.').pop()?.toLowerCase() || '';
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext) || file.mime_type?.startsWith('image/');
const isVideo = ['mp4', 'mkv', 'avi', 'mov', 'webm', 'ogg'].includes(ext) || file.mime_type?.startsWith('video/');
@ -448,7 +486,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
if (ext === 'pdf') {
return (
<div className={`${large ? 'w-12 h-12 rounded-lg text-lg' : 'w-6 h-6 rounded text-[8px]'} flex items-center justify-center font-black tracking-tighter shadow-sm text-white bg-[#ea4335]`}>
<div className={`${large ? 'w-20 h-20 rounded-2xl text-2xl' : 'w-6 h-6 rounded text-[8px]'} flex items-center justify-center font-black tracking-tighter shadow-sm text-white bg-[#ea4335]`}>
PDF
</div>
);
@ -457,16 +495,16 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
const fallbackIcon = (() => {
if (isImage) {
return (
<div className={`${large ? 'w-12 h-12 rounded-lg' : 'w-6 h-6 rounded'} flex items-center justify-center shadow-sm text-white bg-[#3b82f6]`}>
<ImageIcon className={large ? "w-6 h-6 text-white" : "w-3.5 h-3.5 text-white"} />
<div className={`${large ? 'w-20 h-20 rounded-2xl' : 'w-6 h-6 rounded'} flex items-center justify-center shadow-sm text-white bg-[#3b82f6]`}>
<ImageIcon className={large ? "w-10 h-10 text-white" : "w-3.5 h-3.5 text-white"} />
</div>
);
}
if (isVideo) {
return (
<div className={`${large ? 'w-12 h-12 rounded-lg' : 'w-6 h-6 rounded'} flex items-center justify-center shadow-sm text-white bg-[#a855f7]`}>
<FilmIcon className={large ? "w-6 h-6 text-white" : "w-3.5 h-3.5 text-white"} />
<div className={`${large ? 'w-20 h-20 rounded-2xl' : 'w-6 h-6 rounded'} flex items-center justify-center shadow-sm text-white bg-[#a855f7]`}>
<FilmIcon className={large ? "w-10 h-10 text-white" : "w-3.5 h-3.5 text-white"} />
</div>
);
}
@ -676,6 +714,15 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
{(activeSection === 'files' || activeSection === 'pinned' || activeSection === 'trash') && (
<>
<button
onClick={toggleTheme}
className="p-2 rounded-lg transition-colors hover:bg-black/5 dark:hover:bg-white/5"
style={{ color: 'var(--color-text-secondary)' }}
title={theme === 'dark' ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
>
{theme === 'dark' ? <Sun className="w-4.5 h-4.5" /> : <Moon className="w-4.5 h-4.5" />}
</button>
<button
id="info-toggle"
onClick={() => setShowInfo(!showInfo)}
@ -851,7 +898,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
{activeSection === 'files' && displayFiles.some(f => f.is_pinned) && (
<div>
<h3 className="text-xs font-semibold uppercase tracking-wider mb-3 px-1" style={{ color: 'var(--color-text-tertiary)' }}>Pinned</h3>
<div className="grid grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-3">
<div className="grid gap-3" style={{ gridTemplateColumns: `repeat(auto-fill, minmax(${gridSize}px, 1fr))` }}>
{displayFiles.filter(f => f.is_pinned).map((file, idx) => (
<motion.div
key={file.path}
@ -892,7 +939,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
<Pin className="absolute top-2 right-2 w-3 h-3" style={{ color: 'var(--color-warning)' }} />
)}
<div className="flex flex-col items-center gap-3">
<div className="w-24 h-24 flex items-center justify-center">
<div className="w-full h-32 flex items-center justify-center">
{getFileIcon(file, true)}
</div>
{renaming === file.path ? (
@ -930,7 +977,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
{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 grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-3">
<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) => (
<motion.div
key={file.path}
@ -971,7 +1018,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
<Pin className="absolute top-2 right-2 w-3 h-3" style={{ color: 'var(--color-warning)' }} />
)}
<div className="flex flex-col items-center gap-3">
<div className="w-24 h-24 flex items-center justify-center">
<div className="w-full h-32 flex items-center justify-center">
{getFileIcon(file, true)}
</div>
{renaming === file.path ? (
@ -1555,6 +1602,18 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
</div>
</div>
<div className="flex-1 overflow-y-auto p-2 min-h-[200px]">
{moveModalPath !== '.' && (
<button
onClick={() => setMoveModalPath('.')}
className="w-full flex items-center gap-3 p-3 rounded-xl transition-colors text-left mb-1"
style={{ color: 'var(--color-text-primary)' }}
onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = 'var(--color-bg-hover)')}
onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = 'transparent')}
>
<Folder className="w-5 h-5 flex-shrink-0" style={{ color: 'var(--color-accent)' }} />
<span className="text-sm font-medium truncate">Home (All Files)</span>
</button>
)}
{moveFolders.length === 0 ? (
<div className="h-full flex items-center justify-center text-sm italic" style={{ color: 'var(--color-text-tertiary)' }}>
No folders here