🖌 Icon fixes
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user