1 && selectedImage.media_kind === "image"
+ ? "cursor-grab"
+ : ""
}`}
onPointerDown={handleRegionPointerDown}
onPointerMove={handleRegionPointerMove}
@@ -386,9 +461,10 @@ export function Lightbox() {
src={convertFileSrc(selectedImage.path)}
alt={selectedImage.filename}
className="max-w-full rounded-2xl shadow-2xl"
+ draggable={false}
style={{
maxHeight: "calc(100vh - 10rem)",
- transform: `scale(${zoom})`,
+ transform: `translate(${view.panX}px, ${view.panY}px) scale(${zoom})`,
transformOrigin: "center center",
// Slightly dim the image while in region select mode
...(regionSelectMode ? { opacity: 0.85 } : {}),
@@ -399,14 +475,14 @@ export function Lightbox() {
{Math.round(zoom * 100)}%
diff --git a/src/components/PhokusMark.tsx b/src/components/PhokusMark.tsx
new file mode 100644
index 0000000..627267a
--- /dev/null
+++ b/src/components/PhokusMark.tsx
@@ -0,0 +1,39 @@
+// Phokus brand mark — a camera iris, rendered inline so it inherits `currentColor`
+// and scales to any size. Drive color + dimensions from the className, e.g.
+//
+// The full app-icon tile (filled iris on a dark rounded square) lives in
+// branding/phokus-aperture.svg and feeds `pnpm tauri icon`.
+//
+// Pass dotClassName (e.g. "fill-amber-400") to light up the central focal point —
+// used in the titlebar as the "update available" indicator.
+const BLADE = "M0,-4.18 A10,10 0 0 1 6.43,-7.66";
+
+export function PhokusMark({
+ className,
+ dotClassName,
+}: {
+ className?: string;
+ dotClassName?: string;
+}) {
+ return (
+
+ );
+}
diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx
index 14ec1da..b2779b9 100644
--- a/src/components/SettingsModal.tsx
+++ b/src/components/SettingsModal.tsx
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { CleanupOrphanedThumbnailsResult, DatabaseInfo, OrphanedThumbnailsInfo, TaggerAcceleration, TaggingQueueScope, VacuumResult, useGalleryStore } from "../store";
+import { FfmpegStatusRow } from "./onboarding/StepWelcome";
type SettingsSection = "workspace" | "general";
@@ -8,6 +9,12 @@ const SECTIONS: { id: SettingsSection; label: string; detail: string }[] = [
{ id: "general", label: "General", detail: "App data and diagnostics" },
];
+function formatBytesShort(bytes: number): string {
+ if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
+ if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
+ return `${(bytes / 1024).toFixed(0)} KB`;
+}
+
function StatusPill({ children, tone }: { children: React.ReactNode; tone: "ready" | "muted" | "busy" }) {
const className =
tone === "ready"
@@ -178,6 +185,13 @@ export function SettingsModal() {
const vacuumDatabase = useGalleryStore((state) => state.vacuumDatabase);
const getOrphanedThumbnailsInfo = useGalleryStore((state) => state.getOrphanedThumbnailsInfo);
const cleanupOrphanedThumbnails = useGalleryStore((state) => state.cleanupOrphanedThumbnails);
+ const appVersion = useGalleryStore((state) => state.appVersion);
+ const updateStatus = useGalleryStore((state) => state.updateStatus);
+ const updateVersion = useGalleryStore((state) => state.updateVersion);
+ const updateError = useGalleryStore((state) => state.updateError);
+ const checkForUpdates = useGalleryStore((state) => state.checkForUpdates);
+ const installUpdate = useGalleryStore((state) => state.installUpdate);
+ const openOnboarding = useGalleryStore((state) => state.openOnboarding);
useEffect(() => {
if (!settingsOpen) return;
@@ -227,16 +241,24 @@ export function SettingsModal() {
: "no folders selected";
const thresholdDisplay = taggerThresholdDraft ?? String(taggerThreshold);
const batchSizeDisplay = taggerBatchSizeDraft ?? String(taggerBatchSize);
- const taggerDownloadLabel = taggerModelProgress
- ? `Downloading ${taggerModelProgress.completed_files}/${taggerModelProgress.total_files}`
- : taggerModelPreparing
- ? "Preparing WD Tagger..."
- : taggerReady
- ? "Installed"
- : "Install model";
- const taggerDownloadPercent = taggerModelProgress
- ? Math.round((taggerModelProgress.completed_files / Math.max(taggerModelProgress.total_files, 1)) * 100)
- : 0;
+ const taggerBytesKnown =
+ taggerModelProgress?.downloaded_bytes != null &&
+ taggerModelProgress.total_bytes != null &&
+ taggerModelProgress.total_bytes > 0;
+ const taggerDownloadLabel = taggerBytesKnown
+ ? `Downloading ${formatBytesShort(taggerModelProgress!.downloaded_bytes!)} / ${formatBytesShort(taggerModelProgress!.total_bytes!)}`
+ : taggerModelProgress
+ ? `Downloading ${taggerModelProgress.completed_files}/${taggerModelProgress.total_files}`
+ : taggerModelPreparing
+ ? "Preparing WD Tagger..."
+ : taggerReady
+ ? "Installed"
+ : "Install model";
+ const taggerDownloadPercent = taggerBytesKnown
+ ? Math.round((taggerModelProgress!.downloaded_bytes! / taggerModelProgress!.total_bytes!) * 100)
+ : taggerModelProgress
+ ? Math.round((taggerModelProgress.completed_files / Math.max(taggerModelProgress.total_files, 1)) * 100)
+ : 0;
const runQueueAction = (action: "queue" | "clear") => {
const selectedIds = taggingQueueFolderIds;
@@ -578,6 +600,65 @@ export function SettingsModal() {