fix: add failed tag locate and filter controls
Add a failed-tag discovery flow for background worker failures. Changes: - Add a Failed Tags toolbar filter that appears when tag failures exist. - Add Locate buttons for failed tag tasks in the background worker prompt. - Route Locate to the affected folder and filter the gallery to images with tagger errors. - Fetch and display failed tag filenames/errors in the expanded worker details. - Add a backend query and gallery filter flag for images with failed AI tagging. - Improve subtle-light contrast for failed worker chips, filenames, and Locate/Retry buttons. - Also slightly increases the title bar update indicator pulse size for better visibility.
This commit is contained in:
@@ -38,6 +38,7 @@ pub struct GetImagesParams {
|
|||||||
pub favorites_only: Option<bool>,
|
pub favorites_only: Option<bool>,
|
||||||
pub rating_min: Option<i64>,
|
pub rating_min: Option<i64>,
|
||||||
pub embedding_failed_only: Option<bool>,
|
pub embedding_failed_only: Option<bool>,
|
||||||
|
pub tagging_failed_only: Option<bool>,
|
||||||
pub sort: Option<String>,
|
pub sort: Option<String>,
|
||||||
pub offset: Option<i64>,
|
pub offset: Option<i64>,
|
||||||
pub limit: Option<i64>,
|
pub limit: Option<i64>,
|
||||||
@@ -329,6 +330,7 @@ pub async fn get_images(
|
|||||||
let favorites_only = params.favorites_only.unwrap_or(false);
|
let favorites_only = params.favorites_only.unwrap_or(false);
|
||||||
let rating_min = params.rating_min.unwrap_or(0);
|
let rating_min = params.rating_min.unwrap_or(0);
|
||||||
let embedding_failed_only = params.embedding_failed_only.unwrap_or(false);
|
let embedding_failed_only = params.embedding_failed_only.unwrap_or(false);
|
||||||
|
let tagging_failed_only = params.tagging_failed_only.unwrap_or(false);
|
||||||
|
|
||||||
let total = db::count_images(
|
let total = db::count_images(
|
||||||
&conn,
|
&conn,
|
||||||
@@ -338,6 +340,7 @@ pub async fn get_images(
|
|||||||
favorites_only,
|
favorites_only,
|
||||||
rating_min,
|
rating_min,
|
||||||
embedding_failed_only,
|
embedding_failed_only,
|
||||||
|
tagging_failed_only,
|
||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
@@ -349,6 +352,7 @@ pub async fn get_images(
|
|||||||
favorites_only,
|
favorites_only,
|
||||||
rating_min,
|
rating_min,
|
||||||
embedding_failed_only,
|
embedding_failed_only,
|
||||||
|
tagging_failed_only,
|
||||||
sort,
|
sort,
|
||||||
offset,
|
offset,
|
||||||
limit,
|
limit,
|
||||||
@@ -594,6 +598,34 @@ pub async fn retry_failed_embeddings(
|
|||||||
db::retry_failed_embedding_jobs(&conn, params.folder_id).map_err(|e| e.to_string())
|
db::retry_failed_embedding_jobs(&conn, params.folder_id).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct FailedImageItem {
|
||||||
|
pub image_id: i64,
|
||||||
|
pub filename: String,
|
||||||
|
pub path: String,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_failed_tagging_images(
|
||||||
|
db: State<'_, DbState>,
|
||||||
|
folder_id: i64,
|
||||||
|
) -> Result<Vec<FailedImageItem>, String> {
|
||||||
|
let conn = db.get().map_err(|e| e.to_string())?;
|
||||||
|
db::get_failed_tagging_images(&conn, folder_id)
|
||||||
|
.map(|rows| {
|
||||||
|
rows.into_iter()
|
||||||
|
.map(|(image_id, filename, path, error)| FailedImageItem {
|
||||||
|
image_id,
|
||||||
|
filename,
|
||||||
|
path,
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn semantic_search_images(
|
pub async fn semantic_search_images(
|
||||||
db: State<'_, DbState>,
|
db: State<'_, DbState>,
|
||||||
|
|||||||
+39
-4
@@ -324,7 +324,10 @@ pub fn migrate(conn: &Connection) -> Result<()> {
|
|||||||
conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_images_taken_at ON images(taken_at);")?;
|
conn.execute_batch("CREATE INDEX IF NOT EXISTS idx_images_taken_at ON images(taken_at);")?;
|
||||||
ensure_column(conn, "folders", "scan_error", "TEXT")?;
|
ensure_column(conn, "folders", "scan_error", "TEXT")?;
|
||||||
ensure_column(conn, "folders", "sort_order", "INTEGER NOT NULL DEFAULT 0")?;
|
ensure_column(conn, "folders", "sort_order", "INTEGER NOT NULL DEFAULT 0")?;
|
||||||
conn.execute("UPDATE folders SET sort_order = id WHERE sort_order = 0", [])?;
|
conn.execute(
|
||||||
|
"UPDATE folders SET sort_order = id WHERE sort_order = 0",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
vector::migrate(conn)?;
|
vector::migrate(conn)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1629,6 +1632,7 @@ pub fn get_images(
|
|||||||
favorites_only: bool,
|
favorites_only: bool,
|
||||||
rating_min: i64,
|
rating_min: i64,
|
||||||
embedding_failed_only: bool,
|
embedding_failed_only: bool,
|
||||||
|
tagging_failed_only: bool,
|
||||||
sort: &str,
|
sort: &str,
|
||||||
offset: i64,
|
offset: i64,
|
||||||
limit: i64,
|
limit: i64,
|
||||||
@@ -1652,6 +1656,7 @@ pub fn get_images(
|
|||||||
let search_pattern = search.map(|value| format!("%{value}%"));
|
let search_pattern = search.map(|value| format!("%{value}%"));
|
||||||
let favorites_flag = i64::from(favorites_only);
|
let favorites_flag = i64::from(favorites_only);
|
||||||
let embedding_failed_flag = i64::from(embedding_failed_only);
|
let embedding_failed_flag = i64::from(embedding_failed_only);
|
||||||
|
let tagging_failed_flag = i64::from(tagging_failed_only);
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT id, folder_id, path, filename, thumbnail_path, width, height, file_size, created_at, modified_at, taken_at, mime_type,
|
"SELECT id, folder_id, path, filename, thumbnail_path, width, height, file_size, created_at, modified_at, taken_at, mime_type,
|
||||||
media_kind, duration_ms, video_codec, audio_codec, metadata_updated_at, metadata_error,
|
media_kind, duration_ms, video_codec, audio_codec, metadata_updated_at, metadata_error,
|
||||||
@@ -1665,8 +1670,9 @@ pub fn get_images(
|
|||||||
AND (?4 = 0 OR favorite = 1)
|
AND (?4 = 0 OR favorite = 1)
|
||||||
AND rating >= ?5
|
AND rating >= ?5
|
||||||
AND (?6 = 0 OR embedding_status = 'failed')
|
AND (?6 = 0 OR embedding_status = 'failed')
|
||||||
|
AND (?7 = 0 OR ai_tagger_error IS NOT NULL)
|
||||||
ORDER BY {order}
|
ORDER BY {order}
|
||||||
LIMIT ?7 OFFSET ?8"
|
LIMIT ?8 OFFSET ?9"
|
||||||
);
|
);
|
||||||
let mut stmt = conn.prepare(&sql)?;
|
let mut stmt = conn.prepare(&sql)?;
|
||||||
let rows = stmt.query_map(
|
let rows = stmt.query_map(
|
||||||
@@ -1677,6 +1683,7 @@ pub fn get_images(
|
|||||||
favorites_flag,
|
favorites_flag,
|
||||||
rating_min,
|
rating_min,
|
||||||
embedding_failed_flag,
|
embedding_failed_flag,
|
||||||
|
tagging_failed_flag,
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset
|
||||||
],
|
],
|
||||||
@@ -1693,11 +1700,13 @@ pub fn count_images(
|
|||||||
favorites_only: bool,
|
favorites_only: bool,
|
||||||
rating_min: i64,
|
rating_min: i64,
|
||||||
embedding_failed_only: bool,
|
embedding_failed_only: bool,
|
||||||
|
tagging_failed_only: bool,
|
||||||
) -> Result<i64> {
|
) -> Result<i64> {
|
||||||
let search_pattern = search.map(|value| format!("%{value}%"));
|
let search_pattern = search.map(|value| format!("%{value}%"));
|
||||||
|
|
||||||
let favorites_flag = i64::from(favorites_only);
|
let favorites_flag = i64::from(favorites_only);
|
||||||
let embedding_failed_flag = i64::from(embedding_failed_only);
|
let embedding_failed_flag = i64::from(embedding_failed_only);
|
||||||
|
let tagging_failed_flag = i64::from(tagging_failed_only);
|
||||||
let count = conn.query_row(
|
let count = conn.query_row(
|
||||||
"SELECT COUNT(*) FROM images
|
"SELECT COUNT(*) FROM images
|
||||||
WHERE (?1 IS NULL OR folder_id = ?1)
|
WHERE (?1 IS NULL OR folder_id = ?1)
|
||||||
@@ -1705,14 +1714,16 @@ pub fn count_images(
|
|||||||
AND (?3 IS NULL OR media_kind = ?3)
|
AND (?3 IS NULL OR media_kind = ?3)
|
||||||
AND (?4 = 0 OR favorite = 1)
|
AND (?4 = 0 OR favorite = 1)
|
||||||
AND rating >= ?5
|
AND rating >= ?5
|
||||||
AND (?6 = 0 OR embedding_status = 'failed')",
|
AND (?6 = 0 OR embedding_status = 'failed')
|
||||||
|
AND (?7 = 0 OR ai_tagger_error IS NOT NULL)",
|
||||||
params![
|
params![
|
||||||
folder_id,
|
folder_id,
|
||||||
search_pattern,
|
search_pattern,
|
||||||
media_kind,
|
media_kind,
|
||||||
favorites_flag,
|
favorites_flag,
|
||||||
rating_min,
|
rating_min,
|
||||||
embedding_failed_flag
|
embedding_failed_flag,
|
||||||
|
tagging_failed_flag
|
||||||
],
|
],
|
||||||
|row| row.get(0),
|
|row| row.get(0),
|
||||||
)?;
|
)?;
|
||||||
@@ -1947,6 +1958,30 @@ pub fn get_failed_embedding_images(
|
|||||||
Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
|
Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FailedTaggingRow = (i64, String, String, Option<String>);
|
||||||
|
|
||||||
|
pub fn get_failed_tagging_images(
|
||||||
|
conn: &Connection,
|
||||||
|
folder_id: i64,
|
||||||
|
) -> Result<Vec<FailedTaggingRow>> {
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT i.id, i.filename, i.path, COALESCE(j.last_error, i.ai_tagger_error)
|
||||||
|
FROM images i
|
||||||
|
LEFT JOIN tagging_jobs j ON j.image_id = i.id AND j.status = 'failed'
|
||||||
|
WHERE i.folder_id = ?1 AND i.ai_tagger_error IS NOT NULL
|
||||||
|
ORDER BY i.filename",
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map([folder_id], |row| {
|
||||||
|
Ok((
|
||||||
|
row.get::<_, i64>(0)?,
|
||||||
|
row.get::<_, String>(1)?,
|
||||||
|
row.get::<_, String>(2)?,
|
||||||
|
row.get::<_, Option<String>>(3)?,
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update_generated_caption(
|
pub fn update_generated_caption(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
image_id: i64,
|
image_id: i64,
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ pub fn run() {
|
|||||||
commands::get_explore_tags,
|
commands::get_explore_tags,
|
||||||
commands::get_images_by_ids,
|
commands::get_images_by_ids,
|
||||||
commands::get_failed_embedding_images,
|
commands::get_failed_embedding_images,
|
||||||
|
commands::get_failed_tagging_images,
|
||||||
commands::get_tagger_model_status,
|
commands::get_tagger_model_status,
|
||||||
commands::get_tagger_acceleration,
|
commands::get_tagger_acceleration,
|
||||||
commands::set_tagger_acceleration,
|
commands::set_tagger_acceleration,
|
||||||
|
|||||||
@@ -31,24 +31,52 @@ interface Task {
|
|||||||
snapshot: string;
|
snapshot: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FailedEmbeddingItem {
|
interface FailedWorkerItem {
|
||||||
image_id: number;
|
image_id: number;
|
||||||
filename: string;
|
filename: string;
|
||||||
path: string;
|
path: string;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FailedWorkerItemRow({ item }: { item: FailedWorkerItem }) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-w-0 items-start gap-1.5">
|
||||||
|
<svg className="mt-px h-2.5 w-2.5 shrink-0 text-amber-500 light-theme:text-amber-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5}
|
||||||
|
d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
||||||
|
</svg>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate text-[10px] font-medium text-amber-400/80 light-theme:text-amber-700">{item.filename}</p>
|
||||||
|
{item.error && (
|
||||||
|
<p className="truncate text-[9px] text-gray-600">{item.error}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="shrink-0 text-gray-700 transition-colors hover:text-gray-300 light-theme:text-gray-600 light-theme:hover:text-gray-100"
|
||||||
|
title="Reveal in Explorer"
|
||||||
|
onClick={() => void revealItemInDir(item.path)}
|
||||||
|
>
|
||||||
|
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function BackgroundTasks() {
|
export function BackgroundTasks() {
|
||||||
const folders = useGalleryStore((state) => state.folders);
|
const folders = useGalleryStore((state) => state.folders);
|
||||||
const indexingProgress = useGalleryStore((state) => state.indexingProgress);
|
const indexingProgress = useGalleryStore((state) => state.indexingProgress);
|
||||||
const mediaJobProgress = useGalleryStore((state) => state.mediaJobProgress);
|
const mediaJobProgress = useGalleryStore((state) => state.mediaJobProgress);
|
||||||
const retryFailedEmbeddings = useGalleryStore((state) => state.retryFailedEmbeddings);
|
const retryFailedEmbeddings = useGalleryStore((state) => state.retryFailedEmbeddings);
|
||||||
const queueTaggingJobs = useGalleryStore((state) => state.queueTaggingJobs);
|
const queueTaggingJobs = useGalleryStore((state) => state.queueTaggingJobs);
|
||||||
|
const showFailedTagging = useGalleryStore((state) => state.showFailedTagging);
|
||||||
const duplicateScanning = useGalleryStore((state) => state.duplicateScanning);
|
const duplicateScanning = useGalleryStore((state) => state.duplicateScanning);
|
||||||
const duplicateScanProgress = useGalleryStore((state) => state.duplicateScanProgress);
|
const duplicateScanProgress = useGalleryStore((state) => state.duplicateScanProgress);
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [dismissed, setDismissed] = useState<Record<number, string>>({});
|
const [dismissed, setDismissed] = useState<Record<number, string>>({});
|
||||||
const [failedItems, setFailedItems] = useState<Record<number, FailedEmbeddingItem[]>>({});
|
const [failedEmbeddingItems, setFailedEmbeddingItems] = useState<Record<number, FailedWorkerItem[]>>({});
|
||||||
|
const [failedTaggingItems, setFailedTaggingItems] = useState<Record<number, FailedWorkerItem[]>>({});
|
||||||
|
|
||||||
const workerPaused = useGalleryStore((state) => state.workerPaused);
|
const workerPaused = useGalleryStore((state) => state.workerPaused);
|
||||||
const loadWorkerStates = useGalleryStore((state) => state.loadWorkerStates);
|
const loadWorkerStates = useGalleryStore((state) => state.loadWorkerStates);
|
||||||
@@ -58,27 +86,43 @@ export function BackgroundTasks() {
|
|||||||
void loadWorkerStates();
|
void loadWorkerStates();
|
||||||
}, [folders, loadWorkerStates]);
|
}, [folders, loadWorkerStates]);
|
||||||
|
|
||||||
// Fetch failed embedding filenames whenever the expanded panel opens or failure counts change.
|
// Fetch failed filenames whenever the expanded panel opens or failure counts change.
|
||||||
const failedCounts = useMemo(
|
const failedEmbeddingCounts = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
Object.entries(mediaJobProgress).map(([id, p]) => [id, p?.embedding_failed ?? 0]),
|
Object.entries(mediaJobProgress).map(([id, p]) => [id, p?.embedding_failed ?? 0]),
|
||||||
),
|
),
|
||||||
[mediaJobProgress],
|
[mediaJobProgress],
|
||||||
);
|
);
|
||||||
|
const failedTaggingCounts = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(mediaJobProgress).map(([id, p]) => [id, p?.tagging_failed ?? 0]),
|
||||||
|
),
|
||||||
|
[mediaJobProgress],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!expanded) return;
|
if (!expanded) return;
|
||||||
for (const [folderId, count] of Object.entries(failedCounts)) {
|
for (const [folderId, count] of Object.entries(failedEmbeddingCounts)) {
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
invoke<FailedEmbeddingItem[]>("get_failed_embedding_images", {
|
invoke<FailedWorkerItem[]>("get_failed_embedding_images", {
|
||||||
folderId: Number(folderId),
|
folderId: Number(folderId),
|
||||||
})
|
})
|
||||||
.then((items) => setFailedItems((prev) => ({ ...prev, [folderId]: items })))
|
.then((items) => setFailedEmbeddingItems((prev) => ({ ...prev, [folderId]: items })))
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [expanded, failedCounts]);
|
for (const [folderId, count] of Object.entries(failedTaggingCounts)) {
|
||||||
|
if (count > 0) {
|
||||||
|
invoke<FailedWorkerItem[]>("get_failed_tagging_images", {
|
||||||
|
folderId: Number(folderId),
|
||||||
|
})
|
||||||
|
.then((items) => setFailedTaggingItems((prev) => ({ ...prev, [folderId]: items })))
|
||||||
|
.catch(() => undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [expanded, failedEmbeddingCounts, failedTaggingCounts]);
|
||||||
|
|
||||||
const isWorkerPaused = (folderId: number, worker: WorkerKey) => {
|
const isWorkerPaused = (folderId: number, worker: WorkerKey) => {
|
||||||
return workerPaused[folderId]?.[worker] ?? false;
|
return workerPaused[folderId]?.[worker] ?? false;
|
||||||
@@ -304,14 +348,14 @@ export function BackgroundTasks() {
|
|||||||
key={stage.label}
|
key={stage.label}
|
||||||
className={`flex items-center gap-1 rounded-md px-2 py-0.5 text-[11px] shrink-0 ${
|
className={`flex items-center gap-1 rounded-md px-2 py-0.5 text-[11px] shrink-0 ${
|
||||||
stage.failed
|
stage.failed
|
||||||
? "bg-amber-500/10 text-amber-400"
|
? "bg-amber-500/10 text-amber-400 light-theme:border light-theme:border-amber-500/40 light-theme:bg-amber-100 light-theme:text-amber-700"
|
||||||
: isPaused
|
: isPaused
|
||||||
? "bg-white/4 text-gray-600"
|
? "bg-white/4 text-gray-600"
|
||||||
: "bg-white/5 text-gray-400"
|
: "bg-white/5 text-gray-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span>{stage.label}</span>
|
<span>{stage.label}</span>
|
||||||
<span className={`tabular-nums ${stage.failed ? "text-amber-500" : isPaused ? "text-gray-700" : "text-gray-600"}`}>
|
<span className={`tabular-nums ${stage.failed ? "text-amber-500 light-theme:text-amber-700" : isPaused ? "text-gray-700" : "text-gray-600"}`}>
|
||||||
{stage.detail}
|
{stage.detail}
|
||||||
</span>
|
</span>
|
||||||
{workerKey && (
|
{workerKey && (
|
||||||
@@ -357,14 +401,27 @@ export function BackgroundTasks() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Retry (failed embeddings only) */}
|
{primary.pendingMediaWork === 0 && (primary.hasFailedEmbeddings || primary.hasFailedTagging) && (
|
||||||
{primary.hasFailedEmbeddings && primary.pendingMediaWork === 0 && (
|
<div className="flex shrink-0 items-center gap-1.5">
|
||||||
|
{primary.hasFailedTagging ? (
|
||||||
<button
|
<button
|
||||||
className="rounded-lg border border-amber-500/20 bg-amber-500/10 px-2.5 py-1 text-[11px] text-amber-300 hover:bg-amber-500/20 transition-colors shrink-0"
|
className="rounded-lg border border-amber-500/20 bg-amber-500/10 px-2.5 py-1 text-[11px] text-amber-300 transition-colors hover:bg-amber-500/20 light-theme:border-amber-500/50 light-theme:bg-amber-100 light-theme:text-amber-700 light-theme:hover:bg-amber-200"
|
||||||
onClick={(e) => { e.stopPropagation(); void retryFailedEmbeddings(primary.id); }}
|
onClick={(e) => { e.stopPropagation(); showFailedTagging(primary.id); }}
|
||||||
|
>
|
||||||
|
Locate
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
<button
|
||||||
|
className="rounded-lg border border-amber-500/20 bg-amber-500/10 px-2.5 py-1 text-[11px] text-amber-300 transition-colors hover:bg-amber-500/20 light-theme:border-amber-500/50 light-theme:bg-amber-100 light-theme:text-amber-700 light-theme:hover:bg-amber-200"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (primary.hasFailedEmbeddings) void retryFailedEmbeddings(primary.id);
|
||||||
|
if (primary.hasFailedTagging) void queueTaggingJobs(primary.id);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Expand chevron (only when multiple tasks) */}
|
{/* Expand chevron (only when multiple tasks) */}
|
||||||
@@ -416,7 +473,7 @@ export function BackgroundTasks() {
|
|||||||
key={stage.label}
|
key={stage.label}
|
||||||
className={`flex items-center gap-1 rounded-md px-2 py-0.5 text-[11px] shrink-0 ${
|
className={`flex items-center gap-1 rounded-md px-2 py-0.5 text-[11px] shrink-0 ${
|
||||||
stage.failed
|
stage.failed
|
||||||
? "bg-amber-500/10 text-amber-400"
|
? "bg-amber-500/10 text-amber-400 light-theme:border light-theme:border-amber-500/40 light-theme:bg-amber-100 light-theme:text-amber-700"
|
||||||
: isPaused
|
: isPaused
|
||||||
? "bg-white/4 text-gray-600"
|
? "bg-white/4 text-gray-600"
|
||||||
: "bg-white/5 text-gray-500"
|
: "bg-white/5 text-gray-500"
|
||||||
@@ -428,7 +485,7 @@ export function BackgroundTasks() {
|
|||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
<span>{stage.label}</span>
|
<span>{stage.label}</span>
|
||||||
<span className={`tabular-nums ${stage.failed ? "text-amber-500" : "text-gray-600"}`}>
|
<span className={`tabular-nums ${stage.failed ? "text-amber-500 light-theme:text-amber-700" : "text-gray-600"}`}>
|
||||||
{stage.detail}
|
{stage.detail}
|
||||||
</span>
|
</span>
|
||||||
{workerKey && (
|
{workerKey && (
|
||||||
@@ -467,8 +524,17 @@ export function BackgroundTasks() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{taskHasFailed && (
|
{taskHasFailed && (
|
||||||
|
<div className="flex shrink-0 items-center gap-1.5">
|
||||||
|
{task.hasFailedTagging ? (
|
||||||
<button
|
<button
|
||||||
className="rounded-lg border border-amber-500/20 bg-amber-500/10 px-2 py-0.5 text-[11px] text-amber-300 hover:bg-amber-500/20 transition-colors shrink-0"
|
className="rounded-lg border border-amber-500/20 bg-amber-500/10 px-2 py-0.5 text-[11px] text-amber-300 transition-colors hover:bg-amber-500/20 light-theme:border-amber-500/50 light-theme:bg-amber-100 light-theme:text-amber-700 light-theme:hover:bg-amber-200"
|
||||||
|
onClick={() => showFailedTagging(task.id)}
|
||||||
|
>
|
||||||
|
Locate
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
<button
|
||||||
|
className="rounded-lg border border-amber-500/20 bg-amber-500/10 px-2 py-0.5 text-[11px] text-amber-300 transition-colors hover:bg-amber-500/20 light-theme:border-amber-500/50 light-theme:bg-amber-100 light-theme:text-amber-700 light-theme:hover:bg-amber-200"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (task.hasFailedEmbeddings) void retryFailedEmbeddings(task.id);
|
if (task.hasFailedEmbeddings) void retryFailedEmbeddings(task.id);
|
||||||
if (task.hasFailedTagging) void queueTaggingJobs(task.id);
|
if (task.hasFailedTagging) void queueTaggingJobs(task.id);
|
||||||
@@ -476,6 +542,7 @@ export function BackgroundTasks() {
|
|||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{task.id >= 0 && (
|
{task.id >= 0 && (
|
||||||
@@ -497,31 +564,18 @@ export function BackgroundTasks() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Failed embedding file list */}
|
{/* Failed worker file lists */}
|
||||||
{taskHasFailed && failedItems[task.id] && failedItems[task.id].length > 0 && (
|
{taskHasFailed && failedEmbeddingItems[task.id] && failedEmbeddingItems[task.id].length > 0 && (
|
||||||
<div className="mt-2 pl-[calc(7rem+0.75rem)] space-y-0.5">
|
<div className="mt-2 pl-[calc(7rem+0.75rem)] space-y-0.5">
|
||||||
{failedItems[task.id].map((item) => (
|
{failedEmbeddingItems[task.id].map((item) => (
|
||||||
<div key={item.image_id} className="flex items-start gap-1.5 min-w-0">
|
<FailedWorkerItemRow key={item.image_id} item={item} />
|
||||||
<svg className="h-2.5 w-2.5 text-amber-500 shrink-0 mt-px" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
))}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5}
|
</div>
|
||||||
d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
|
|
||||||
</svg>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="text-[10px] text-amber-400/80 truncate font-medium">{item.filename}</p>
|
|
||||||
{item.error && (
|
|
||||||
<p className="text-[9px] text-gray-600 truncate">{item.error}</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
{taskHasFailed && failedTaggingItems[task.id] && failedTaggingItems[task.id].length > 0 && (
|
||||||
<button
|
<div className="mt-2 pl-[calc(7rem+0.75rem)] space-y-0.5">
|
||||||
className="shrink-0 text-gray-700 hover:text-gray-300 transition-colors"
|
{failedTaggingItems[task.id].map((item) => (
|
||||||
title="Reveal in Explorer"
|
<FailedWorkerItemRow key={item.image_id} item={item} />
|
||||||
onClick={() => void revealItemInDir(item.path)}
|
|
||||||
>
|
|
||||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export function TitleBar() {
|
|||||||
aria-label={`Update available — click to update to Phokus v${updateVersion}`}
|
aria-label={`Update available — click to update to Phokus v${updateVersion}`}
|
||||||
className="relative flex h-5 w-5 items-center justify-center rounded-md bg-white/8 overflow-hidden text-gray-300 transition-colors hover:bg-white/12"
|
className="relative flex h-5 w-5 items-center justify-center rounded-md bg-white/8 overflow-hidden text-gray-300 transition-colors hover:bg-white/12"
|
||||||
>
|
>
|
||||||
<span className="pointer-events-none absolute left-1/2 top-1/2 h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-amber-400/60 animate-ping" />
|
<span className="pointer-events-none absolute left-1/2 top-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-amber-400/60 animate-ping" />
|
||||||
<PhokusMark className="relative h-4 w-4" dotClassName="fill-amber-400" />
|
<PhokusMark className="relative h-4 w-4" dotClassName="fill-amber-400" />
|
||||||
</button>
|
</button>
|
||||||
{/* Custom tooltip — fades in ~100ms instead of the native ~500ms delay. */}
|
{/* Custom tooltip — fades in ~100ms instead of the native ~500ms delay. */}
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ export function Toolbar() {
|
|||||||
const setMinimumRating = useGalleryStore((state) => state.setMinimumRating);
|
const setMinimumRating = useGalleryStore((state) => state.setMinimumRating);
|
||||||
const failedEmbeddingsOnly = useGalleryStore((state) => state.failedEmbeddingsOnly);
|
const failedEmbeddingsOnly = useGalleryStore((state) => state.failedEmbeddingsOnly);
|
||||||
const setFailedEmbeddingsOnly = useGalleryStore((state) => state.setFailedEmbeddingsOnly);
|
const setFailedEmbeddingsOnly = useGalleryStore((state) => state.setFailedEmbeddingsOnly);
|
||||||
|
const failedTaggingOnly = useGalleryStore((state) => state.failedTaggingOnly);
|
||||||
|
const setFailedTaggingOnly = useGalleryStore((state) => state.setFailedTaggingOnly);
|
||||||
const similarScope = useGalleryStore((state) => state.similarScope);
|
const similarScope = useGalleryStore((state) => state.similarScope);
|
||||||
const setSimilarScope = useGalleryStore((state) => state.setSimilarScope);
|
const setSimilarScope = useGalleryStore((state) => state.setSimilarScope);
|
||||||
const mediaJobProgress = useGalleryStore((state) => state.mediaJobProgress);
|
const mediaJobProgress = useGalleryStore((state) => state.mediaJobProgress);
|
||||||
@@ -166,6 +168,7 @@ export function Toolbar() {
|
|||||||
const activeView = useGalleryStore((state) => state.activeView);
|
const activeView = useGalleryStore((state) => state.activeView);
|
||||||
|
|
||||||
const hasAnyFailedEmbeddings = Object.values(mediaJobProgress).some((p) => p.embedding_failed > 0);
|
const hasAnyFailedEmbeddings = Object.values(mediaJobProgress).some((p) => p.embedding_failed > 0);
|
||||||
|
const hasAnyFailedTagging = Object.values(mediaJobProgress).some((p) => p.tagging_failed > 0);
|
||||||
|
|
||||||
const [searchCommand, setSearchCommand] = useState<SearchCommand | null>(null);
|
const [searchCommand, setSearchCommand] = useState<SearchCommand | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState(search);
|
const [searchQuery, setSearchQuery] = useState(search);
|
||||||
@@ -441,12 +444,12 @@ export function Toolbar() {
|
|||||||
|
|
||||||
{/* Filter row */}
|
{/* Filter row */}
|
||||||
<div className="flex items-center gap-1 px-4 pb-1.5">
|
<div className="flex items-center gap-1 px-4 pb-1.5">
|
||||||
<FilterPill label="All" active={mediaFilter === "all" && !favoritesOnly && !failedEmbeddingsOnly && minimumRating === 0} onClick={() => { setMediaFilter("all"); setFavoritesOnly(false); setMinimumRating(0); setFailedEmbeddingsOnly(false); }} />
|
<FilterPill label="All" active={mediaFilter === "all" && !favoritesOnly && !failedEmbeddingsOnly && !failedTaggingOnly && minimumRating === 0} onClick={() => { setMediaFilter("all"); setFavoritesOnly(false); setMinimumRating(0); setFailedEmbeddingsOnly(false); setFailedTaggingOnly(false); }} />
|
||||||
<FilterPill label="Images" active={mediaFilter === "image" && !favoritesOnly && !failedEmbeddingsOnly} onClick={() => { setMediaFilter("image"); setFavoritesOnly(false); setFailedEmbeddingsOnly(false); }} />
|
<FilterPill label="Images" active={mediaFilter === "image" && !favoritesOnly && !failedEmbeddingsOnly && !failedTaggingOnly} onClick={() => { setMediaFilter("image"); setFavoritesOnly(false); setFailedEmbeddingsOnly(false); setFailedTaggingOnly(false); }} />
|
||||||
<FilterPill label="Videos" active={mediaFilter === "video" && !favoritesOnly && !failedEmbeddingsOnly} onClick={() => { setMediaFilter("video"); setFavoritesOnly(false); setFailedEmbeddingsOnly(false); }} />
|
<FilterPill label="Videos" active={mediaFilter === "video" && !favoritesOnly && !failedEmbeddingsOnly && !failedTaggingOnly} onClick={() => { setMediaFilter("video"); setFavoritesOnly(false); setFailedEmbeddingsOnly(false); setFailedTaggingOnly(false); }} />
|
||||||
<FilterPill label="Favorites" active={favoritesOnly} onClick={() => { setFavoritesOnly(!favoritesOnly); setFailedEmbeddingsOnly(false); }} />
|
<FilterPill label="Favorites" active={favoritesOnly} onClick={() => { setFavoritesOnly(!favoritesOnly); setFailedEmbeddingsOnly(false); setFailedTaggingOnly(false); }} />
|
||||||
<FilterPill label="Rated" active={minimumRating === 1} onClick={() => { setMinimumRating(minimumRating === 1 ? 0 : 1); setFailedEmbeddingsOnly(false); }} />
|
<FilterPill label="Rated" active={minimumRating === 1} onClick={() => { setMinimumRating(minimumRating === 1 ? 0 : 1); setFailedEmbeddingsOnly(false); setFailedTaggingOnly(false); }} />
|
||||||
<FilterPill label="4★+" active={minimumRating === 4} onClick={() => { setMinimumRating(minimumRating === 4 ? 0 : 4); setFailedEmbeddingsOnly(false); }} />
|
<FilterPill label="4★+" active={minimumRating === 4} onClick={() => { setMinimumRating(minimumRating === 4 ? 0 : 4); setFailedEmbeddingsOnly(false); setFailedTaggingOnly(false); }} />
|
||||||
<FilterPill label="Similar: Folder" active={similarScope === "current_folder"} onClick={() => setSimilarScope("current_folder")} />
|
<FilterPill label="Similar: Folder" active={similarScope === "current_folder"} onClick={() => setSimilarScope("current_folder")} />
|
||||||
<FilterPill label="Similar: All" active={similarScope === "all_media"} onClick={() => setSimilarScope("all_media")} />
|
<FilterPill label="Similar: All" active={similarScope === "all_media"} onClick={() => setSimilarScope("all_media")} />
|
||||||
{hasAnyFailedEmbeddings ? (
|
{hasAnyFailedEmbeddings ? (
|
||||||
@@ -457,6 +460,14 @@ export function Toolbar() {
|
|||||||
onClick={() => setFailedEmbeddingsOnly(!failedEmbeddingsOnly)}
|
onClick={() => setFailedEmbeddingsOnly(!failedEmbeddingsOnly)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{hasAnyFailedTagging ? (
|
||||||
|
<FilterPill
|
||||||
|
label="Failed Tags"
|
||||||
|
active={failedTaggingOnly}
|
||||||
|
variant="amber"
|
||||||
|
onClick={() => setFailedTaggingOnly(!failedTaggingOnly)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{isSimilarResults ? <span className="ml-2 text-[11px] text-gray-500">Current similar scope: {similarScope === "current_folder" ? "current folder" : "all media"}</span> : null}
|
{isSimilarResults ? <span className="ml-2 text-[11px] text-gray-500">Current similar scope: {similarScope === "current_folder" ? "current folder" : "all media"}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+41
-4
@@ -309,6 +309,7 @@ interface GalleryState {
|
|||||||
favoritesOnly: boolean;
|
favoritesOnly: boolean;
|
||||||
minimumRating: number;
|
minimumRating: number;
|
||||||
failedEmbeddingsOnly: boolean;
|
failedEmbeddingsOnly: boolean;
|
||||||
|
failedTaggingOnly: boolean;
|
||||||
zoomPreset: ZoomPreset;
|
zoomPreset: ZoomPreset;
|
||||||
selectedImage: ImageRecord | null;
|
selectedImage: ImageRecord | null;
|
||||||
collectionTitle: string | null;
|
collectionTitle: string | null;
|
||||||
@@ -403,6 +404,8 @@ interface GalleryState {
|
|||||||
setFavoritesOnly: (favoritesOnly: boolean) => void;
|
setFavoritesOnly: (favoritesOnly: boolean) => void;
|
||||||
setMinimumRating: (minimumRating: number) => void;
|
setMinimumRating: (minimumRating: number) => void;
|
||||||
setFailedEmbeddingsOnly: (failedEmbeddingsOnly: boolean) => void;
|
setFailedEmbeddingsOnly: (failedEmbeddingsOnly: boolean) => void;
|
||||||
|
setFailedTaggingOnly: (failedTaggingOnly: boolean) => void;
|
||||||
|
showFailedTagging: (folderId: number) => void;
|
||||||
setZoomPreset: (zoomPreset: ZoomPreset) => void;
|
setZoomPreset: (zoomPreset: ZoomPreset) => void;
|
||||||
openImage: (image: ImageRecord) => void;
|
openImage: (image: ImageRecord) => void;
|
||||||
closeImage: () => void;
|
closeImage: () => void;
|
||||||
@@ -593,6 +596,7 @@ function matchesFilters(
|
|||||||
favoritesOnly: boolean,
|
favoritesOnly: boolean,
|
||||||
minimumRating: number,
|
minimumRating: number,
|
||||||
failedEmbeddingsOnly: boolean,
|
failedEmbeddingsOnly: boolean,
|
||||||
|
failedTaggingOnly: boolean,
|
||||||
search: string,
|
search: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
const matchesFolder = selectedFolderId === null || image.folder_id === selectedFolderId;
|
const matchesFolder = selectedFolderId === null || image.folder_id === selectedFolderId;
|
||||||
@@ -600,7 +604,8 @@ function matchesFilters(
|
|||||||
const matchesFavorite = !favoritesOnly || image.favorite;
|
const matchesFavorite = !favoritesOnly || image.favorite;
|
||||||
const matchesRating = image.rating >= minimumRating;
|
const matchesRating = image.rating >= minimumRating;
|
||||||
const matchesFailedEmbedding = !failedEmbeddingsOnly || image.embedding_status === "failed";
|
const matchesFailedEmbedding = !failedEmbeddingsOnly || image.embedding_status === "failed";
|
||||||
return matchesFolder && matchesMedia && matchesFavorite && matchesRating && matchesFailedEmbedding && matchesSearch(image, search);
|
const matchesFailedTagging = !failedTaggingOnly || image.ai_tagger_error !== null;
|
||||||
|
return matchesFolder && matchesMedia && matchesFavorite && matchesRating && matchesFailedEmbedding && matchesFailedTagging && matchesSearch(image, search);
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareNullableNumber(a: number | null, b: number | null): number {
|
function compareNullableNumber(a: number | null, b: number | null): number {
|
||||||
@@ -710,6 +715,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
favoritesOnly: false,
|
favoritesOnly: false,
|
||||||
minimumRating: 0,
|
minimumRating: 0,
|
||||||
failedEmbeddingsOnly: false,
|
failedEmbeddingsOnly: false,
|
||||||
|
failedTaggingOnly: false,
|
||||||
zoomPreset: "comfortable",
|
zoomPreset: "comfortable",
|
||||||
selectedImage: null,
|
selectedImage: null,
|
||||||
collectionTitle: null,
|
collectionTitle: null,
|
||||||
@@ -879,7 +885,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
selectFolder: (folderId) => {
|
selectFolder: (folderId) => {
|
||||||
set({ selectedFolderId: folderId, images: [], loadedCount: 0, collectionTitle: null, similarSourceImageId: null, similarHasMore: false, activeView: "gallery", failedEmbeddingsOnly: false, imageLoadError: null });
|
set({ selectedFolderId: folderId, images: [], loadedCount: 0, collectionTitle: null, similarSourceImageId: null, similarHasMore: false, activeView: "gallery", failedEmbeddingsOnly: false, failedTaggingOnly: false, imageLoadError: null });
|
||||||
void get().loadImages(true);
|
void get().loadImages(true);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -912,7 +918,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
loadImages: async (reset = false) => {
|
loadImages: async (reset = false) => {
|
||||||
const { selectedFolderId, search, sort, loadedCount, mediaFilter, favoritesOnly, minimumRating, failedEmbeddingsOnly, activeView } = get();
|
const { selectedFolderId, search, sort, loadedCount, mediaFilter, favoritesOnly, minimumRating, failedEmbeddingsOnly, failedTaggingOnly, activeView } = get();
|
||||||
const parsedSearch = parseSearchValue(search);
|
const parsedSearch = parseSearchValue(search);
|
||||||
const requestToken = ++galleryRequestToken;
|
const requestToken = ++galleryRequestToken;
|
||||||
set({ loadingImages: true, imageLoadError: null });
|
set({ loadingImages: true, imageLoadError: null });
|
||||||
@@ -999,6 +1005,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
favorites_only: favoritesOnly,
|
favorites_only: favoritesOnly,
|
||||||
rating_min: minimumRating > 0 ? minimumRating : null,
|
rating_min: minimumRating > 0 ? minimumRating : null,
|
||||||
embedding_failed_only: failedEmbeddingsOnly,
|
embedding_failed_only: failedEmbeddingsOnly,
|
||||||
|
tagging_failed_only: failedTaggingOnly,
|
||||||
sort,
|
sort,
|
||||||
offset,
|
offset,
|
||||||
limit: activeView === "timeline" ? TIMELINE_PAGE_SIZE : PAGE_SIZE,
|
limit: activeView === "timeline" ? TIMELINE_PAGE_SIZE : PAGE_SIZE,
|
||||||
@@ -1108,7 +1115,35 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
setFailedEmbeddingsOnly: (failedEmbeddingsOnly) => {
|
setFailedEmbeddingsOnly: (failedEmbeddingsOnly) => {
|
||||||
set({ failedEmbeddingsOnly, images: [], loadedCount: 0, collectionTitle: null, similarSourceImageId: null, similarHasMore: false, imageLoadError: null });
|
set({ failedEmbeddingsOnly, failedTaggingOnly: failedEmbeddingsOnly ? false : get().failedTaggingOnly, images: [], loadedCount: 0, collectionTitle: null, similarSourceImageId: null, similarHasMore: false, imageLoadError: null });
|
||||||
|
void get().loadImages(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
setFailedTaggingOnly: (failedTaggingOnly) => {
|
||||||
|
set({ failedTaggingOnly, failedEmbeddingsOnly: failedTaggingOnly ? false : get().failedEmbeddingsOnly, images: [], loadedCount: 0, collectionTitle: null, similarSourceImageId: null, similarHasMore: false, imageLoadError: null });
|
||||||
|
void get().loadImages(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
showFailedTagging: (folderId) => {
|
||||||
|
set({
|
||||||
|
selectedFolderId: folderId,
|
||||||
|
activeView: "gallery",
|
||||||
|
search: "",
|
||||||
|
mediaFilter: "all",
|
||||||
|
favoritesOnly: false,
|
||||||
|
minimumRating: 0,
|
||||||
|
failedEmbeddingsOnly: false,
|
||||||
|
failedTaggingOnly: true,
|
||||||
|
images: [],
|
||||||
|
loadedCount: 0,
|
||||||
|
collectionTitle: null,
|
||||||
|
similarSourceImageId: null,
|
||||||
|
similarSourceFolderId: null,
|
||||||
|
similarHasMore: false,
|
||||||
|
similarFolderId: null,
|
||||||
|
similarCrop: null,
|
||||||
|
imageLoadError: null,
|
||||||
|
});
|
||||||
void get().loadImages(true);
|
void get().loadImages(true);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -2205,6 +2240,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
state.favoritesOnly,
|
state.favoritesOnly,
|
||||||
state.minimumRating,
|
state.minimumRating,
|
||||||
state.failedEmbeddingsOnly,
|
state.failedEmbeddingsOnly,
|
||||||
|
state.failedTaggingOnly,
|
||||||
state.search,
|
state.search,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2244,6 +2280,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|||||||
state.favoritesOnly,
|
state.favoritesOnly,
|
||||||
state.minimumRating,
|
state.minimumRating,
|
||||||
state.failedEmbeddingsOnly,
|
state.failedEmbeddingsOnly,
|
||||||
|
state.failedTaggingOnly,
|
||||||
state.search,
|
state.search,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user