mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-08-16 13:47:28 +02:00
Formatting
This commit is contained in:
parent
32dba498ce
commit
5acb700f71
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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`)
|
||||||
|
@ -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}
|
||||||
|
@ -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)" />}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -9,5 +9,6 @@ export interface FileListItemProps {
|
|||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
onDoubleClick?: () => void;
|
onDoubleClick?: () => void;
|
||||||
|
isLast?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user