🖌 Icon fixes

This commit is contained in:
2026-03-09 22:31:59 -04:00
parent 867935be39
commit 8546641152
6 changed files with 64 additions and 173 deletions

View File

@@ -131,12 +131,6 @@ export const RecentlyImported = (
</span> </span>
)} )}
</div> </div>
{/* Raw file presence */}
{isMissingFile && (
<span className="h-6 w-5 sm:shrink-0 sm:items-center sm:gap-2">
<i className="icon-[solar--file-corrupted-outline] h-5 w-5" />
</span>
)}
</div> </div>
</Card> </Card>
</div> </div>

View File

@@ -4,7 +4,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { import {
useGetImportStatisticsQuery, useGetImportStatisticsQuery,
useGetWantedComicsQuery, useGetWantedComicsQuery,
useStartIncrementalImportMutation useStartIncrementalImportMutation,
} from "../../graphql/generated"; } from "../../graphql/generated";
import { useStore } from "../../store"; import { useStore } from "../../store";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
@@ -25,12 +25,12 @@ export const RealTimeImportStats = (): ReactElement => {
getSocket: state.getSocket, getSocket: state.getSocket,
disconnectSocket: state.disconnectSocket, disconnectSocket: state.disconnectSocket,
importJobQueue: state.importJobQueue, importJobQueue: state.importJobQueue,
})) })),
); );
const { data: importStats, isLoading } = useGetImportStatisticsQuery( const { data: importStats, isLoading } = useGetImportStatisticsQuery(
{}, {},
{ refetchOnWindowFocus: false, refetchInterval: false } { refetchOnWindowFocus: false, refetchInterval: false },
); );
const stats = importStats?.getImportStatistics?.stats; const stats = importStats?.getImportStatistics?.stats;
@@ -45,7 +45,7 @@ export const RealTimeImportStats = (): ReactElement => {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchInterval: false, refetchInterval: false,
enabled: (stats?.missingFiles ?? 0) > 0, enabled: (stats?.missingFiles ?? 0) > 0,
} },
); );
const missingDocs = missingComicsData?.getComicBooks?.docs ?? []; const missingDocs = missingComicsData?.getComicBooks?.docs ?? [];
@@ -64,17 +64,20 @@ export const RealTimeImportStats = (): ReactElement => {
const importSession = useImportSessionStatus(); const importSession = useImportSessionStatus();
const { mutate: startIncrementalImport, isPending: isStartingImport } = useStartIncrementalImportMutation({ const { mutate: startIncrementalImport, isPending: isStartingImport } =
onSuccess: (data) => { useStartIncrementalImportMutation({
if (data.startIncrementalImport.success) { onSuccess: (data) => {
importJobQueue.setStatus("running"); if (data.startIncrementalImport.success) {
setImportError(null); importJobQueue.setStatus("running");
} setImportError(null);
}, }
onError: (error: any) => { },
setImportError(error?.message || "Failed to start import. Please try again."); onError: (error: any) => {
}, setImportError(
}); error?.message || "Failed to start import. Please try again.",
);
},
});
const hasNewFiles = stats && stats.newFiles > 0; const hasNewFiles = stats && stats.newFiles > 0;
const missingCount = stats?.missingFiles ?? 0; const missingCount = stats?.missingFiles ?? 0;
@@ -113,7 +116,7 @@ export const RealTimeImportStats = (): ReactElement => {
if (importSession.isActive) { if (importSession.isActive) {
setImportError( setImportError(
`Cannot start import: An import session "${importSession.sessionId}" is already active. Please wait for it to complete.` `Cannot start import: An import session "${importSession.sessionId}" is already active. Please wait for it to complete.`,
); );
return; return;
} }
@@ -148,7 +151,9 @@ export const RealTimeImportStats = (): ReactElement => {
const hasSessionStats = importSession.isActive && sessionStats !== null; const hasSessionStats = importSession.isActive && sessionStats !== null;
const totalFiles = stats.totalLocalFiles; const totalFiles = stats.totalLocalFiles;
const importedCount = hasSessionStats ? sessionStats!.filesSucceeded : stats.alreadyImported; const importedCount = hasSessionStats
? sessionStats!.filesSucceeded
: stats.alreadyImported;
const failedCount = hasSessionStats ? sessionStats!.filesFailed : 0; const failedCount = hasSessionStats ? sessionStats!.filesFailed : 0;
const showProgressBar = importSession.isActive; const showProgressBar = importSession.isActive;
@@ -165,8 +170,12 @@ export const RealTimeImportStats = (): ReactElement => {
<i className="h-6 w-6 icon-[solar--danger-circle-bold]"></i> <i className="h-6 w-6 icon-[solar--danger-circle-bold]"></i>
</span> </span>
<div className="flex-1"> <div className="flex-1">
<p className="font-semibold text-red-800 dark:text-red-300">Import Error</p> <p className="font-semibold text-red-800 dark:text-red-300">
<p className="text-sm text-red-700 dark:text-red-400 mt-1">{importError}</p> Import Error
</p>
<p className="text-sm text-red-700 dark:text-red-400 mt-1">
{importError}
</p>
</div> </div>
<button <button
onClick={() => setImportError(null)} onClick={() => setImportError(null)}
@@ -181,7 +190,7 @@ export const RealTimeImportStats = (): ReactElement => {
{/* File detected toast */} {/* File detected toast */}
{detectedFile && ( {detectedFile && (
<div className="rounded-lg border-l-4 border-blue-500 bg-blue-50 dark:bg-blue-900/20 p-3 flex items-center gap-3"> <div className="rounded-lg border-l-4 border-blue-500 bg-blue-50 dark:bg-blue-900/20 p-3 flex items-center gap-3">
<i className="h-5 w-5 text-blue-600 dark:text-blue-400 icon-[solar--file-add-bold-duotone] shrink-0"></i> <i className="h-5 w-5 text-blue-600 dark:text-blue-400 icon-[solar--document-add-bold-duotone] shrink-0"></i>
<p className="text-sm text-blue-800 dark:text-blue-300 font-mono truncate"> <p className="text-sm text-blue-800 dark:text-blue-300 font-mono truncate">
New file detected: {detectedFile} New file detected: {detectedFile}
</p> </p>
@@ -229,14 +238,22 @@ export const RealTimeImportStats = (): ReactElement => {
{/* Stats cards */} {/* Stats cards */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
{/* Total files */} {/* Total files */}
<div className="rounded-lg p-6 text-center" style={{ backgroundColor: '#6b7280' }}> <div
className="rounded-lg p-6 text-center"
style={{ backgroundColor: "#6b7280" }}
>
<div className="text-4xl font-bold text-white mb-2">{totalFiles}</div> <div className="text-4xl font-bold text-white mb-2">{totalFiles}</div>
<div className="text-sm text-gray-200 font-medium">total files</div> <div className="text-sm text-gray-200 font-medium">total files</div>
</div> </div>
{/* Imported */} {/* Imported */}
<div className="rounded-lg p-6 text-center" style={{ backgroundColor: '#d8dab2' }}> <div
<div className="text-4xl font-bold text-gray-800 mb-2">{importedCount}</div> className="rounded-lg p-6 text-center"
style={{ backgroundColor: "#d8dab2" }}
>
<div className="text-4xl font-bold text-gray-800 mb-2">
{importedCount}
</div>
<div className="text-sm text-gray-700 font-medium"> <div className="text-sm text-gray-700 font-medium">
{importSession.isActive ? "imported so far" : "imported"} {importSession.isActive ? "imported so far" : "imported"}
</div> </div>
@@ -245,16 +262,20 @@ export const RealTimeImportStats = (): ReactElement => {
{/* Failed — only shown after a session with failures */} {/* Failed — only shown after a session with failures */}
{showFailedCard && ( {showFailedCard && (
<div className="rounded-lg p-6 text-center bg-red-500"> <div className="rounded-lg p-6 text-center bg-red-500">
<div className="text-4xl font-bold text-white mb-2">{failedCount}</div> <div className="text-4xl font-bold text-white mb-2">
{failedCount}
</div>
<div className="text-sm text-red-100 font-medium">failed</div> <div className="text-sm text-red-100 font-medium">failed</div>
</div> </div>
)} )}
{/* Missing files — shown when watcher detects moved/deleted files */} {/* Missing files — shown when watcher detects moved/deleted files */}
{showMissingCard && ( {showMissingCard && (
<div className="rounded-lg p-6 text-center bg-amber-500"> <div className="rounded-lg p-6 text-center bg-card-missing">
<div className="text-4xl font-bold text-white mb-2">{missingCount}</div> <div className="text-4xl font-bold text-slate-700 mb-2">
<div className="text-sm text-amber-100 font-medium">missing</div> {missingCount}
</div>
<div className="text-sm text-slate-800 font-medium">missing</div>
</div> </div>
)} )}
</div> </div>
@@ -269,12 +290,16 @@ export const RealTimeImportStats = (): ReactElement => {
{missingCount} {missingCount === 1 ? "file" : "files"} missing {missingCount} {missingCount === 1 ? "file" : "files"} missing
</p> </p>
<p className="text-sm text-amber-700 dark:text-amber-400 mt-1"> <p className="text-sm text-amber-700 dark:text-amber-400 mt-1">
These files were previously imported but can no longer be found on disk. Move them back to restore access. These files were previously imported but can no longer be found
on disk. Move them back to restore access.
</p> </p>
{missingDocs.length > 0 && ( {missingDocs.length > 0 && (
<ul className="mt-2 space-y-1"> <ul className="mt-2 space-y-1">
{missingDocs.map((comic, i) => ( {missingDocs.map((comic, i) => (
<li key={i} className="text-xs text-amber-700 dark:text-amber-400 truncate"> <li
key={i}
className="text-xs text-amber-700 dark:text-amber-400 truncate"
>
{getMissingComicLabel(comic)} is missing {getMissingComicLabel(comic)} is missing
</li> </li>
))} ))}
@@ -289,8 +314,12 @@ export const RealTimeImportStats = (): ReactElement => {
to="/library?filter=missingFiles" to="/library?filter=missingFiles"
className="inline-flex items-center gap-1.5 mt-3 text-xs font-medium text-amber-800 dark:text-amber-300 underline underline-offset-2 hover:text-amber-600" className="inline-flex items-center gap-1.5 mt-3 text-xs font-medium text-amber-800 dark:text-amber-300 underline underline-offset-2 hover:text-amber-600"
> >
<i className="h-4 w-4 icon-[solar--library-bold-duotone]"></i>
View all in Library <span className="underline">
<i className="icon-[solar--file-corrupted-outline] w-4 h-4 px-3" />
View Missing Files In Library
<i className="icon-[solar--arrow-right-up-outline] w-3 h-3" />
</span>
</Link> </Link>
</div> </div>
</div> </div>

View File

@@ -176,7 +176,7 @@ export const Library = (): ReactElement => {
id: "missingStatus", id: "missingStatus",
cell: () => ( cell: () => (
<div className="flex flex-col items-center gap-1.5 px-2 py-3 min-w-[80px]"> <div className="flex flex-col items-center gap-1.5 px-2 py-3 min-w-[80px]">
<i className="icon-[solar--file-broken-bold] w-8 h-8 text-red-500"></i> <i className="icon-[solar--file-corrupted-outline] w-8 h-8 text-red-500"></i>
<span className="inline-flex items-center rounded-md bg-red-100 px-2 py-1 text-xs font-semibold text-red-700 ring-1 ring-inset ring-red-600/20"> <span className="inline-flex items-center rounded-md bg-red-100 px-2 py-1 text-xs font-semibold text-red-700 ring-1 ring-inset ring-red-600/20">
MISSING MISSING
</span> </span>

View File

@@ -116,7 +116,7 @@ const renderCard = (props: ICardProps): ReactElement => {
)} )}
{props.cardState === "missing" && ( {props.cardState === "missing" && (
<div className="absolute inset-0 flex items-center justify-center rounded-t-md bg-card-missing/70"> <div className="absolute inset-0 flex items-center justify-center rounded-t-md bg-card-missing/70">
<i className="icon-[solar--file-broken-bold] w-16 h-16 text-red-500" /> <i className="icon-[solar--file-corrupted-outline] w-16 h-16 text-red-500" />
</div> </div>
)} )}
</div> </div>

View File

@@ -78,7 +78,7 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
{rawFileDetails.fileSize != null && ( {rawFileDetails.fileSize != null && (
<span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-1.5 sm:px-2 rounded-md dark:text-slate-900 dark:bg-slate-400"> <span className="inline-flex items-center bg-slate-50 text-slate-800 text-xs font-medium px-1.5 sm:px-2 rounded-md dark:text-slate-900 dark:bg-slate-400">
<span className="pr-1 pt-1"> <span className="pr-1 pt-1">
<i className="icon-[solar--mirror-right-bold-duotone] w-4 h-4 sm:w-5 sm:h-5"></i> <i className="icon-[solar--database-bold-duotone] w-4 h-4 sm:w-5 sm:h-5"></i>
</span> </span>
<span className="text-xs sm:text-md text-slate-500 dark:text-slate-900"> <span className="text-xs sm:text-md text-slate-500 dark:text-slate-900">
{prettyBytes(rawFileDetails.fileSize)} {prettyBytes(rawFileDetails.fileSize)}
@@ -89,7 +89,7 @@ export const MetadataPanel = (props: IMetadatPanelProps): ReactElement => {
{/* Missing file Icon */} {/* Missing file Icon */}
{isMissing && ( {isMissing && (
<span className="pr-2 pt-1" title="File backing this comic is missing"> <span className="pr-2 pt-1" title="File backing this comic is missing">
<i className="icon-[solar--file-remove-broken] w-5 h-5 text-red-600 shrink-0"></i> <i className="icon-[solar--file-corrupted-outline] w-5 h-5 text-red-600 shrink-0"></i>
</span> </span>
)} )}

View File

@@ -1,132 +0,0 @@
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export function iconifyPlugin() {
const iconCache = new Map();
const collections = new Map();
function loadCollection(prefix) {
if (collections.has(prefix)) {
return collections.get(prefix);
}
try {
const collectionPath = join(__dirname, 'node_modules', '@iconify-json', prefix, 'icons.json');
const data = JSON.parse(readFileSync(collectionPath, 'utf8'));
collections.set(prefix, data);
return data;
} catch (e) {
return null;
}
}
function getIconCSS(iconData, selector) {
const { body, width, height } = iconData;
const viewBox = `0 0 ${width || 24} ${height || 24}`;
// Create SVG data URI
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}">${body}</svg>`;
const dataUri = `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`;
return `${selector} {
display: inline-block;
width: 1em;
height: 1em;
background-color: currentColor;
-webkit-mask-image: url("${dataUri}");
mask-image: url("${dataUri}");
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}`;
}
return {
name: 'vite-plugin-iconify',
transform(code, id) {
// Only process files that might contain icon classes
if (!id.endsWith('.tsx') && !id.endsWith('.jsx') && !id.endsWith('.ts') && !id.endsWith('.js')) {
return null;
}
// Find all icon-[...] patterns
const iconPattern = /icon-\[([^\]]+)\]/g;
const matches = [...code.matchAll(iconPattern)];
if (matches.length === 0) {
return null;
}
// Extract unique icons
const icons = new Set(matches.map(m => m[1]));
// Generate CSS for each icon
for (const iconName of icons) {
if (iconCache.has(iconName)) {
continue;
}
try {
// Parse icon name (e.g., "solar--add-square-bold-duotone")
const parts = iconName.split('--');
if (parts.length !== 2) continue;
const [prefix, name] = parts;
// Load collection
const collection = loadCollection(prefix);
if (!collection || !collection.icons || !collection.icons[name]) {
continue;
}
// Get icon data
const iconData = collection.icons[name];
// Generate CSS
const selector = `.icon-\\[${iconName}\\]`;
const iconCSS = getIconCSS(iconData, selector);
iconCache.set(iconName, iconCSS);
} catch (e) {
// Silently skip failed icons
}
}
return null;
},
resolveId(id) {
if (id === '/@iconify-css') {
return id;
}
},
load(id) {
if (id === '/@iconify-css') {
const allCSS = Array.from(iconCache.values()).join('\n');
return allCSS;
}
},
transformIndexHtml() {
// Inject icon CSS into HTML
const allCSS = Array.from(iconCache.values()).join('\n');
if (allCSS) {
return [
{
tag: 'style',
attrs: { type: 'text/css' },
children: allCSS,
injectTo: 'head'
}
];
}
}
};
}