Formatting

This commit is contained in:
Connor Yoh 2025-08-05 17:52:18 +01:00
parent 32dba498ce
commit 5acb700f71
14 changed files with 269 additions and 124 deletions

View File

@ -1738,17 +1738,18 @@
"recent": "Recent", "recent": "Recent",
"localFiles": "Local Files", "localFiles": "Local Files",
"googleDrive": "Google Drive", "googleDrive": "Google Drive",
"googleDriveShort": "Drive",
"myFiles": "My Files", "myFiles": "My Files",
"noRecentFiles": "No recent files found", "noRecentFiles": "No recent files found",
"dropFilesHint": "Drop files here to upload", "dropFilesHint": "Drop files here to upload",
"googleDriveNotAvailable": "Google Drive integration not available", "googleDriveNotAvailable": "Google Drive integration not available",
"openFiles": "Open Files", "openFiles": "Open Files",
"openFile": "Open File", "openFile": "Open File",
"details": "Details", "details": "File Details",
"fileName": "File Name", "fileName": "Name",
"fileFormat": "File Format", "fileFormat": "Format",
"fileSize": "File Size", "fileSize": "Size",
"fileVersion": "File Version", "fileVersion": "Version",
"totalSelected": "Total Selected", "totalSelected": "Total Selected",
"dropFilesHere": "Drop files here" "dropFilesHere": "Drop files here"
}, },

View File

@ -22,7 +22,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [isMobile, setIsMobile] = useState(false); const [isMobile, setIsMobile] = useState(false);
const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile } = useFileManager(); const { loadRecentFiles, handleRemoveFile, storeFile, convertToFile, touchFile } = useFileManager();
// File management handlers // File management handlers
const isFileSupported = useCallback((fileName: string) => { const isFileSupported = useCallback((fileName: string) => {
@ -70,7 +70,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
}, [handleRemoveFile, recentFiles]); }, [handleRemoveFile, recentFiles]);
useEffect(() => { useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768); const checkMobile = () => setIsMobile(window.innerWidth < 1030);
checkMobile(); checkMobile();
window.addEventListener('resize', checkMobile); window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile); return () => window.removeEventListener('resize', checkMobile);
@ -99,10 +99,10 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
// Modal size constants for consistent scaling // Modal size constants for consistent scaling
const modalHeight = '80vh'; const modalHeight = '80vh';
const modalWidth = isMobile ? '100%' : '60vw'; const modalWidth = isMobile ? '100%' : '80vw';
const modalMaxWidth = isMobile ? '100%' : '1200px'; const modalMaxWidth = isMobile ? '100%' : '1200px';
const modalMaxHeight = '1200px'; const modalMaxHeight = '1200px';
const modalMinWidth = isMobile ? '320px' : '1030px'; const modalMinWidth = isMobile ? '320px' : '800px';
return ( return (
<Modal <Modal
@ -140,12 +140,11 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
multiple={true} multiple={true}
activateOnClick={false} activateOnClick={false}
style={{ style={{
padding: '1rem',
height: '100%', height: '100%',
width: '100%', width: '100%',
border: 'none', border: 'none',
borderRadius: '30px', borderRadius: '30px',
backgroundColor: 'transparent' backgroundColor: 'var(--bg-file-manager)'
}} }}
styles={{ styles={{
inner: { pointerEvents: 'all' } inner: { pointerEvents: 'all' }
@ -159,6 +158,8 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
isOpen={isFilesModalOpen} isOpen={isFilesModalOpen}
onFileRemove={handleRemoveFileByIndex} onFileRemove={handleRemoveFileByIndex}
modalHeight={modalHeight} modalHeight={modalHeight}
storeFile={storeFile}
refreshRecentFiles={refreshRecentFiles}
> >
{isMobile ? <MobileLayout /> : <DesktopLayout />} {isMobile ? <MobileLayout /> : <DesktopLayout />}
</FileManagerProvider> </FileManagerProvider>

View File

@ -15,11 +15,11 @@ const DesktopLayout: React.FC = () => {
} = useFileManagerContext(); } = useFileManagerContext();
return ( return (
<Grid gutter="md" h="100%" grow={false} style={{ flexWrap: 'nowrap' }}> <Grid gutter="xs" h="100%" grow={false} style={{ flexWrap: 'nowrap', minWidth: 0 }}>
{/* Column 1: File Sources */} {/* Column 1: File Sources */}
<Grid.Col span="content" style={{ <Grid.Col span="content" p="lg" style={{
minWidth: '15.625rem', minWidth: '13.625rem',
width: '15.625rem', width: '13.625rem',
flexShrink: 0, flexShrink: 0,
height: '100%', height: '100%',
}}> }}>
@ -27,24 +27,55 @@ const DesktopLayout: React.FC = () => {
</Grid.Col> </Grid.Col>
{/* Column 2: File List */} {/* Column 2: File List */}
<Grid.Col span="auto" style={{ display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}> <Grid.Col span="auto" style={{
{activeSource === 'recent' && ( display: 'flex',
<SearchInput style={{ marginBottom: '1rem', flexShrink: 0 }} /> flexDirection: 'column',
)} height: '100%',
minHeight: 0,
<div style={{ flex: 1, minHeight: 0 }}> minWidth: 0,
<FileListArea flex: '1 1 0px'
scrollAreaHeight={`calc(${modalHeight} - 6rem)`} }}>
scrollAreaStyle={{ <div style={{
height: activeSource === 'recent' && recentFiles.length > 0 ? `calc(${modalHeight} - 6rem)` : '100%' flex: 1,
}} display: 'flex',
/> flexDirection: 'column',
backgroundColor: 'var(--bg-file-list)',
border: '1px solid var(--mantine-color-gray-2)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
overflow: 'hidden'
}}>
{activeSource === 'recent' && (
<div style={{
flexShrink: 0,
borderBottom: '1px solid var(--mantine-color-gray-3)'
}}>
<SearchInput />
</div>
)}
<div style={{ flex: 1, minHeight: 0 }}>
<FileListArea
scrollAreaHeight={`calc(${modalHeight} )`}
scrollAreaStyle={{
height: activeSource === 'recent' && recentFiles.length > 0 ? modalHeight : '100%',
backgroundColor: 'transparent',
border: 'none',
borderRadius: 0
}}
/>
</div>
</div> </div>
</Grid.Col> </Grid.Col>
{/* Column 3: File Details */} {/* Column 3: File Details */}
<Grid.Col span="content" style={{ minWidth: '20rem', width: '20rem', flexShrink: 0, height: '100%' }}> <Grid.Col p="xl" span="content" style={{
<div style={{ height: '100%' }}> minWidth: '25rem',
width: '25rem',
flexShrink: 0,
height: '100%',
maxWidth: '18rem'
}}>
<div style={{ height: '100%', overflow: 'hidden' }}>
<FileDetails /> <FileDetails />
</div> </div>
</Grid.Col> </Grid.Col>

View File

@ -62,9 +62,9 @@ const FileDetails: React.FC<FileDetailsProps> = ({
return ( return (
<Stack gap="xs" style={{ height: '100%' }}> <Stack gap="xs" style={{ height: '100%' }}>
{/* Compact mobile layout */} {/* Compact mobile layout */}
<Box style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}> <Box style={{ display: 'flex', gap: '0.75rem', alignItems: 'center' }}>
{/* Small preview */} {/* Small preview */}
<Box style={{ width: '60px', height: '80px', flexShrink: 0, position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <Box style={{ width: '120px', height: '150px', flexShrink: 0, position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{currentFile && getCurrentThumbnail() ? ( {currentFile && getCurrentThumbnail() ? (
<img <img
src={getCurrentThumbnail()} src={getCurrentThumbnail()}
@ -134,6 +134,10 @@ const FileDetails: React.FC<FileDetailsProps> = ({
onClick={onOpenFiles} onClick={onOpenFiles}
disabled={!hasSelection} disabled={!hasSelection}
fullWidth fullWidth
style={{
backgroundColor: hasSelection ? 'var(--btn-open-file)' : 'var(--mantine-color-gray-4)',
color: 'white'
}}
> >
{selectedFiles.length > 1 {selectedFiles.length > 1
? t('fileManager.openFiles', `Open ${selectedFiles.length} Files`) ? t('fileManager.openFiles', `Open ${selectedFiles.length} Files`)
@ -145,7 +149,7 @@ const FileDetails: React.FC<FileDetailsProps> = ({
} }
return ( return (
<Stack gap="sm" h={`calc(${modalHeight} - 2rem)`}> <Stack gap="lg" h={`calc(${modalHeight} - 2rem)`}>
{/* Section 1: Thumbnail Preview */} {/* Section 1: Thumbnail Preview */}
<Box p="xs" style={{ textAlign: 'center', flexShrink: 0 }}> <Box p="xs" style={{ textAlign: 'center', flexShrink: 0 }}>
<Box style={{ position: 'relative', width: "100%", height: `calc(${modalHeight} * 0.5 - 2rem)`, margin: '0 auto', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <Box style={{ position: 'relative', width: "100%", height: `calc(${modalHeight} * 0.5 - 2rem)`, margin: '0 auto', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
@ -182,7 +186,6 @@ const FileDetails: React.FC<FileDetailsProps> = ({
width: '100%', width: '100%',
height: '100%', height: '100%',
backgroundColor: 'var(--mantine-color-gray-2)', backgroundColor: 'var(--mantine-color-gray-2)',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
transform: 'translate(12px, 12px) rotate(2deg)', transform: 'translate(12px, 12px) rotate(2deg)',
zIndex: 1 zIndex: 1
@ -197,7 +200,6 @@ const FileDetails: React.FC<FileDetailsProps> = ({
width: '100%', width: '100%',
height: '100%', height: '100%',
backgroundColor: 'var(--mantine-color-gray-1)', backgroundColor: 'var(--mantine-color-gray-1)',
borderRadius: '8px',
boxShadow: '0 3px 10px rgba(0, 0, 0, 0.12)', boxShadow: '0 3px 10px rgba(0, 0, 0, 0.12)',
transform: 'translate(6px, 6px) rotate(1deg)', transform: 'translate(6px, 6px) rotate(1deg)',
zIndex: 2 zIndex: 2
@ -212,14 +214,13 @@ const FileDetails: React.FC<FileDetailsProps> = ({
src={getCurrentThumbnail()} src={getCurrentThumbnail()}
alt={currentFile.name} alt={currentFile.name}
fit="contain" fit="contain"
radius="md"
style={{ style={{
maxWidth: '100%', maxWidth: '100%',
maxHeight: '100%', maxHeight: '100%',
width: 'auto', width: 'auto',
height: 'auto', height: 'auto',
boxShadow: '0 6px 16px rgba(0, 0, 0, 0.2)', boxShadow: '0 6px 16px rgba(0, 0, 0, 0.2)',
borderRadius: '8px',
position: 'relative', position: 'relative',
zIndex: 3, zIndex: 3,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
@ -232,7 +233,6 @@ const FileDetails: React.FC<FileDetailsProps> = ({
width: '80%', width: '80%',
height: '80%', height: '80%',
backgroundColor: 'var(--mantine-color-gray-1)', backgroundColor: 'var(--mantine-color-gray-1)',
borderRadius: 8,
boxShadow: '0 6px 16px rgba(0, 0, 0, 0.2)', boxShadow: '0 6px 16px rgba(0, 0, 0, 0.2)',
position: 'relative', position: 'relative',
zIndex: 3, zIndex: 3,
@ -275,7 +275,7 @@ const FileDetails: React.FC<FileDetailsProps> = ({
</Text> </Text>
</Box> </Box>
<ScrollArea style={{ flex: 1 }} p="md"> <ScrollArea style={{ flex: 1 }} p="md">
<Stack gap={0}> <Stack gap="sm">
<Group justify="space-between" py="xs"> <Group justify="space-between" py="xs">
<Text size="sm" c="dimmed">{t('fileManager.fileName', 'Name')}</Text> <Text size="sm" c="dimmed">{t('fileManager.fileName', 'Name')}</Text>
<Text size="sm" fw={500} style={{ maxWidth: '60%', textAlign: 'right' }} truncate> <Text size="sm" fw={500} style={{ maxWidth: '60%', textAlign: 'right' }} truncate>
@ -328,10 +328,15 @@ const FileDetails: React.FC<FileDetailsProps> = ({
<Button <Button
size="md" size="md"
mb="xl"
onClick={onOpenFiles} onClick={onOpenFiles}
disabled={!hasSelection} disabled={!hasSelection}
fullWidth fullWidth
style={{ flexShrink: 0 }} style={{
flexShrink: 0,
backgroundColor: hasSelection ? 'var(--btn-open-file)' : 'var(--mantine-color-gray-4)',
color: 'white'
}}
> >
{selectedFiles.length > 1 {selectedFiles.length > 1
? t('fileManager.openFiles', `Open ${selectedFiles.length} Files`) ? t('fileManager.openFiles', `Open ${selectedFiles.length} Files`)

View File

@ -45,11 +45,13 @@ const FileListArea: React.FC<FileListAreaProps> = ({
return ( return (
<ScrollArea <ScrollArea
h={scrollAreaHeight} h={scrollAreaHeight}
style={{ ...scrollAreaStyle }} style={{
...scrollAreaStyle
}}
type="always" type="always"
scrollbarSize={8} scrollbarSize={8}
> >
<Stack gap="xs" p="xs"> <Stack gap={0}>
{filteredFiles.map((file, index) => ( {filteredFiles.map((file, index) => (
<FileListItem <FileListItem
key={file.id || file.name} key={file.id || file.name}

View File

@ -1,6 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Card, Group, Box, Center, Text, ActionIcon } from '@mantine/core'; import { Group, Box, Text, ActionIcon, Checkbox, Divider } from '@mantine/core';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import { getFileSize, getFileDate } from '../../../utils/fileUtils'; import { getFileSize, getFileDate } from '../../../utils/fileUtils';
import { FileListItemProps } from './types'; import { FileListItemProps } from './types';
@ -11,54 +10,64 @@ const FileListItem: React.FC<FileListItemProps> = ({
isSupported, isSupported,
onSelect, onSelect,
onRemove, onRemove,
onDoubleClick onDoubleClick
}) => { }) => {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
return ( return (
<Card <>
p="xs" <Box
withBorder p="sm"
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
backgroundColor: isSelected ? 'var(--mantine-color-blue-0)' : (isHovered ? 'var(--mantine-color-gray-0)' : undefined), backgroundColor: isSelected ? 'var(--mantine-color-gray-0)' : (isHovered ? 'var(--mantine-color-gray-0)' : 'var(--bg-file-list)'),
border: isSelected ? '1px solid var(--mantine-color-blue-3)' : undefined, opacity: isSupported ? 1 : 0.5,
opacity: isSupported ? 1 : 0.5, transition: 'background-color 0.15s ease'
boxShadow: isHovered && !isSelected ? '0 2px 8px rgba(0, 0, 0, 0.1)' : undefined, }}
transition: 'background-color 0.15s ease, box-shadow 0.15s ease' onClick={onSelect}
}} onDoubleClick={onDoubleClick}
onClick={onSelect} onMouseEnter={() => setIsHovered(true)}
onDoubleClick={onDoubleClick} onMouseLeave={() => setIsHovered(false)}
onMouseEnter={() => setIsHovered(true)} >
onMouseLeave={() => setIsHovered(false)} <Group gap="sm">
> <Box>
<Group gap="sm"> <Checkbox
<Box style={{ width: 40, height: 40, flexShrink: 0 }}> checked={isSelected}
<Center style={{ width: '100%', height: '100%', backgroundColor: 'var(--mantine-color-gray-1)', borderRadius: 4 }}> onChange={() => {}} // Handled by parent onClick
<PictureAsPdfIcon style={{ fontSize: 20, color: 'var(--mantine-color-gray-6)' }} /> size="sm"
</Center> pl="sm"
</Box> pr="xs"
<Box style={{ flex: 1, minWidth: 0 }}> styles={{
<Text size="sm" fw={500} truncate>{file.name}</Text> input: {
<Text size="xs" c="dimmed">{getFileSize(file)} {getFileDate(file)}</Text> cursor: 'pointer'
</Box> }
{/* Delete button - fades in/out on hover */} }}
<ActionIcon />
variant="subtle" </Box>
c="dimmed"
size="md" <Box style={{ flex: 1, minWidth: 0 }}>
onClick={(e) => { e.stopPropagation(); onRemove(); }} <Text size="sm" fw={500} truncate>{file.name}</Text>
style={{ <Text size="xs" c="dimmed">{getFileSize(file)} {getFileDate(file)}</Text>
opacity: isHovered ? 1 : 0, </Box>
transform: isHovered ? 'scale(1)' : 'scale(0.8)', {/* Delete button - fades in/out on hover */}
transition: 'opacity 0.3s ease, transform 0.3s ease', <ActionIcon
pointerEvents: isHovered ? 'auto' : 'none' variant="subtle"
}} c="dimmed"
> size="md"
<DeleteIcon style={{ fontSize: 20 }} /> onClick={(e) => { e.stopPropagation(); onRemove(); }}
</ActionIcon> style={{
</Group> opacity: isHovered ? 1 : 0,
</Card> transform: isHovered ? 'scale(1)' : 'scale(0.8)',
transition: 'opacity 0.3s ease, transform 0.3s ease',
pointerEvents: isHovered ? 'auto' : 'none'
}}
>
<DeleteIcon style={{ fontSize: 20 }} />
</ActionIcon>
</Group>
</Box>
{ <Divider color="var(--mantine-color-gray-3)" />}
</>
); );
}; };

View File

@ -41,6 +41,8 @@ interface FileManagerProviderProps {
isOpen: boolean; isOpen: boolean;
onFileRemove: (index: number) => void; onFileRemove: (index: number) => void;
modalHeight: string; modalHeight: string;
storeFile: (file: File) => Promise<void>;
refreshRecentFiles: () => Promise<void>;
} }
export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
@ -52,6 +54,8 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
isOpen, isOpen,
onFileRemove, onFileRemove,
modalHeight, modalHeight,
storeFile,
refreshRecentFiles,
}) => { }) => {
const [activeSource, setActiveSource] = useState<FileSource>('recent'); const [activeSource, setActiveSource] = useState<FileSource>('recent');
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]); const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
@ -115,26 +119,35 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
setSearchTerm(value); setSearchTerm(value);
}, []); }, []);
const handleFileInputChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { const handleFileInputChange = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files || []); const files = Array.from(event.target.files || []);
if (files.length > 0) { if (files.length > 0) {
const fileWithUrls = files.map(file => { try {
const url = URL.createObjectURL(file); // Store files and refresh recent files (same as drag-and-drop)
createdBlobUrls.current.add(url); await Promise.all(files.map(file => storeFile(file)));
return {
id: `local-${Date.now()}-${Math.random()}`, const fileWithUrls = files.map(file => {
name: file.name, const url = URL.createObjectURL(file);
file, createdBlobUrls.current.add(url);
url, return {
size: file.size, id: `local-${Date.now()}-${Math.random()}`,
lastModified: file.lastModified, name: file.name,
}; file,
}); url,
onFilesSelected(fileWithUrls); size: file.size,
onClose(); lastModified: file.lastModified,
};
});
onFilesSelected(fileWithUrls);
await refreshRecentFiles();
onClose();
} catch (error) {
console.error('Failed to process selected files:', error);
}
} }
event.target.value = ''; event.target.value = '';
}, [onFilesSelected, onClose]); }, [storeFile, onFilesSelected, refreshRecentFiles, onClose]);
// Cleanup blob URLs when component unmounts // Cleanup blob URLs when component unmounts
useEffect(() => { useEffect(() => {

View File

@ -18,10 +18,11 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
const buttonProps = { const buttonProps = {
variant: (source: string) => activeSource === source ? 'filled' : 'subtle', variant: (source: string) => activeSource === source ? 'filled' : 'subtle',
getColor: (source: string) => activeSource === source ? 'var(--mantine-color-gray-4)' : undefined, getColor: (source: string) => activeSource === source ? 'var(--mantine-color-gray-2)' : undefined,
getStyles: (source: string) => ({ getStyles: (source: string) => ({
root: { root: {
backgroundColor: activeSource === source ? undefined : 'transparent', backgroundColor: activeSource === source ? undefined : 'transparent',
color: activeSource === source ? 'var(--mantine-color-gray-9)' : 'var(--mantine-color-gray-6)',
border: 'none', border: 'none',
'&:hover': { '&:hover': {
backgroundColor: activeSource === source ? undefined : 'var(--mantine-color-gray-0)' backgroundColor: activeSource === source ? undefined : 'var(--mantine-color-gray-0)'
@ -33,7 +34,6 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
const buttons = ( const buttons = (
<> <>
<Button <Button
variant={buttonProps.variant('recent')}
leftSection={<HistoryIcon />} leftSection={<HistoryIcon />}
justify={horizontal ? "center" : "flex-start"} justify={horizontal ? "center" : "flex-start"}
onClick={() => onSourceChange('recent')} onClick={() => onSourceChange('recent')}
@ -47,7 +47,7 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
<Button <Button
variant="subtle" variant="subtle"
color='var(--mantine-color-gray-5)' color='var(--mantine-color-gray-6)'
leftSection={<FolderIcon />} leftSection={<FolderIcon />}
justify={horizontal ? "center" : "flex-start"} justify={horizontal ? "center" : "flex-start"}
onClick={onLocalFileClick} onClick={onLocalFileClick}
@ -77,14 +77,14 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
color={activeSource === 'drive' ? 'gray' : undefined} color={activeSource === 'drive' ? 'gray' : undefined}
styles={buttonProps.getStyles('drive')} styles={buttonProps.getStyles('drive')}
> >
{horizontal ? t('fileManager.googleDrive', 'Drive') : t('fileManager.googleDrive', 'Google Drive')} {horizontal ? t('fileManager.googleDriveShort', 'Drive') : t('fileManager.googleDrive', 'Google Drive')}
</Button> </Button>
</> </>
); );
if (horizontal) { if (horizontal) {
return ( return (
<Group gap="md" justify="center" style={{ width: '100%' }}> <Group gap="xs" justify="center" style={{ width: '100%' }}>
{buttons} {buttons}
</Group> </Group>
); );
@ -92,7 +92,7 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
return ( return (
<Stack gap="xs" style={{ height: '100%' }}> <Stack gap="xs" style={{ height: '100%' }}>
<Text size="sm" fw={500} c="dimmed" mb="xs"> <Text size="sm" pt="sm" fw={500} c="dimmed" mb="xs" style={{ paddingLeft: '1rem' }}>
{t('fileManager.myFiles', 'My Files')} {t('fileManager.myFiles', 'My Files')}
</Text> </Text>
{buttons} {buttons}

View File

@ -14,8 +14,22 @@ const MobileLayout: React.FC = () => {
modalHeight, modalHeight,
} = useFileManagerContext(); } = useFileManagerContext();
// Calculate the height more accurately based on actual content
const calculateFileListHeight = () => {
// Base modal height minus padding and gaps
const baseHeight = `calc(${modalHeight} - 2rem)`; // Account for Stack padding
// Estimate heights of fixed components
const fileSourceHeight = '3rem'; // FileSourceButtons height
const fileDetailsHeight = selectedFiles.length > 0 ? '10rem' : '8rem'; // FileDetails compact height
const searchHeight = activeSource === 'recent' ? '3rem' : '0rem'; // SearchInput height
const gapHeight = activeSource === 'recent' ? '3rem' : '2rem'; // Stack gaps
return `calc(${baseHeight} - ${fileSourceHeight} - ${fileDetailsHeight} - ${searchHeight} - ${gapHeight})`;
};
return ( return (
<Stack h="100%" gap="sm" p="sm"> <Box h="100%" p="sm" style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{/* Section 1: File Sources - Fixed at top */} {/* Section 1: File Sources - Fixed at top */}
<Box style={{ flexShrink: 0 }}> <Box style={{ flexShrink: 0 }}>
<FileSourceButtons horizontal={true} /> <FileSourceButtons horizontal={true} />
@ -25,24 +39,44 @@ const MobileLayout: React.FC = () => {
<FileDetails compact={true} /> <FileDetails compact={true} />
</Box> </Box>
{/* Section 3: Search Bar - Fixed above file list */} {/* Section 3 & 4: Search Bar + File List - Unified background extending to modal edge */}
{activeSource === 'recent' && ( <Box style={{
<Box style={{ flexShrink: 0 }}> flex: 1,
<SearchInput /> display: 'flex',
flexDirection: 'column',
backgroundColor: 'var(--bg-file-list)',
borderRadius: '8px',
border: '1px solid var(--mantine-color-gray-2)',
overflow: 'hidden',
minHeight: 0
}}>
{activeSource === 'recent' && (
<Box style={{
flexShrink: 0,
borderBottom: '1px solid var(--mantine-color-gray-2)'
}}>
<SearchInput />
</Box>
)}
<Box style={{ flex: 1, minHeight: 0 }}>
<FileListArea
scrollAreaHeight={calculateFileListHeight()}
scrollAreaStyle={{
height: calculateFileListHeight(),
maxHeight: '60vh',
minHeight: '150px',
backgroundColor: 'transparent',
border: 'none',
borderRadius: 0
}}
/>
</Box> </Box>
)}
{/* Section 4: File List - Fixed height scrollable area */}
<Box style={{ flexShrink: 0 }}>
<FileListArea
scrollAreaHeight={`calc(${modalHeight} - ${selectedFiles.length > 0 ? '300px' : '200px'})`}
scrollAreaStyle={{ maxHeight: '400px', minHeight: '150px' }}
/>
</Box> </Box>
{/* Hidden file input for local file selection */} {/* Hidden file input for local file selection */}
<HiddenFileInput /> <HiddenFileInput />
</Stack> </Box>
); );
}; };

View File

@ -18,7 +18,14 @@ const SearchInput: React.FC<SearchInputProps> = ({ style }) => {
leftSection={<SearchIcon />} leftSection={<SearchIcon />}
value={searchTerm} value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)} onChange={(e) => onSearchChange(e.target.value)}
style={style}
style={{ padding: '0.5rem', ...style }}
styles={{
input: {
border: 'none',
backgroundColor: 'transparent'
}
}}
/> />
); );
}; };

View File

@ -9,5 +9,6 @@ export interface FileListItemProps {
onSelect: () => void; onSelect: () => void;
onRemove: () => void; onRemove: () => void;
onDoubleClick?: () => void; onDoubleClick?: () => void;
isLast?: boolean;
} }

View File

@ -111,12 +111,21 @@ export const useFileManager = () => {
}; };
}, [convertToFile]); }, [convertToFile]);
const touchFile = useCallback(async (id: string) => {
try {
await fileStorage.touchFile(id);
} catch (error) {
console.error('Failed to touch file:', error);
}
}, []);
return { return {
loading, loading,
convertToFile, convertToFile,
loadRecentFiles, loadRecentFiles,
handleRemoveFile, handleRemoveFile,
storeFile, storeFile,
touchFile,
createFileSelectionHandlers createFileSelectionHandlers
}; };
}; };

View File

@ -225,6 +225,32 @@ class FileStorageService {
}); });
} }
/**
* Update the lastModified timestamp of a file (for most recently used sorting)
*/
async touchFile(id: string): Promise<boolean> {
if (!this.db) await this.init();
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const getRequest = store.get(id);
getRequest.onsuccess = () => {
const file = getRequest.result;
if (file) {
// Update lastModified to current timestamp
file.lastModified = Date.now();
const updateRequest = store.put(file);
updateRequest.onsuccess = () => resolve(true);
updateRequest.onerror = () => reject(updateRequest.error);
} else {
resolve(false); // File not found
}
};
getRequest.onerror = () => reject(getRequest.error);
});
}
/** /**
* Clear all stored files * Clear all stored files
*/ */

View File

@ -74,6 +74,9 @@
--bg-muted: #f3f4f6; --bg-muted: #f3f4f6;
--bg-background: #f9fafb; --bg-background: #f9fafb;
--bg-toolbar: #ffffff; --bg-toolbar: #ffffff;
--bg-file-manager: #F5F6F8;
--bg-file-list: #ffffff;
--btn-open-file: #0A8BFF;
--text-primary: #111827; --text-primary: #111827;
--text-secondary: #4b5563; --text-secondary: #4b5563;
--text-muted: #6b7280; --text-muted: #6b7280;
@ -144,6 +147,9 @@
--bg-muted: #1F2329; --bg-muted: #1F2329;
--bg-background: #2A2F36; --bg-background: #2A2F36;
--bg-toolbar: #272A2E; --bg-toolbar: #272A2E;
--bg-file-manager: #1F2329;
--bg-file-list: #2A2F36;
--btn-open-file: #0A8BFF;
--text-primary: #f9fafb; --text-primary: #f9fafb;
--text-secondary: #d1d5db; --text-secondary: #d1d5db;
--text-muted: #9ca3af; --text-muted: #9ca3af;