support webp, some fixes.

This commit is contained in:
Stefan Hardegger
2026-06-08 09:19:42 +02:00
parent ca20e54115
commit f4908637b3
7 changed files with 760 additions and 22 deletions

View File

@@ -56,6 +56,14 @@ export default function SystemSettings({}: SystemSettingsProps) {
execute: { loading: false, message: '' }
});
const [migrationStatus, setMigrationStatus] = useState<{
preview: { loading: boolean; message: string; success?: boolean; data?: any };
execute: { loading: boolean; message: string; success?: boolean };
}>({
preview: { loading: false, message: '' },
execute: { loading: false, message: '' }
});
const [hoveredImage, setHoveredImage] = useState<{ src: string; alt: string } | null>(null);
const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
@@ -401,6 +409,115 @@ export default function SystemSettings({}: SystemSettingsProps) {
}, 10000);
};
const handleEpubMigrationPreview = async () => {
setMigrationStatus(prev => ({
...prev,
preview: { loading: true, message: 'Scanning for misplaced EPUB images...', success: undefined }
}));
try {
const result = await configApi.previewEpubImageMigration();
if (result.success) {
const matchedCount = result.movedFiles.filter(f => f.action !== 'unmatched').length;
setMigrationStatus(prev => ({
...prev,
preview: {
loading: false,
message: `Found ${matchedCount} image(s) to move into story subfolders, ${result.unmatchedCount} unmatched (no story reference found).`,
success: true,
data: result
}
}));
} else {
setMigrationStatus(prev => ({
...prev,
preview: {
loading: false,
message: result.error || 'Preview failed',
success: false
}
}));
}
} catch (error: any) {
setMigrationStatus(prev => ({
...prev,
preview: {
loading: false,
message: error.message || 'Network error occurred',
success: false
}
}));
}
};
const handleEpubMigrationExecute = async () => {
const matchedCount = migrationStatus.preview.data?.movedFiles?.filter((f: any) => f.action !== 'unmatched').length ?? 0;
if (!migrationStatus.preview.data || matchedCount === 0) {
setMigrationStatus(prev => ({
...prev,
execute: {
loading: false,
message: 'Please run preview first, or there are no images to migrate.',
success: false
}
}));
return;
}
const confirmed = window.confirm(
`Move ${matchedCount} EPUB image(s) into their story subfolders? Story content URLs will be updated automatically.`
);
if (!confirmed) return;
setMigrationStatus(prev => ({
...prev,
execute: { loading: true, message: 'Migrating EPUB images...', success: undefined }
}));
try {
const result = await configApi.executeEpubImageMigration();
if (result.success) {
setMigrationStatus(prev => ({
...prev,
execute: {
loading: false,
message: `Successfully moved ${result.movedCount} image(s). ${result.unmatchedCount} unmatched file(s) left in place.${result.hasErrors ? ` Errors: ${result.errors.length}` : ''}`,
success: true
},
preview: { loading: false, message: '', success: undefined, data: undefined }
}));
} else {
setMigrationStatus(prev => ({
...prev,
execute: {
loading: false,
message: result.error || 'Migration failed',
success: false
}
}));
}
} catch (error: any) {
setMigrationStatus(prev => ({
...prev,
execute: {
loading: false,
message: error.message || 'Network error occurred',
success: false
}
}));
}
setTimeout(() => {
setMigrationStatus(prev => ({
...prev,
execute: { loading: false, message: '', success: undefined }
}));
}, 10000);
};
// Search Engine Management Functions
const loadSearchEngineStatus = async () => {
try {
@@ -856,6 +973,147 @@ export default function SystemSettings({}: SystemSettingsProps) {
<li> <strong>Backup recommended:</strong> Consider backing up before large cleanups</li>
</ul>
</div>
{/* EPUB Image Migration Section */}
<div className="border theme-border rounded-lg p-4">
<h3 className="text-lg font-semibold theme-header mb-3">📦 EPUB Image Migration</h3>
<p className="text-sm theme-text mb-4">
Move images from EPUB imports into the correct story subfolders. These images are still
referenced by stories and display correctly, but were placed flat in the content directory
due to an import bug. Run this once to reorganise them.
</p>
<div className="flex flex-col sm:flex-row gap-3 mb-3">
<Button
onClick={handleEpubMigrationPreview}
disabled={migrationStatus.preview.loading || migrationStatus.execute.loading}
loading={migrationStatus.preview.loading}
variant="ghost"
className="flex-1"
>
{migrationStatus.preview.loading ? 'Scanning...' : 'Preview Migration'}
</Button>
<Button
onClick={handleEpubMigrationExecute}
disabled={
migrationStatus.execute.loading ||
!migrationStatus.preview.data ||
migrationStatus.preview.data.movedFiles?.filter((f: any) => f.action !== 'unmatched').length === 0
}
loading={migrationStatus.execute.loading}
variant="secondary"
className="flex-1"
>
{migrationStatus.execute.loading ? 'Migrating...' : 'Execute Migration'}
</Button>
{migrationStatus.preview.data && (
<Button
onClick={() => setMigrationStatus(prev => ({
...prev,
preview: { loading: false, message: '', success: undefined, data: undefined }
}))}
variant="ghost"
className="px-4 py-2 text-sm"
>
Clear Preview
</Button>
)}
</div>
{migrationStatus.preview.message && (
<div className={`text-sm p-3 rounded mb-3 ${
migrationStatus.preview.success
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-800 dark:text-blue-200'
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
}`}>
{migrationStatus.preview.message}
</div>
)}
{migrationStatus.execute.message && (
<div className={`text-sm p-3 rounded mb-3 ${
migrationStatus.execute.success
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
}`}>
{migrationStatus.execute.message}
</div>
)}
{migrationStatus.preview.data && migrationStatus.preview.success && (
<div className="text-sm theme-text bg-gray-50 dark:bg-gray-800 p-3 rounded border">
<div className="grid grid-cols-2 gap-3 mb-3">
<div>
<span className="font-medium">To Move:</span>{' '}
{migrationStatus.preview.data.movedFiles?.filter((f: any) => f.action !== 'unmatched').length ?? 0}
</div>
<div>
<span className="font-medium">Unmatched:</span>{' '}
{migrationStatus.preview.data.unmatchedCount}
</div>
</div>
{migrationStatus.preview.data.movedFiles?.length > 0 && (
<details>
<summary className="cursor-pointer font-medium text-sm theme-header mb-2">
📁 View Files ({migrationStatus.preview.data.movedFiles.length})
</summary>
<div className="mt-3 max-h-80 overflow-y-auto border theme-border rounded">
<table className="w-full text-xs">
<thead className="bg-gray-100 dark:bg-gray-800 sticky top-0">
<tr>
<th className="text-left p-2 font-medium">File Name</th>
<th className="text-left p-2 font-medium">Size</th>
<th className="text-left p-2 font-medium">Story</th>
<th className="text-left p-2 font-medium">Action</th>
</tr>
</thead>
<tbody>
{migrationStatus.preview.data.movedFiles.map((file: any, index: number) => (
<tr key={index} className="border-t theme-border hover:bg-gray-50 dark:hover:bg-gray-800">
<td className="p-2">
<div className="truncate max-w-xs" title={file.fileName}>
🖼 {file.fileName}
</div>
</td>
<td className="p-2">{file.formattedSize}</td>
<td className="p-2">
{file.storyTitle ? (
<a
href={`/stories/${file.storyId}`}
className="text-blue-600 dark:text-blue-400 hover:underline truncate max-w-xs block"
title={file.storyTitle}
>
{file.storyTitle}
</a>
) : (
<span className="text-gray-400">No match</span>
)}
</td>
<td className="p-2">
{file.action === 'move' && <span className="text-blue-600 dark:text-blue-400">Move</span>}
{file.action === 'copy' && <span className="text-purple-600 dark:text-purple-400">Copy</span>}
{file.action === 'unmatched' && <span className="text-gray-500">Skip</span>}
</td>
</tr>
))}
</tbody>
</table>
</div>
</details>
)}
</div>
)}
<div className="text-sm theme-text bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg mt-3">
<p className="font-medium mb-1"> Notes:</p>
<ul className="text-xs space-y-1 ml-4">
<li> <strong>Safe to run multiple times</strong> after migration, no flat images remain and the scan returns zero</li>
<li> <strong>Unmatched files</strong> have no story reference and are left in place use the orphaned cleanup to remove them</li>
<li> Story content URLs are updated automatically so images continue to display correctly</li>
</ul>
</div>
</div>
</div>
</div>

View File

@@ -763,6 +763,52 @@ export const configApi = {
const response = await api.post('/config/cleanup/images/execute');
return response.data;
},
previewEpubImageMigration: async (): Promise<{
success: boolean;
movedCount: number;
unmatchedCount: number;
errors: string[];
hasErrors: boolean;
dryRun: boolean;
movedFiles: {
fileName: string;
filePath: string;
targetPath?: string;
fileSize: number;
formattedSize: string;
storyId: string | null;
storyTitle: string | null;
action: 'move' | 'copy' | 'unmatched';
}[];
error?: string;
}> => {
const response = await api.post('/config/migrate/epub-images/preview');
return response.data;
},
executeEpubImageMigration: async (): Promise<{
success: boolean;
movedCount: number;
unmatchedCount: number;
errors: string[];
hasErrors: boolean;
dryRun: boolean;
movedFiles: {
fileName: string;
filePath: string;
targetPath?: string;
fileSize: number;
formattedSize: string;
storyId: string | null;
storyTitle: string | null;
action: 'move' | 'copy' | 'unmatched';
}[];
error?: string;
}> => {
const response = await api.post('/config/migrate/epub-images/execute');
return response.data;
},
};
// Search Engine Management API