import { useRef, useState } from "react"; import { PhokusMark } from "./components/PhokusMark"; // Screenshots are 2560×1600 masters; imagetools downscales + transcodes to // AVIF/WebP at build (see the Shot helper). `as=picture` must be the last query // param so the `*as=picture` module declaration matches. import heroShot from "../assets/screenshots/phokus_galleryHero.jpg?w=1600&format=avif;webp&quality=50&as=picture"; import semanticShot from "../assets/screenshots/phokus_semanticSearch.jpg?w=1600&format=avif;webp&quality=50&as=picture"; import exploreShot from "../assets/screenshots/phokus_exploreClusters.png?w=1600&format=avif;webp&quality=50&as=picture"; import timelineShot from "../assets/screenshots/phokus_timeline.jpg?w=1600&format=avif;webp&quality=50&as=picture"; import lightboxShot from "../assets/screenshots/phokus_lightboxWithTags.jpg?w=1600&format=avif;webp&quality=50&as=picture"; import videoShot from "../assets/screenshots/phokus_videoPlayer.jpg?w=1600&format=avif;webp&quality=50&as=picture"; import duplicatesShot from "../assets/screenshots/phokus_duplicates.png?w=1600&format=avif;webp&quality=50&as=picture"; const REPO_URL = "https://github.com/JezzWTF/phokus"; const DOWNLOAD_URL = "https://github.com/JezzWTF/phokus/releases/latest"; function GitHubIcon({ className }: { className?: string }) { return ( ); } // Renders an imagetools `?as=picture` import: AVIF + WebP sources with the WebP // fallback on the . `display:contents` so the layout/rounding lives on the // , exactly as a bare would. width/height come from the transcode to // reserve space and avoid layout shift. type PictureImport = { sources: Record; img: { src: string; w: number; h: number } }; function Shot({ pic, alt, className, ariaHidden, }: { pic: PictureImport; alt: string; className?: string; ariaHidden?: boolean; }) { return ( {Object.entries(pic.sources).map(([type, srcSet]) => ( ))} {alt} ); } function Header() { return (
Phokus Download
); } function MobileChapterNav() { return ( ); } function MobileChapter({ number, label, title, children, }: { number: string; label: string; title: string; children: React.ReactNode; }) { return (
{number} {label}

{title}

{children}
); } function Hero() { return (
Your folders. Your machine. Windows

See everything.
Find anything.

Search and curate the images and videos already on your computer. Everything stays local.

Local no uploads Private no account Offline after setup
{/* Product leads: the gallery dominates the right and bleeds off the frame, cropped so the workspace appears to continue beyond the viewport. */}

Local-first · Windows desktop

Your local media library,
made searchable.

Browse, understand, and curate large image and video folders — semantic search, visual discovery, and duplicate cleanup over the files already on your computer.

Windows 10/11 · Open source · On-device processing · No account

); } function LocalFirst() { const points = [ { title: "Your files stay put", body: "Phokus reads the folders you point it at. Media and everything it derives — thumbnails, embeddings, tags, ratings — never leave your machine.", }, { title: "On-device intelligence", body: "Semantic search and AI tagging run locally. The models download once on first use; after that, search works offline.", }, { title: "No account, no cloud", body: "No sign-up, no telemetry, no server. The only network traffic is the one-time model downloads and checking for updates.", }, ]; return (

A library that stays on your machine.

Phokus runs entirely on your computer. Nothing is uploaded, and there's nothing to sign into — your photos and everything Phokus learns about them stay with you.

{points.map((p) => (

{p.title}

{p.body}

))}
); } function SectionEyebrow({ label }: { label: string }) { return ( {label} ); } function SearchSection() { return ( ); } function MobileFeatureDeck() { const [activeCard, setActiveCard] = useState(0); const cardRefs = useRef>([]); function updateActiveCard(event: React.UIEvent) { const deckCenter = event.currentTarget.scrollLeft + event.currentTarget.clientWidth / 2; let nearestIndex = 0; let nearestDistance = Number.POSITIVE_INFINITY; cardRefs.current.forEach((card, index) => { if (!card) return; const cardCenter = card.offsetLeft + card.offsetWidth / 2; const distance = Math.abs(cardCenter - deckCenter); if (distance < nearestDistance) { nearestDistance = distance; nearestIndex = index; } }); setActiveCard(nearestIndex); } function showCard(index: number) { cardRefs.current[index]?.scrollIntoView({ behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth", block: "nearest", inline: "center", }); } return (

Swipe through the core workflows. Each one stays focused on the media, not app chrome.

{ cardRefs.current[0] = node; }} className="w-[78vw] max-w-[19rem] shrink-0 snap-center overflow-hidden rounded-2xl border border-edge bg-canvas" >
EXPLORE

See visual clusters.

Wander through related shots, then switch to a chronological timeline.

{ cardRefs.current[1] = node; }} className="w-[78vw] max-w-[19rem] shrink-0 snap-center overflow-hidden rounded-2xl border border-edge bg-canvas" >
CURATE

Review without leaving.

Rate, favourite, tag, zoom, play video, and find similar images in place.

{ cardRefs.current[2] = node; }} className="w-[78vw] max-w-[19rem] shrink-0 snap-center overflow-hidden rounded-2xl border border-edge bg-canvas" >
CLEANUP

Compare exact pairs.

Confirm byte-identical files and choose what to remove. Nothing is automatic.

{[0, 1, 2].map((index) => (
); } // Recurring aperture motif. One mark per section, vertically centered and bleeding // off the LEFT or RIGHT edge, sides alternating down the page — a clean zigzag that // never lets two marks collide at a section boundary (corner placement did). // // The iris is the full mark, but with blades reconstructed to meet the rim // *tangentially* — each blade is an arc internally tangent to the rim (radius 3.93, // center on the line out to the tip), so it kisses the circle and peels inward to // the opening instead of crossing it. The shipped logo's blades use radius 10 // (== rim), which reads fine at icon size but looks "broken" blown up huge; this is // the same mark, geometrically clean at any scale. const WM_BLADE = "M0,-4.18 A3.93,3.93 0 0 1 6.43,-7.66"; function EdgeMark({ side }: { side: "left" | "right" }) { const place = side === "left" ? "-left-[19rem]" : "-right-[19rem]"; return ( ); } function ExploreSection() { return (

See the shape of everything you've shot.

Explore groups your library into visual clusters — sets of similar shots you can open and wander, gathered by what they look like rather than where they're filed. A tag cloud surfaces recurring themes, and the Timeline walks everything by the date it was taken.

); } function CurateSection() { return (

Rate, tag, and dig in — without losing your place.

Open anything full-screen with zoom and pan, set ratings and favourites, edit tags, or search within an image for look-alikes. Video gets a proper player too — scrubbing, volume, speed, and fullscreen.

); } function CleanupSection() { return (

Find exact duplicates and reclaim the space.

A three-pass scan — size, then a sample hash, then a full hash — groups byte-identical files so you can review each set and bulk-delete with confidence. Nothing is removed without your say-so.

); } function TechFactsSection() { const facts = [ { k: "Platform", v: "Windows 10 / 11 (x64). The installer fetches the WebView2 runtime if it's missing." }, { k: "Media", v: "Common image formats plus video — thumbnails and metadata come from a bundled FFmpeg." }, { k: "On-device AI", v: "CLIP embeddings power semantic search (~580 MB); an optional WD tagger (~1.3 GB) adds tags." }, { k: "Inference", v: "CPU / DirectML in the standard build; an optional CUDA build accelerates NVIDIA GPUs." }, { k: "Storage", v: "A local SQLite database and thumbnail cache in your app-data folder. Nothing in the cloud." }, { k: "License & build", v: "MIT, open source. The installer is currently unsigned — see the note below." }, ]; return (
{facts.map((fact, index) => (
0{index + 1} {fact.k} +

{fact.v}

))}

What you're actually running.

{facts.map((f) => (
{f.k}
{f.v}
))}
); } function DownloadSection() { return (

Point Phokus at a folder.

Turn a folder full of files into a library you can actually explore.

Heads up: this build isn't code-signed yet, so Windows SmartScreen will warn that the publisher is unrecognised — choose{" "} More info → Run anyway. An NVIDIA CUDA build is on the releases page too.

); } function Footer() { return ( ); } export function App() { return (
); }