Initial Mobile UI
All checks were successful
Automated Container Build / build-and-push (push) Successful in 40s

This commit is contained in:
Elijah 2026-05-24 08:48:22 -07:00
parent 322685e3a3
commit 95a2b3ad0e
2 changed files with 171 additions and 62 deletions

View file

@ -8,10 +8,10 @@ import {
MoreVertical, Star, Download, Pencil, Copy, Move,
FolderPlus, ArrowUp, X, Check, RefreshCw, Info, Link as LinkIcon, Image as ImageIcon, Film as FilmIcon,
Sun, Moon, Music as MusicIcon, Code as CodeIcon, Archive as ArchiveIcon, CornerDownRight,
CheckCircle, AlertCircle
CheckCircle, AlertCircle, Menu
} from 'lucide-react';
import api from '@/lib/api';
import { useIsMobile } from '@/hooks/useIsMobile';
const Folder = ({ className, style }: { className?: string, style?: React.CSSProperties }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="24" height="24" className={className} style={style}>
<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" />
@ -205,6 +205,11 @@ function ThumbnailImage({ checksum, fallbackIcon, downloadToken }: { checksum: s
}
export default function FileExplorer({ onLogout }: FileExplorerProps) {
const isMobile = useIsMobile();
const [sidebarOpen, setSidebarOpen] = useState(false);
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
const [currentPath, setCurrentPath] = useState(() => {
if (typeof window !== 'undefined') {
return new URLSearchParams(window.location.search).get('path') || '.';
@ -644,7 +649,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
});
}
function handleContextMenu(e: React.MouseEvent, file: FileItem) {
function handleContextMenu(e: React.MouseEvent | { preventDefault: () => void, clientX: number, clientY: number }, file: FileItem) {
e.preventDefault();
const estimatedHeight = activeSection === 'trash' ? 100 : 320;
setContextMenu({
@ -654,6 +659,43 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
});
}
const touchTimerRef = useRef<NodeJS.Timeout | null>(null);
const getTouchHandlers = (file: FileItem) => ({
onTouchStart: (e: React.TouchEvent) => {
if (!isMobile) return;
const touch = e.touches[0];
const startX = touch.clientX;
const startY = touch.clientY;
touchTimerRef.current = setTimeout(() => {
handleContextMenu({ preventDefault: () => {}, clientX: startX, clientY: startY }, file);
// Provide haptic feedback if available
if (typeof window !== 'undefined' && window.navigator && window.navigator.vibrate) {
window.navigator.vibrate(50);
}
}, 500);
},
onTouchMove: () => {
if (touchTimerRef.current) {
clearTimeout(touchTimerRef.current);
touchTimerRef.current = null;
}
},
onTouchEnd: () => {
if (touchTimerRef.current) {
clearTimeout(touchTimerRef.current);
touchTimerRef.current = null;
}
},
onTouchCancel: () => {
if (touchTimerRef.current) {
clearTimeout(touchTimerRef.current);
touchTimerRef.current = null;
}
}
});
async function handleSearch(query: string) {
setSearchQuery(query);
if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
@ -1236,9 +1278,21 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
}, [displayFiles, activeSection, trashAutoPurgeDays]);
return (
<div className="flex h-screen" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
<div className="flex h-screen w-full overflow-hidden" style={{ backgroundColor: 'var(--color-bg-primary)' }}>
{isMobile && sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-40"
onClick={() => setSidebarOpen(false)}
/>
)}
<aside
className="w-60 flex-shrink-0 flex flex-col border-r"
className={`w-60 flex-shrink-0 flex flex-col border-r ${
isMobile
? `fixed inset-y-0 left-0 z-50 transition-transform duration-300 ease-in-out ${
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`
: ''
}`}
style={{
backgroundColor: 'var(--color-sidebar-bg)',
borderColor: 'var(--color-border-subtle)',
@ -1397,7 +1451,17 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
borderColor: 'var(--color-border-subtle)',
}}
>
{(!isMobile || !mobileSearchOpen) && (
<div className="flex items-center gap-1 flex-1 min-w-0">
{isMobile && (
<button
onClick={() => setSidebarOpen(true)}
className="p-1.5 mr-1 rounded-lg transition-colors hover:bg-black/5 dark:hover:bg-white/5"
style={{ color: 'var(--color-text-secondary)' }}
>
<Menu className="w-5 h-5" />
</button>
)}
<button
onClick={() => { navigateTo('.'); setActiveSection('files'); }}
className="text-sm font-medium hover:opacity-80 transition-opacity"
@ -1410,7 +1474,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
<ChevronRight className="w-3.5 h-3.5" style={{ color: 'var(--color-text-tertiary)' }} />
<button
onClick={() => navigateTo(pathSegments.slice(0, i + 1).join('/'))}
className="text-sm font-medium hover:opacity-80 transition-opacity truncate max-w-[150px]"
className={`text-sm font-medium hover:opacity-80 transition-opacity truncate ${isMobile ? 'max-w-[60px]' : 'max-w-[150px]'}`}
style={{
color: i === pathSegments.length - 1 ? 'var(--color-text-primary)' : 'var(--color-text-secondary)',
}}
@ -1420,9 +1484,12 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
</div>
))}
</div>
)}
{activeSection !== 'settings' && (
<div className="relative w-72">
<div className={`relative transition-all ${isMobile && mobileSearchOpen ? 'flex-1' : isMobile ? 'w-auto' : 'w-72'}`}>
{!isMobile || mobileSearchOpen ? (
<>
<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"
@ -1430,33 +1497,41 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
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"
className="w-full pl-9 pr-8 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)',
}}
autoFocus={isMobile && mobileSearchOpen}
/>
{searchQuery && (
{(searchQuery || isMobile) && (
<button
onClick={() => { setSearchQuery(''); setSearchResults(null); }}
className="absolute right-3 top-1/2 -translate-y-1/2"
onClick={() => { setSearchQuery(''); setSearchResults(null); if (isMobile) setMobileSearchOpen(false); }}
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 hover:bg-black/10 dark:hover:bg-white/10 rounded"
>
<X className="w-3.5 h-3.5" style={{ color: 'var(--color-text-tertiary)' }} />
</button>
)}
</>
) : (
<button onClick={() => setMobileSearchOpen(true)} className="p-2 rounded-lg transition-colors hover:bg-black/5 dark:hover:bg-white/5">
<Search className="w-4.5 h-4.5" style={{ color: 'var(--color-text-secondary)' }} />
</button>
)}
</div>
)}
<div className="flex items-center gap-2">
{(!isMobile || !mobileSearchOpen) && (
<div className="flex items-center gap-1 sm:gap-2">
{activeSection === 'trash' && (
<button
onClick={() => setShowTrashModal(true)}
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all hover:opacity-90"
className={`flex items-center gap-2 ${isMobile ? 'p-2' : 'px-4 py-2'} rounded-lg text-sm font-medium transition-all hover:opacity-90`}
style={{ backgroundColor: 'rgba(239, 68, 68, 0.1)', color: 'var(--color-danger)' }}
>
<Trash2 className="w-4 h-4" />
Empty Trash
{!isMobile && "Empty Trash"}
</button>
)}
@ -1474,9 +1549,15 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
<div className="w-px h-5 mx-1" style={{ backgroundColor: 'var(--color-border-subtle)' }} />
<div className="relative group">
<button className="flex items-center gap-1.5 p-2 rounded-lg transition-colors text-sm font-medium" style={{ color: 'var(--color-text-secondary)' }}>
<button className="flex items-center gap-1.5 p-2 rounded-lg transition-colors text-sm font-medium" style={{ color: 'var(--color-text-secondary)' }} title="Sort">
{isMobile ? (
<ArrowUp className={`w-4.5 h-4.5 ${sortAsc ? '' : 'rotate-180 transition-transform'}`} />
) : (
<>
Sort by: <span className="capitalize">{sortBy}</span>
{sortAsc ? <ArrowUp className="w-3 h-3" /> : <ArrowUp className="w-3 h-3 rotate-180 transition-transform" />}
</>
)}
</button>
<div className="absolute right-0 top-full pt-1 w-40 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all z-50">
<div className="w-full rounded-xl shadow-lg border overflow-hidden" style={{ backgroundColor: 'var(--color-bg-elevated)', borderColor: 'var(--color-border)' }}>
@ -1516,13 +1597,14 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
<button
id="upload-button"
onClick={() => fileInputRef.current?.click()}
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium text-white transition-all hover:opacity-90"
className={`flex items-center gap-2 ${isMobile ? 'p-2' : 'px-4 py-2'} rounded-lg text-sm font-medium text-white transition-all hover:opacity-90`}
style={{
background: 'linear-gradient(135deg, var(--color-accent), #8b5cf6)',
}}
title="Upload"
>
<Upload className="w-4 h-4" />
Upload
{!isMobile && "Upload"}
</button>
<input
ref={fileInputRef}
@ -1536,6 +1618,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
</>
)}
</div>
)}
</header>
{activeSection === 'settings' ? (
@ -1672,6 +1755,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
transition={{ delay: idx * 0.02 }}
onClick={(e) => handleFileClick(file, e)}
onContextMenu={(e) => handleContextMenu(e, file)}
{...getTouchHandlers(file)}
className={`group relative p-4 rounded-xl cursor-pointer transition-all duration-150 ${draggedFile === file.path ? 'opacity-50' : ''}`}
style={{
backgroundColor: selectedFiles.has(file.path) || dragOverFolder === file.path ? 'var(--color-accent-subtle)' : 'var(--color-bg-elevated)',
@ -1766,6 +1850,7 @@ export default function FileExplorer({ onLogout }: FileExplorerProps) {
transition={{ delay: idx * 0.015 }}
onClick={(e) => handleFileClick(file, e)}
onContextMenu={(e) => handleContextMenu(e, file)}
{...getTouchHandlers(file)}
className={`grid grid-cols-[1fr_100px_140px_40px] gap-4 items-center px-4 py-2.5 rounded-lg cursor-pointer transition-all duration-100 group ${draggedFile === file.path ? 'opacity-50' : ''}`}
style={{ backgroundColor: selectedFiles.has(file.path) || dragOverFolder === file.path ? 'var(--color-accent-subtle)' : 'transparent' }}
>

View file

@ -0,0 +1,24 @@
import { useState, useEffect } from 'react';
const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
const [isMobile, setIsMobile] = useState<boolean>(false);
useEffect(() => {
// Initial check
const checkIsMobile = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
checkIsMobile();
// Add resize listener
window.addEventListener('resize', checkIsMobile);
// Cleanup
return () => window.removeEventListener('resize', checkIsMobile);
}, []);
return isMobile;
}