Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ const App: React.FC = () => {
{isOffline && (
<div className="bg-amber-500/10 text-amber-600 dark:text-amber-500 text-sm font-medium py-1.5 px-4 flex items-center justify-center gap-2 border-b border-amber-500/20">
<WifiOff size={16} />
<span>{t('offlineWarning') || 'You are currently offline. Local features are still available, but AI generation is disabled.'}</span>
<span>{t('offlineWarning')}</span>
</div>
)}

Expand Down
24 changes: 12 additions & 12 deletions src/components/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export const Sidebar = ({

<div className="flex gap-2">
<button onClick={handlers.onOpenSettings} className="flex-1 flex items-center justify-center gap-2 py-2.5 rounded-xl border border-zinc-200 dark:border-zinc-800 hover:bg-white dark:hover:bg-zinc-800 text-xs font-bold transition-colors shadow-sm"><Settings className="w-3.5 h-3.5" />{t('settings')}</button>
<div className="px-3 py-2 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-100 dark:border-emerald-500/20 flex items-center justify-center" title="Saved">
<div className="px-3 py-2 rounded-xl bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-100 dark:border-emerald-500/20 flex items-center justify-center" title={t('saved')}>
<CheckCircle className="w-4 h-4 text-emerald-500" />
</div>
</div>
Expand Down Expand Up @@ -187,14 +187,14 @@ export const Inspector = ({
) : (
<span
className="truncate max-w-[200px] text-zinc-800 dark:text-zinc-200 cursor-pointer hover:text-indigo-600"
title="Click to rename"
title={t('clickToRename')}
onClick={() => setIsRenaming(true)}
>
{activeImage.file.name}
</span>
)}
<div className="flex gap-1 shrink-0">
<span className="text-[10px] px-2 py-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 text-zinc-500 border border-zinc-200 dark:border-zinc-700 truncate max-w-[80px]">{inspectorProjectName || 'Unknown'}</span>
<span className="text-[10px] px-2 py-0.5 rounded-full bg-zinc-100 dark:bg-zinc-800 text-zinc-500 border border-zinc-200 dark:border-zinc-700 truncate max-w-[80px]">{inspectorProjectName || t('noCaption')}</span>
</div>
</div>
<div className="flex gap-2 text-[10px] text-zinc-400 font-mono">
Expand Down Expand Up @@ -225,11 +225,11 @@ export const Inspector = ({
className="w-full h-48 p-3 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950 text-sm font-mono focus:ring-2 focus:ring-indigo-500 focus:border-transparent resize-none text-zinc-800 dark:text-zinc-200"
value={activeImage.caption}
onChange={(e) => onUpdateCaption(e.target.value)}
placeholder="Caption..."
placeholder={t('captionPlaceholder')}
/>
<div className="flex justify-between">
<button onClick={onDownload} className="text-xs flex items-center gap-1 text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200 transition-colors"><Download className="w-3 h-3" /> Save .txt</button>
<span className="text-[10px] text-zinc-400">{activeImage.caption.length} chars</span>
<button onClick={onDownload} className="text-xs flex items-center gap-1 text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200 transition-colors"><Download className="w-3 h-3" /> {t('saveTxt')}</button>
<span className="text-[10px] text-zinc-400">{activeImage.caption.length} {t('chars')}</span>
</div>
</div>
</div>
Expand Down Expand Up @@ -292,12 +292,12 @@ export const SmartToolbar = ({
<div className="flex items-center gap-2 shrink-0">
{/* View Filters */}
<div className="flex items-center gap-1 bg-zinc-200/50 dark:bg-zinc-950 p-1 rounded-xl border border-zinc-300/50 dark:border-zinc-800/50">
<button onClick={() => setViewFilter('all')} className={`px-2.5 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center gap-2 ${viewFilter === 'all' ? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm' : 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200'}`} title="All Items"><Layers className="w-3.5 h-3.5" /></button>
<button onClick={() => setViewFilter('pending')} className={`px-2.5 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center gap-2 ${viewFilter === 'pending' ? 'bg-indigo-600 text-white shadow-sm' : 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200'}`} title="Pending">
<button onClick={() => setViewFilter('all')} className={`px-2.5 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center gap-2 ${viewFilter === 'all' ? 'bg-white dark:bg-zinc-800 text-zinc-900 dark:text-white shadow-sm' : 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200'}`} title={t('allItems')}><Layers className="w-3.5 h-3.5" /></button>
<button onClick={() => setViewFilter('pending')} className={`px-2.5 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center gap-2 ${viewFilter === 'pending' ? 'bg-indigo-600 text-white shadow-sm' : 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200'}`} title={t('pending')}>
<ListFilter className="w-3.5 h-3.5" />
{stats.pending > 0 && <span className="hidden md:inline bg-black/10 dark:bg-black/20 px-1.5 py-0.5 rounded text-[9px] font-mono">{stats.pending}</span>}
</button>
<button onClick={() => setViewFilter('completed')} className={`px-2.5 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center gap-2 ${viewFilter === 'completed' ? 'bg-emerald-600 text-white shadow-sm' : 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200'}`} title="Completed">
<button onClick={() => setViewFilter('completed')} className={`px-2.5 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center gap-2 ${viewFilter === 'completed' ? 'bg-emerald-600 text-white shadow-sm' : 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200'}`} title={t('completed')}>
<CheckSquare className="w-3.5 h-3.5" />
{stats.success > 0 && <span className="hidden md:inline bg-black/10 dark:bg-black/20 px-1.5 py-0.5 rounded text-[9px] font-mono">{stats.success}</span>}
</button>
Expand All @@ -307,8 +307,8 @@ export const SmartToolbar = ({

{/* View Mode Toggle */}
<div className="flex items-center gap-1 bg-zinc-200/50 dark:bg-zinc-950 p-1 rounded-xl border border-zinc-300/50 dark:border-zinc-800/50">
<button onClick={() => setViewMode('grid')} className={`p-1.5 rounded-lg transition-all ${viewMode === 'grid' ? 'bg-white dark:bg-zinc-800 text-indigo-600 dark:text-indigo-400 shadow-sm' : 'text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200'}`} title="Grid View"><Grid3X3 className="w-4 h-4" /></button>
<button onClick={() => setViewMode('list')} className={`p-1.5 rounded-lg transition-all ${viewMode === 'list' ? 'bg-white dark:bg-zinc-800 text-indigo-600 dark:text-indigo-400 shadow-sm' : 'text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200'}`} title="List View"><List className="w-4 h-4" /></button>
<button onClick={() => setViewMode('grid')} className={`p-1.5 rounded-lg transition-all ${viewMode === 'grid' ? 'bg-white dark:bg-zinc-800 text-indigo-600 dark:text-indigo-400 shadow-sm' : 'text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200'}`} title={t('gridView')}><Grid3X3 className="w-4 h-4" /></button>
<button onClick={() => setViewMode('list')} className={`p-1.5 rounded-lg transition-all ${viewMode === 'list' ? 'bg-white dark:bg-zinc-800 text-indigo-600 dark:text-indigo-400 shadow-sm' : 'text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200'}`} title={t('listView')}><List className="w-4 h-4" /></button>

{/* Grid Columns Slider */}
{viewMode === 'grid' && (
Expand Down Expand Up @@ -376,7 +376,7 @@ export const SmartToolbar = ({
<Filter className="w-4 h-4" />
<span className="hidden lg:inline">{t('batchEdit')}</span>
</button>
<button onClick={handlers.onOpenClean} className="flex items-center gap-2 px-2.5 py-2 bg-amber-500/10 text-amber-600 dark:text-amber-400 hover:bg-amber-500/20 border border-amber-500/20 rounded-xl text-xs font-bold whitespace-nowrap flex-shrink-0 transition-all active:scale-95" title="Clean Tags (Regex)">
<button onClick={handlers.onOpenClean} className="flex items-center gap-2 px-2.5 py-2 bg-amber-500/10 text-amber-600 dark:text-amber-400 hover:bg-amber-500/20 border border-amber-500/20 rounded-xl text-xs font-bold whitespace-nowrap flex-shrink-0 transition-all active:scale-95" title={t('cleanTagsRegex')}>
<Eraser className="w-4 h-4" />
<span className="hidden xl:inline">{t('cleanLabel')}</span>
</button>
Expand Down
19 changes: 10 additions & 9 deletions src/components/AppModals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,14 @@ export const SettingsModal = ({
<div className="flex bg-zinc-100 dark:bg-zinc-800 p-1 rounded-xl">
<button onClick={() => setLocalSettings(s => ({ ...s, language: 'en' }))} className={`flex-1 py-1.5 rounded-lg text-xs font-medium transition-all ${localSettings.language === 'en' ? 'bg-white dark:bg-zinc-600 shadow-sm text-zinc-900 dark:text-white' : 'text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300'}`}>English</button>
<button onClick={() => setLocalSettings(s => ({ ...s, language: 'zh' }))} className={`flex-1 py-1.5 rounded-lg text-xs font-medium transition-all ${localSettings.language === 'zh' ? 'bg-white dark:bg-zinc-600 shadow-sm text-zinc-900 dark:text-white' : 'text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300'}`}>中文</button>
<button onClick={() => setLocalSettings(s => ({ ...s, language: 'ja' }))} className={`flex-1 py-1.5 rounded-lg text-xs font-medium transition-all ${localSettings.language === 'ja' ? 'bg-white dark:bg-zinc-600 shadow-sm text-zinc-900 dark:text-white' : 'text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300'}`}>日本語</button>
</div>
</div>
<div>
<label className="block text-xs font-bold text-zinc-500 uppercase mb-2">{t('theme')}</label>
<div className="flex bg-zinc-100 dark:bg-zinc-800 p-1 rounded-xl">
<button onClick={() => handleThemeChange('light')} className={`flex-1 py-1.5 rounded-lg text-xs font-medium flex items-center justify-center gap-1.5 transition-all ${localSettings.theme === 'light' ? 'bg-white text-amber-600 shadow-sm' : 'text-zinc-500 hover:text-zinc-700'}`}><Sun className="w-3.5 h-3.5" /> Light</button>
<button onClick={() => handleThemeChange('dark')} className={`flex-1 py-1.5 rounded-lg text-xs font-medium flex items-center justify-center gap-1.5 transition-all ${localSettings.theme === 'dark' ? 'bg-zinc-700 text-white shadow-sm' : 'text-zinc-500 hover:text-zinc-300'}`}><Moon className="w-3.5 h-3.5" /> Dark</button>
<button onClick={() => handleThemeChange('light')} className={`flex-1 py-1.5 rounded-lg text-xs font-medium flex items-center justify-center gap-1.5 transition-all ${localSettings.theme === 'light' ? 'bg-white text-amber-600 shadow-sm' : 'text-zinc-500 hover:text-zinc-700'}`}><Sun className="w-3.5 h-3.5" /> {t('themeLight')}</button>
<button onClick={() => handleThemeChange('dark')} className={`flex-1 py-1.5 rounded-lg text-xs font-medium flex items-center justify-center gap-1.5 transition-all ${localSettings.theme === 'dark' ? 'bg-zinc-700 text-white shadow-sm' : 'text-zinc-500 hover:text-zinc-300'}`}><Moon className="w-3.5 h-3.5" /> {t('themeDark')}</button>
</div>
</div>
</div>
Expand Down Expand Up @@ -456,32 +457,32 @@ export const ExportModal = ({
<div className="flex justify-between items-center"><h2 className="text-xl font-bold">{t('exportAll')}</h2><button onClick={onClose}><X className="w-5 h-5" /></button></div>

<div className="space-y-3">
<label className="block text-xs font-bold text-zinc-500 uppercase">Format</label>
<label className="block text-xs font-bold text-zinc-500 uppercase">{t('format')}</label>
<div className="flex flex-col gap-2">
<button onClick={() => setFormat('txt')} className={`flex items-center gap-3 p-3 rounded-lg border transition-all ${format === 'txt' ? 'bg-indigo-50 border-indigo-500 text-indigo-700 dark:bg-indigo-500/20 dark:border-indigo-500/50 dark:text-indigo-300' : 'border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800'}`}>
<div className={`w-4 h-4 rounded-full border flex items-center justify-center ${format === 'txt' ? 'border-indigo-500' : 'border-zinc-400'}`}>
{format === 'txt' && <div className="w-2 h-2 rounded-full bg-indigo-500" />}
</div>
<div className="text-left">
<div className="font-bold text-sm">Text Files (.txt)</div>
<div className="text-xs opacity-70">Standard caption files</div>
<div className="font-bold text-sm">{t('textFilesTxt')}</div>
<div className="text-xs opacity-70">{t('standardCaptionFiles')}</div>
</div>
</button>
<button onClick={() => setFormat('json')} className={`flex items-center gap-3 p-3 rounded-lg border transition-all ${format === 'json' ? 'bg-indigo-50 border-indigo-500 text-indigo-700 dark:bg-indigo-500/20 dark:border-indigo-500/50 dark:text-indigo-300' : 'border-zinc-200 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800'}`}>
<div className={`w-4 h-4 rounded-full border flex items-center justify-center ${format === 'json' ? 'border-indigo-500' : 'border-zinc-400'}`}>
{format === 'json' && <div className="w-2 h-2 rounded-full bg-indigo-500" />}
</div>
<div className="text-left">
<div className="font-bold text-sm">JSON Files (.json)</div>
<div className="text-xs opacity-70">Sidecar JSON format</div>
<div className="font-bold text-sm">{t('jsonFilesJson')}</div>
<div className="text-xs opacity-70">{t('sidecarJsonFormat')}</div>
</div>
</button>
</div>
</div>

<div className="flex justify-end gap-2">
<button onClick={onClose} className="px-4 py-2 text-sm font-medium text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200">Cancel</button>
<button onClick={() => { onExport(format); onClose(); }} className="bg-zinc-900 dark:bg-white text-white dark:text-black px-6 py-2 rounded-lg font-bold text-sm">Export</button>
<button onClick={onClose} className="px-4 py-2 text-sm font-medium text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200">{t('cancel')}</button>
<button onClick={() => { onExport(format); onClose(); }} className="bg-zinc-900 dark:bg-white text-white dark:text-black px-6 py-2 rounded-lg font-bold text-sm">{t('export')}</button>
</div>
</div>
</div>
Expand Down
13 changes: 8 additions & 5 deletions src/components/GlobalFallbacks.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React from 'react';
import { AlertTriangle, RotateCcw } from 'lucide-react';
import { FallbackProps } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';

export function GlobalErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
const { t } = useTranslation();
return (
<div className="flex flex-col items-center justify-center min-h-[50vh] p-8 text-center bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-red-100 dark:border-red-900/30 m-4">
<div className="w-16 h-16 bg-red-50 dark:bg-red-900/20 text-red-500 rounded-full flex items-center justify-center mb-6">
<AlertTriangle size={32} />
</div>
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-2">Something went wrong</h2>
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-2">{t('somethingWentWrong')}</h2>
<p className="text-gray-500 dark:text-gray-400 max-w-md mb-6 whitespace-pre-wrap font-mono text-sm bg-gray-50 dark:bg-gray-900/50 p-4 rounded-lg border border-gray-100 dark:border-gray-800 text-left overflow-auto max-h-40">
{error instanceof Error ? error.message : String(error)}
</p>
Expand All @@ -17,25 +19,26 @@ export function GlobalErrorFallback({ error, resetErrorBoundary }: FallbackProps
className="flex items-center gap-2 px-6 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors shadow-sm"
>
<RotateCcw size={18} />
Try Again
{t('tryAgain')}
</button>
</div>
);
}

export function ViewErrorFallback({ resetErrorBoundary }: FallbackProps) {
const { t } = useTranslation();
return (
<div className="flex flex-col items-center justify-center h-full min-h-[300px] p-6 text-center border-2 border-dashed border-red-200 dark:border-red-900/30 rounded-xl m-4 bg-red-50/50 dark:bg-red-900/10">
<AlertTriangle size={24} className="text-red-500 mb-3" />
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2">View Error</h3>
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2">{t('viewError')}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 max-w-sm mb-4">
Failed to render this section. The rest of the app should still work.
{t('viewErrorDesc')}
</p>
<button
onClick={resetErrorBoundary}
className="px-4 py-2 bg-white dark:bg-gray-800 text-red-600 dark:text-red-400 border border-red-200 dark:border-red-900/50 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg text-sm font-medium transition-colors"
>
Reload View
{t('reloadView')}
</button>
</div>
);
Expand Down
Loading