feat: add Phokus UI Lab

Add a dev-only Vite UI mode with Tauri API mocks, in-memory fixture scenarios, reusable media source handling, and documentation for browser-based visual testing.
This commit is contained in:
2026-06-29 00:04:48 +01:00
parent cebd709391
commit 7a18011b0f
16 changed files with 1006 additions and 20 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
import { useRef, useState } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import { convertFileSrc } from "@tauri-apps/api/core";
import { DuplicateGroup, useGalleryStore } from "../store";
import { FolderScopeDropdown } from "./FolderScopeDropdown";
import { mediaSrc } from "../lib/mediaSrc";
function formatBytes(bytes: number): string {
if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`;
@@ -68,7 +68,7 @@ function DuplicateGroupCard({ group }: { group: DuplicateGroup }) {
<div className="flex flex-wrap gap-3">
{group.images.map((image) => {
const isSelected = selectedIds.has(image.id);
const src = image.thumbnail_path ? convertFileSrc(image.thumbnail_path) : null;
const src = mediaSrc(image.thumbnail_path);
return (
<button
key={image.id}
+2 -2
View File
@@ -1,9 +1,9 @@
import { useEffect, useLayoutEffect, useRef, useCallback, useMemo, useState } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import { convertFileSrc } from "@tauri-apps/api/core";
import { ImageRecord, parseSearchValue, tileSizeForZoom, useGalleryStore } from "../store";
import { BulkActionBar } from "./BulkActionBar";
import { Tooltip } from "./Tooltip";
import { mediaSrc } from "../lib/mediaSrc";
const GAP = 6;
@@ -123,7 +123,7 @@ export function ImageTile({
const toggleGallerySelected = useGalleryStore((state) => state.toggleGallerySelected);
const canFindSimilar = image.embedding_status === "ready";
const src = image.thumbnail_path ? convertFileSrc(image.thumbnail_path) : null;
const src = mediaSrc(image.thumbnail_path);
return (
<Tooltip label={image.filename} delay={500} block followCursor>
+4 -3
View File
@@ -1,9 +1,10 @@
import { useEffect, useCallback, useRef, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { convertFileSrc, invoke } from "@tauri-apps/api/core";
import { invoke } from "@tauri-apps/api/core";
import { revealItemInDir } from "@tauri-apps/plugin-opener";
import { useGalleryStore, ImageTag, ImageExif, AiRating } from "../store";
import { VideoPlayer } from "./VideoPlayer";
import { mediaSrc } from "../lib/mediaSrc";
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
@@ -475,12 +476,12 @@ export function Lightbox() {
transition={{ duration: 0.12 }}
>
{selectedImage.media_kind === "video" ? (
<VideoPlayer src={convertFileSrc(selectedImage.path)} />
<VideoPlayer src={mediaSrc(selectedImage.path) ?? ""} />
) : (
<>
<img
ref={imgRef}
src={convertFileSrc(selectedImage.path)}
src={mediaSrc(selectedImage.path) ?? ""}
alt={selectedImage.filename}
className="max-w-full rounded-2xl shadow-2xl"
draggable={false}
+2 -2
View File
@@ -1,9 +1,9 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { Reorder, useDragControls } from "framer-motion";
import { open } from "@tauri-apps/plugin-dialog";
import { convertFileSrc } from "@tauri-apps/api/core";
import { useGalleryStore, Folder, Album, IndexProgress } from "../store";
import { ThemedDropdown } from "./ThemedDropdown";
import { mediaSrc } from "../lib/mediaSrc";
interface ContextMenuState {
folderId: number;
@@ -445,7 +445,7 @@ function AlbumItem({
setRenaming(false);
};
const cover = album.cover_thumbnail_path ? convertFileSrc(album.cover_thumbnail_path) : null;
const cover = mediaSrc(album.cover_thumbnail_path);
const row = (
<div
+2 -2
View File
@@ -1,9 +1,9 @@
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { motion, useReducedMotion } from "framer-motion";
import { convertFileSrc } from "@tauri-apps/api/core";
import { ExploreTagEntry, TagCloudEntry, useGalleryStore } from "../store";
import { FolderScopeDropdown } from "./FolderScopeDropdown";
import { Tooltip } from "./Tooltip";
import { mediaSrc } from "../lib/mediaSrc";
const ACCENTS = [
"#60a5fa",
@@ -148,7 +148,7 @@ function buildCloud(entries: TagCloudEntry[], containerW: number, containerH: nu
}
function CloudCard({ node, onOpen, animated }: { node: PlacedNode; onOpen: (imageIds: number[]) => void; animated: boolean }) {
const src = node.entry.thumbnail_path ? convertFileSrc(node.entry.thumbnail_path) : null;
const src = mediaSrc(node.entry.thumbnail_path);
const { w, h, accent } = node;
const driftTransition = {
duration: node.driftDuration,