(null);
@@ -92,16 +102,15 @@ function HomePageContent() {
>
{/* Quick Access Bar */}
{/* Left: Tool Picker or Selected Tool Panel */}
{/* Global Modals */}
-
+
);
}
@@ -279,7 +288,9 @@ function HomePageContent() {
export default function HomePage() {
return (
-
+
+
+
);
}
diff --git a/frontend/src/services/fileStorage.ts b/frontend/src/services/fileStorage.ts
index 9ba2e7def..5fd5739e8 100644
--- a/frontend/src/services/fileStorage.ts
+++ b/frontend/src/services/fileStorage.ts
@@ -225,6 +225,32 @@ class FileStorageService {
});
}
+ /**
+ * Update the lastModified timestamp of a file (for most recently used sorting)
+ */
+ async touchFile(id: string): Promise {
+ 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
*/
diff --git a/frontend/src/styles/theme.css b/frontend/src/styles/theme.css
index 7cdb46c55..9ec48bca7 100644
--- a/frontend/src/styles/theme.css
+++ b/frontend/src/styles/theme.css
@@ -74,6 +74,9 @@
--bg-muted: #f3f4f6;
--bg-background: #f9fafb;
--bg-toolbar: #ffffff;
+ --bg-file-manager: #F5F6F8;
+ --bg-file-list: #ffffff;
+ --btn-open-file: #0A8BFF;
--text-primary: #111827;
--text-secondary: #4b5563;
--text-muted: #6b7280;
@@ -103,9 +106,40 @@
--icon-config-bg: #9CA3AF;
--icon-config-color: #FFFFFF;
+ /* Colors for tooltips */
+ --tooltip-title-bg: #DBEFFF;
+ --tooltip-title-color: #31528E;
+ --tooltip-header-bg: #31528E;
+ --tooltip-header-color: white;
+ --tooltip-border: var(--border-default);
+
/* Inactive icon colors for light mode */
--icon-inactive-bg: #9CA3AF;
--icon-inactive-color: #FFFFFF;
+
+ --accent-interactive: #4A90E2;
+ --text-instruction: #4A90E2;
+ --text-brand: var(--color-gray-700);
+ --text-brand-accent: #DC2626;
+
+ /* container */
+ --landing-paper-bg: var(--bg-surface);
+ --landing-inner-paper-bg: #EEF8FF;
+ --landing-inner-paper-border: #CDEAFF;
+ --landing-button-bg: var(--bg-surface);
+ --landing-button-color: var(--icon-tools-bg);
+ --landing-button-border: #E0F2F7;
+ --landing-button-hover-bg: rgb(251, 251, 251);
+
+ /* drop state */
+ --landing-drop-paper-bg: #E3F2FD;
+ --landing-drop-inner-paper-bg: #BBDEFB;
+ --landing-drop-inner-paper-border: #90CAF9;
+
+ /* shadows */
+ --drop-shadow-color: rgba(0, 0, 0, 0.08);
+ --drop-shadow-color-strong: rgba(0, 0, 0, 0.04);
+ --drop-shadow-filter: drop-shadow(0 0.2rem 0.4rem rgba(0, 0, 0, 0.08)) drop-shadow(0 0.6rem 0.6rem rgba(0, 0, 0, 0.06)) drop-shadow(0 1.2rem 1rem rgba(0, 0, 0, 0.04));
}
[data-mantine-color-scheme="dark"] {
@@ -144,6 +178,9 @@
--bg-muted: #1F2329;
--bg-background: #2A2F36;
--bg-toolbar: #272A2E;
+ --bg-file-manager: #1F2329;
+ --bg-file-list: #2A2F36;
+ --btn-open-file: #0A8BFF;
--text-primary: #f9fafb;
--text-secondary: #d1d5db;
--text-muted: #9ca3af;
@@ -177,6 +214,37 @@
--icon-inactive-bg: #2A2F36;
--icon-inactive-color: #6E7581;
+ /* Dark mode tooltip colors */
+ --tooltip-title-bg: #4B525A;
+ --tooltip-title-color: #fff;
+ --tooltip-header-bg: var(--bg-raised);
+ --tooltip-header-color: var(--text-primary);
+ --tooltip-border: var(--border-default);
+
+ --accent-interactive: #ffffff;
+ --text-instruction: #ffffff;
+ --text-brand: var(--color-gray-800);
+ --text-brand-accent: #EF4444;
+
+ /* container */
+ --landing-paper-bg: #171A1F;
+ --landing-inner-paper-bg: var(--bg-raised);
+ --landing-inner-paper-border: #2D3237;
+ --landing-button-bg: #2B3037;
+ --landing-button-color: #ffffff;
+ --landing-button-border: #2D3237;
+ --landing-button-hover-bg: #4c525b;
+
+ /* drop state */
+ --landing-drop-paper-bg: #1A2332;
+ --landing-drop-inner-paper-bg: #2A3441;
+ --landing-drop-inner-paper-border: #3A4451;
+
+ /* shadows */
+ --drop-shadow-color: rgba(255, 255, 255, 0.08);
+ --drop-shadow-color-strong: rgba(255, 255, 255, 0.04);
+ --drop-shadow-filter: drop-shadow(0 0.2rem 0.4rem rgba(200, 200, 200, 0.08)) drop-shadow(0 0.6rem 0.6rem rgba(200, 200, 200, 0.06)) drop-shadow(0 1.2rem 1rem rgba(200, 200, 200, 0.04));
+
/* Adjust shadows for dark mode */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.4);
@@ -185,6 +253,12 @@
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.4);
}
+/* Dropzone drop state styling */
+[data-accept] .dropzone-inner {
+ background-color: var(--landing-drop-inner-paper-bg) !important;
+ border-color: var(--landing-drop-inner-paper-border) !important;
+}
+
/* Smooth transitions for theme switching */
* {
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
diff --git a/frontend/src/tests/convert/ConvertIntegration.test.tsx b/frontend/src/tests/convert/ConvertIntegration.test.tsx
index c9a636035..5ac978810 100644
--- a/frontend/src/tests/convert/ConvertIntegration.test.tsx
+++ b/frontend/src/tests/convert/ConvertIntegration.test.tsx
@@ -23,13 +23,31 @@ import axios from 'axios';
vi.mock('axios');
const mockedAxios = vi.mocked(axios);
-// Mock utility modules
-vi.mock('../../utils/thumbnailUtils', () => ({
- generateThumbnailForFile: vi.fn().mockResolvedValue('data:image/png;base64,fake-thumbnail')
+// Mock only essential services that are actually called by the tests
+vi.mock('../../services/fileStorage', () => ({
+ fileStorage: {
+ init: vi.fn().mockResolvedValue(undefined),
+ storeFile: vi.fn().mockImplementation((file, thumbnail) => {
+ return Promise.resolve({
+ id: `mock-id-${file.name}`,
+ name: file.name,
+ size: file.size,
+ type: file.type,
+ lastModified: file.lastModified,
+ thumbnail: thumbnail
+ });
+ }),
+ getAllFileMetadata: vi.fn().mockResolvedValue([]),
+ cleanup: vi.fn().mockResolvedValue(undefined)
+ }
}));
-vi.mock('../../utils/api', () => ({
- makeApiUrl: vi.fn((path: string) => `/api/v1${path}`)
+vi.mock('../../services/thumbnailGenerationService', () => ({
+ thumbnailGenerationService: {
+ generateThumbnail: vi.fn().mockResolvedValue('data:image/png;base64,fake-thumbnail'),
+ cleanup: vi.fn(),
+ destroy: vi.fn()
+ }
}));
// Create realistic test files
@@ -194,7 +212,14 @@ describe('Convert Tool Integration Tests', () => {
test('should correctly map image conversion parameters to API call', async () => {
const mockBlob = new Blob(['fake-data'], { type: 'image/jpeg' });
- mockedAxios.post.mockResolvedValueOnce({ data: mockBlob });
+ mockedAxios.post.mockResolvedValueOnce({
+ data: mockBlob,
+ status: 200,
+ headers: {
+ 'content-type': 'image/jpeg',
+ 'content-disposition': 'attachment; filename="test_converted.jpg"'
+ }
+ });
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
@@ -472,7 +497,14 @@ describe('Convert Tool Integration Tests', () => {
test('should record operation in FileContext', async () => {
const mockBlob = new Blob(['fake-data'], { type: 'image/png' });
- mockedAxios.post.mockResolvedValueOnce({ data: mockBlob });
+ mockedAxios.post.mockResolvedValueOnce({
+ data: mockBlob,
+ status: 200,
+ headers: {
+ 'content-type': 'image/png',
+ 'content-disposition': 'attachment; filename="test_converted.png"'
+ }
+ });
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
@@ -506,7 +538,14 @@ describe('Convert Tool Integration Tests', () => {
test('should clean up blob URLs on reset', async () => {
const mockBlob = new Blob(['fake-data'], { type: 'image/png' });
- mockedAxios.post.mockResolvedValueOnce({ data: mockBlob });
+ mockedAxios.post.mockResolvedValueOnce({
+ data: mockBlob,
+ status: 200,
+ headers: {
+ 'content-type': 'image/png',
+ 'content-disposition': 'attachment; filename="test_converted.png"'
+ }
+ });
const { result } = renderHook(() => useConvertOperation(), {
wrapper: TestWrapper
diff --git a/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx b/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx
index 3fac5b4ba..64aafc488 100644
--- a/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx
+++ b/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx
@@ -18,9 +18,31 @@ import { detectFileExtension } from '../../utils/fileUtils';
vi.mock('axios');
const mockedAxios = vi.mocked(axios);
-// Mock utility modules
-vi.mock('../../utils/thumbnailUtils', () => ({
- generateThumbnailForFile: vi.fn().mockResolvedValue('data:image/png;base64,fake-thumbnail')
+// Mock only essential services that are actually called by the tests
+vi.mock('../../services/fileStorage', () => ({
+ fileStorage: {
+ init: vi.fn().mockResolvedValue(undefined),
+ storeFile: vi.fn().mockImplementation((file, thumbnail) => {
+ return Promise.resolve({
+ id: `mock-id-${file.name}`,
+ name: file.name,
+ size: file.size,
+ type: file.type,
+ lastModified: file.lastModified,
+ thumbnail: thumbnail
+ });
+ }),
+ getAllFileMetadata: vi.fn().mockResolvedValue([]),
+ cleanup: vi.fn().mockResolvedValue(undefined)
+ }
+}));
+
+vi.mock('../../services/thumbnailGenerationService', () => ({
+ thumbnailGenerationService: {
+ generateThumbnail: vi.fn().mockResolvedValue('data:image/png;base64,fake-thumbnail'),
+ cleanup: vi.fn(),
+ destroy: vi.fn()
+ }
}));
const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
diff --git a/frontend/src/tools/Compress.tsx b/frontend/src/tools/Compress.tsx
index cc0cd5cbc..f4b50b264 100644
--- a/frontend/src/tools/Compress.tsx
+++ b/frontend/src/tools/Compress.tsx
@@ -17,6 +17,7 @@ import CompressSettings from "../components/tools/compress/CompressSettings";
import { useCompressParameters } from "../hooks/tools/compress/useCompressParameters";
import { useCompressOperation } from "../hooks/tools/compress/useCompressOperation";
import { BaseToolProps } from "../types/tool";
+import { CompressTips } from "../components/tooltips/CompressTips";
const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const { t } = useTranslation();
@@ -25,6 +26,7 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const compressParams = useCompressParameters();
const compressOperation = useCompressOperation();
+ const compressTips = CompressTips();
// Endpoint validation
const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("compress-pdf");
@@ -104,6 +106,7 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
isCompleted={settingsCollapsed}
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
completedMessage={settingsCollapsed ? "Compression completed" : undefined}
+ tooltip={compressTips}
>
{
const { t } = useTranslation();
@@ -26,6 +27,7 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
const ocrParams = useOCRParameters();
const ocrOperation = useOCROperation();
+ const ocrTips = OcrTips();
// Step expansion state management
const [expandedStep, setExpandedStep] = useState<'files' | 'settings' | 'advanced' | null>('files');
@@ -126,6 +128,7 @@ const OCR = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
setExpandedStep(expandedStep === 'settings' ? null : 'settings');
}}
completedMessage={hasFiles && hasValidSettings && settingsCollapsed ? "Basic settings configured" : undefined}
+ tooltip={ocrTips}
>
;
+ toolPanelRef: React.RefObject;
+}
+
+export interface SidebarInfo {
+ rect: DOMRect | null;
+ isToolPanelActive: boolean;
+ sidebarState: SidebarState;
+}
+
+// Context-related interfaces
+export interface SidebarContextValue {
+ sidebarState: SidebarState;
+ sidebarRefs: SidebarRefs;
+ setSidebarsVisible: React.Dispatch>;
+ setLeftPanelView: React.Dispatch>;
+ setReaderMode: React.Dispatch>;
+}
+
+export interface SidebarProviderProps {
+ children: React.ReactNode;
+}
+
+// QuickAccessBar related interfaces
+export interface QuickAccessBarProps {
+ onToolsClick: () => void;
+ onReaderToggle: () => void;
+}
+
+export interface ButtonConfig {
+ id: string;
+ name: string;
+ icon: React.ReactNode;
+ tooltip: string;
+ isRound?: boolean;
+ size?: 'sm' | 'md' | 'lg' | 'xl';
+ onClick: () => void;
+ type?: 'navigation' | 'modal' | 'action';
+}
diff --git a/frontend/src/types/tips.ts b/frontend/src/types/tips.ts
new file mode 100644
index 000000000..58519e114
--- /dev/null
+++ b/frontend/src/types/tips.ts
@@ -0,0 +1,13 @@
+export interface TooltipContent {
+ header?: {
+ title: string;
+ logo?: string | React.ReactNode;
+ };
+ tips?: Array<{
+ title?: string;
+ description?: string;
+ bullets?: string[];
+ body?: React.ReactNode;
+ }>;
+ content?: React.ReactNode;
+}
\ No newline at end of file
diff --git a/frontend/src/utils/fileUtils.ts b/frontend/src/utils/fileUtils.ts
index b42d2f646..682cd9f3c 100644
--- a/frontend/src/utils/fileUtils.ts
+++ b/frontend/src/utils/fileUtils.ts
@@ -1,8 +1,8 @@
import { FileWithUrl } from "../types/file";
import { StoredFile, fileStorage } from "../services/fileStorage";
-export function getFileId(file: File): string {
- return (file as File & { id?: string }).id || file.name;
+export function getFileId(file: File): string | null {
+ return (file as File & { id?: string }).id || null;
}
/**
diff --git a/frontend/src/utils/genericUtils.ts b/frontend/src/utils/genericUtils.ts
new file mode 100644
index 000000000..253346292
--- /dev/null
+++ b/frontend/src/utils/genericUtils.ts
@@ -0,0 +1,42 @@
+/**
+ * DOM utility functions for common operations
+ */
+
+/**
+ * Clamps a value between a minimum and maximum
+ * @param value - The value to clamp
+ * @param min - The minimum allowed value
+ * @param max - The maximum allowed value
+ * @returns The clamped value
+ */
+export function clamp(value: number, min: number, max: number): number {
+ return Math.min(Math.max(value, min), max);
+}
+
+/**
+ * Safely adds an event listener with proper cleanup
+ * @param target - The target element or window/document
+ * @param event - The event type
+ * @param handler - The event handler function
+ * @param options - Event listener options
+ * @returns A cleanup function to remove the listener
+ */
+export function addEventListenerWithCleanup(
+ target: EventTarget,
+ event: string,
+ handler: EventListener,
+ options?: boolean | AddEventListenerOptions
+): () => void {
+ target.addEventListener(event, handler, options);
+ return () => target.removeEventListener(event, handler, options);
+}
+
+/**
+ * Checks if a click event occurred outside of a specified element
+ * @param event - The click event
+ * @param element - The element to check against
+ * @returns True if the click was outside the element
+ */
+export function isClickOutside(event: MouseEvent, element: HTMLElement | null): boolean {
+ return element ? !element.contains(event.target as Node) : true;
+}
diff --git a/frontend/src/utils/sidebarUtils.ts b/frontend/src/utils/sidebarUtils.ts
new file mode 100644
index 000000000..cef144971
--- /dev/null
+++ b/frontend/src/utils/sidebarUtils.ts
@@ -0,0 +1,34 @@
+import { SidebarRefs, SidebarState, SidebarInfo } from '../types/sidebar';
+
+/**
+ * Gets the All tools sidebar information using React refs and state
+ * @param refs - Object containing refs to sidebar elements
+ * @param state - Current sidebar state
+ * @returns Object containing the sidebar rect and whether the tool panel is active
+ */
+export function getSidebarInfo(refs: SidebarRefs, state: SidebarState): SidebarInfo {
+ const { quickAccessRef, toolPanelRef } = refs;
+ const { sidebarsVisible, readerMode } = state;
+
+ // Determine if tool panel should be active based on state
+ const isToolPanelActive = sidebarsVisible && !readerMode;
+
+ let rect: DOMRect | null = null;
+
+ if (isToolPanelActive && toolPanelRef.current) {
+ // Tool panel is expanded: use its rect
+ rect = toolPanelRef.current.getBoundingClientRect();
+ } else if (quickAccessRef.current) {
+ // Fall back to quick access bar
+ // This probably isn't needed but if we ever have tooltips or modals that need to be positioned relative to the quick access bar, we can use this
+ rect = quickAccessRef.current.getBoundingClientRect();
+ }
+
+ return {
+ rect,
+ isToolPanelActive,
+ sidebarState: state
+ };
+}
+
+
\ No newline at end of file
diff --git a/frontend/src/utils/thumbnailUtils.ts b/frontend/src/utils/thumbnailUtils.ts
index 35444035a..f4f224044 100644
--- a/frontend/src/utils/thumbnailUtils.ts
+++ b/frontend/src/utils/thumbnailUtils.ts
@@ -15,19 +15,172 @@ export function calculateScaleFromFileSize(fileSize: number): number {
}
/**
- * Generate thumbnail for a PDF file during upload
+ * Generate modern placeholder thumbnail with file extension
+ */
+function generatePlaceholderThumbnail(file: File): string {
+ const canvas = document.createElement('canvas');
+ canvas.width = 120;
+ canvas.height = 150;
+ const ctx = canvas.getContext('2d')!;
+
+ // Get file extension for color theming
+ const extension = file.name.split('.').pop()?.toUpperCase() || 'FILE';
+ const colorScheme = getFileTypeColorScheme(extension);
+
+ // Create gradient background
+ const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
+ gradient.addColorStop(0, colorScheme.bgTop);
+ gradient.addColorStop(1, colorScheme.bgBottom);
+
+ // Rounded rectangle background
+ drawRoundedRect(ctx, 8, 8, canvas.width - 16, canvas.height - 16, 8);
+ ctx.fillStyle = gradient;
+ ctx.fill();
+
+ // Subtle shadow/border
+ ctx.strokeStyle = colorScheme.border;
+ ctx.lineWidth = 1.5;
+ ctx.stroke();
+
+ // Modern document icon
+ drawModernDocumentIcon(ctx, canvas.width / 2, 45, colorScheme.icon);
+
+ // Extension badge
+ drawExtensionBadge(ctx, canvas.width / 2, canvas.height / 2 + 15, extension, colorScheme);
+
+ // File size with subtle styling
+ const sizeText = formatFileSize(file.size);
+ ctx.font = '11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
+ ctx.fillStyle = colorScheme.textSecondary;
+ ctx.textAlign = 'center';
+ ctx.fillText(sizeText, canvas.width / 2, canvas.height - 15);
+
+ return canvas.toDataURL();
+}
+
+/**
+ * Get color scheme based on file extension
+ */
+function getFileTypeColorScheme(extension: string) {
+ const schemes: Record = {
+ // Documents
+ 'PDF': { bgTop: '#FF6B6B20', bgBottom: '#FF6B6B10', border: '#FF6B6B40', icon: '#FF6B6B', badge: '#FF6B6B', textPrimary: '#FFFFFF', textSecondary: '#666666' },
+ 'DOC': { bgTop: '#4ECDC420', bgBottom: '#4ECDC410', border: '#4ECDC440', icon: '#4ECDC4', badge: '#4ECDC4', textPrimary: '#FFFFFF', textSecondary: '#666666' },
+ 'DOCX': { bgTop: '#4ECDC420', bgBottom: '#4ECDC410', border: '#4ECDC440', icon: '#4ECDC4', badge: '#4ECDC4', textPrimary: '#FFFFFF', textSecondary: '#666666' },
+ 'TXT': { bgTop: '#95A5A620', bgBottom: '#95A5A610', border: '#95A5A640', icon: '#95A5A6', badge: '#95A5A6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
+
+ // Spreadsheets
+ 'XLS': { bgTop: '#2ECC7120', bgBottom: '#2ECC7110', border: '#2ECC7140', icon: '#2ECC71', badge: '#2ECC71', textPrimary: '#FFFFFF', textSecondary: '#666666' },
+ 'XLSX': { bgTop: '#2ECC7120', bgBottom: '#2ECC7110', border: '#2ECC7140', icon: '#2ECC71', badge: '#2ECC71', textPrimary: '#FFFFFF', textSecondary: '#666666' },
+ 'CSV': { bgTop: '#2ECC7120', bgBottom: '#2ECC7110', border: '#2ECC7140', icon: '#2ECC71', badge: '#2ECC71', textPrimary: '#FFFFFF', textSecondary: '#666666' },
+
+ // Presentations
+ 'PPT': { bgTop: '#E67E2220', bgBottom: '#E67E2210', border: '#E67E2240', icon: '#E67E22', badge: '#E67E22', textPrimary: '#FFFFFF', textSecondary: '#666666' },
+ 'PPTX': { bgTop: '#E67E2220', bgBottom: '#E67E2210', border: '#E67E2240', icon: '#E67E22', badge: '#E67E22', textPrimary: '#FFFFFF', textSecondary: '#666666' },
+
+ // Archives
+ 'ZIP': { bgTop: '#9B59B620', bgBottom: '#9B59B610', border: '#9B59B640', icon: '#9B59B6', badge: '#9B59B6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
+ 'RAR': { bgTop: '#9B59B620', bgBottom: '#9B59B610', border: '#9B59B640', icon: '#9B59B6', badge: '#9B59B6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
+ '7Z': { bgTop: '#9B59B620', bgBottom: '#9B59B610', border: '#9B59B640', icon: '#9B59B6', badge: '#9B59B6', textPrimary: '#FFFFFF', textSecondary: '#666666' },
+
+ // Default
+ 'DEFAULT': { bgTop: '#74B9FF20', bgBottom: '#74B9FF10', border: '#74B9FF40', icon: '#74B9FF', badge: '#74B9FF', textPrimary: '#FFFFFF', textSecondary: '#666666' }
+ };
+
+ return schemes[extension] || schemes['DEFAULT'];
+}
+
+/**
+ * Draw rounded rectangle
+ */
+function drawRoundedRect(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number) {
+ ctx.beginPath();
+ ctx.moveTo(x + radius, y);
+ ctx.lineTo(x + width - radius, y);
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+ ctx.lineTo(x + width, y + height - radius);
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
+ ctx.lineTo(x + radius, y + height);
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
+ ctx.lineTo(x, y + radius);
+ ctx.quadraticCurveTo(x, y, x + radius, y);
+ ctx.closePath();
+}
+
+/**
+ * Draw modern document icon
+ */
+function drawModernDocumentIcon(ctx: CanvasRenderingContext2D, centerX: number, centerY: number, color: string) {
+ const size = 24;
+ ctx.fillStyle = color;
+ ctx.strokeStyle = color;
+ ctx.lineWidth = 2;
+
+ // Document body
+ drawRoundedRect(ctx, centerX - size/2, centerY - size/2, size, size * 1.2, 3);
+ ctx.fill();
+
+ // Folded corner
+ ctx.beginPath();
+ ctx.moveTo(centerX + size/2 - 6, centerY - size/2);
+ ctx.lineTo(centerX + size/2, centerY - size/2 + 6);
+ ctx.lineTo(centerX + size/2 - 6, centerY - size/2 + 6);
+ ctx.closePath();
+ ctx.fillStyle = '#FFFFFF40';
+ ctx.fill();
+}
+
+/**
+ * Draw extension badge
+ */
+function drawExtensionBadge(ctx: CanvasRenderingContext2D, centerX: number, centerY: number, extension: string, colorScheme: any) {
+ const badgeWidth = Math.max(extension.length * 8 + 16, 40);
+ const badgeHeight = 22;
+
+ // Badge background
+ drawRoundedRect(ctx, centerX - badgeWidth/2, centerY - badgeHeight/2, badgeWidth, badgeHeight, 11);
+ ctx.fillStyle = colorScheme.badge;
+ ctx.fill();
+
+ // Badge text
+ ctx.font = 'bold 11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
+ ctx.fillStyle = colorScheme.textPrimary;
+ ctx.textAlign = 'center';
+ ctx.fillText(extension, centerX, centerY + 4);
+}
+
+/**
+ * Format file size for display
+ */
+function formatFileSize(bytes: number): string {
+ if (bytes === 0) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
+}
+
+
+/**
+ * Generate thumbnail for any file type
* Returns base64 data URL or undefined if generation fails
*/
export async function generateThumbnailForFile(file: File): Promise {
- // Skip thumbnail generation for large files to avoid memory issues
- if (file.size >= 50 * 1024 * 1024) { // 50MB limit
+ // Skip thumbnail generation for very large files to avoid memory issues
+ if (file.size >= 100 * 1024 * 1024) { // 100MB limit
console.log('Skipping thumbnail generation for large file:', file.name);
- return undefined;
+ return generatePlaceholderThumbnail(file);
}
+ // Handle image files - use original file directly
+ if (file.type.startsWith('image/')) {
+ return URL.createObjectURL(file);
+ }
+
+ // Handle PDF files
if (!file.type.startsWith('application/pdf')) {
- console.warn('File is not a PDF, skipping thumbnail generation:', file.name);
- return undefined;
+ console.log('File is not a PDF or image, generating placeholder:', file.name);
+ return generatePlaceholderThumbnail(file);
}
try {