feat(folder-picker): add editable address navigation
github/actions/ci GitHub Actions CI finished: success
github/actions/ci GitHub Actions CI finished: success
Add a File Explorer-style dual-mode path bar to the folder picker so users can navigate with breadcrumbs or switch into an editable address field for pasted paths. Also allow staging the current or typed path directly and show friendlier feedback for missing folders.
This commit is contained in:
+99
-144
@@ -9,157 +9,112 @@ aims to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **Quick theme switch** — right-click the settings cog in the title bar to
|
|
||||||
switch theme (Phokus / Subtle Light / Conventional Dark) on the spot, without
|
|
||||||
opening Settings.
|
|
||||||
- **Albums** — curate your own collections. A new Albums section in the sidebar
|
|
||||||
(with cover thumbnails, kept visually distinct from Libraries) lets you create,
|
|
||||||
rename, reorder (drag the row), and open albums; albums can span multiple
|
|
||||||
folders. Add images from the gallery's bulk action bar or from the lightbox,
|
|
||||||
remove them from within an album, and use Manage mode to multi-select and
|
|
||||||
delete albums in one go. Deleting an album never touches your files — only the
|
|
||||||
grouping is removed.
|
|
||||||
- **Multi-select & bulk actions in the gallery** — hover a thumbnail's top-left
|
|
||||||
corner to reveal a selection checkbox (click it to start selecting); while
|
|
||||||
selecting, click tiles to toggle and double-click to open. A floating action
|
|
||||||
bar then lets you tag (with autocomplete), rate, favorite, add to an album, or
|
|
||||||
delete the whole selection at once. Works in similar-image, region, and album
|
|
||||||
views too.
|
|
||||||
- **Colour search** — filter the gallery (and Timeline) by dominant colour via a
|
|
||||||
collapsible swatch palette plus a custom colour picker in the toolbar; existing
|
|
||||||
libraries are backfilled automatically in the background.
|
|
||||||
- **Album-scoped similar search** — when finding visually similar images or
|
|
||||||
searching by a selected region from an album, you can now keep results scoped
|
|
||||||
to that album, switch back to the source folder, or search all media.
|
|
||||||
- **Tag management** — Explore → Tag Cloud gains a Manage mode with a flat tag
|
|
||||||
list where you can rename a tag, merge it into another (rename it to an
|
|
||||||
existing tag's name), or delete it from every image. Changes apply across the
|
|
||||||
whole library. Settings → AI Workspace also has an "Open tag manager" button
|
|
||||||
that jumps straight into it.
|
|
||||||
- **Camera info in the lightbox** — the image info panel now shows EXIF details
|
|
||||||
(camera, lens, aperture, shutter, ISO, focal length) and, when a photo is
|
|
||||||
geotagged, its GPS coordinates as a link that opens the location in your
|
|
||||||
browser. Read on demand from the file, so it works on already-indexed images
|
|
||||||
without re-indexing.
|
|
||||||
- **What's New** — after updating, Phokus now greets you with a "What's new"
|
- **What's New** — after updating, Phokus now greets you with a "What's new"
|
||||||
toast that opens an in-app release-notes screen for the new version, with the
|
toast that opens a tidy in-app tour of the new version. Added, Changed, and
|
||||||
changes grouped into collapsible Added / Changed / Fixed sections.
|
Fixed notes are grouped into collapsible sections, so you can skim the good
|
||||||
- **Build badge in Settings** — the version line in Settings → Updates now shows
|
bits without playing "spot the difference".
|
||||||
whether the running build is the CPU or CUDA (GPU-accelerated) variant.
|
- **Quick theme switch** — right-click the settings cog in the title bar to
|
||||||
- **Choose your tagging model** — Settings → AI Workspace now lets you switch the
|
swap between Phokus, Subtle Light, and Conventional Dark instantly. No Settings
|
||||||
AI tagger between the WD tagger (anime-focused) and **JoyTag**, which also
|
detour required.
|
||||||
handles photographic content and is stronger on NSFW concepts — a better fit
|
- **Albums** — make your own cross-folder collections without moving a single
|
||||||
for real-photo libraries. Each model downloads on demand, and tags are
|
file. Albums live in their own sidebar section with cover thumbnails, can be
|
||||||
attributed to the model that produced them. JoyTag has no built-in rating, so
|
created, renamed, reordered, opened, and cleaned up in Manage mode, and deleting
|
||||||
its explicitness rating is derived from its tags.
|
one only removes the grouping.
|
||||||
- **Related tags in Explore** — clicking a tag in the Tag Cloud atlas now reveals
|
- **Gallery multi-select** — hover a thumbnail's top-left corner to start
|
||||||
its most co-occurring tags with animated connection lines and per-tag image
|
selecting, then use the floating action bar to tag, rate, favorite, add to an
|
||||||
counts, so you can quickly see how tags cluster together and jump to a refined
|
album, or delete a whole batch at once. It also works in similar-image, region,
|
||||||
search.
|
and album views, because bulk work should not disappear the moment you need it.
|
||||||
- **Persist worker pauses across restarts** — a new toggle in Settings → General
|
- **Colour search** — narrow the Gallery, Timeline, or tag results by dominant
|
||||||
lets you save per-folder worker pause states so they survive an app restart;
|
colour using toolbar swatches or a custom picker. Great for those "I know it
|
||||||
useful when you want a folder permanently excluded from background processing
|
was mostly blue" moments.
|
||||||
without having to re-pause it every launch.
|
- **Album-aware similar search** — similar-image and region searches started from
|
||||||
|
an album can now stay inside that album, jump back to the source folder, or
|
||||||
|
search everything.
|
||||||
|
- **Tag manager** — Explore's Tag Cloud now has a Manage mode for renaming,
|
||||||
|
merging, and deleting tags across the whole library. There is also an "Open tag
|
||||||
|
manager" shortcut in Settings -> AI Workspace when you want to get straight to
|
||||||
|
the cleanup.
|
||||||
|
- **Camera info in the lightbox** — the info panel now shows EXIF details like
|
||||||
|
camera, lens, aperture, shutter speed, ISO, and focal length. Geotagged photos
|
||||||
|
also get a browser link for their GPS coordinates, and already-indexed images
|
||||||
|
do not need a re-index.
|
||||||
|
- **Build badge in Settings** — Settings -> Updates now shows whether you are
|
||||||
|
running the CPU build or the CUDA build.
|
||||||
|
- **Choose your tagging model** — Settings -> AI Workspace now lets you pick
|
||||||
|
between the anime-focused WD tagger and JoyTag, which is better suited to photo
|
||||||
|
libraries and stronger on NSFW concepts (if that's your thing, we don't judge).
|
||||||
|
Models download only when needed, and tags remember which model created them.
|
||||||
|
- **Related tags in Explore** — Hover over a tag in the Tag Cloud to see the tags
|
||||||
|
that most often appear with it, complete with connection lines and image counts.
|
||||||
|
Handy for finding little clusters you did not know were there.
|
||||||
|
- **Pause workers for longer** — Settings -> General can now remember per-folder
|
||||||
|
worker pauses across app restarts, useful for folders you want to keep in the
|
||||||
|
library but leave out of background processing for now.
|
||||||
|
- **Editable folder path** — the folder picker now has an address bar, so you can
|
||||||
|
paste a path directly while still using breadcrumbs for quick jumps.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- **Two-column lightbox details** — the image info panel now lays metadata out in
|
- **Neater lightbox details** — image and video metadata now sits in two columns,
|
||||||
two columns (Dimensions/Duration, Video codec/Audio codec, Type/File size sit
|
so the info panel shows more at a glance with less scrolling.
|
||||||
side by side), so the panel is more compact and less scrolling.
|
- **Faster Explore revisits** — returning to a folder's visual clusters should
|
||||||
- **Faster Explore on large libraries** — revisiting a folder's visual clusters
|
feel much faster now, even in big libraries.
|
||||||
is now near-instant. The cluster cache is validated from a lightweight
|
- **Faster first-time clustering** — large libraries build their first visual
|
||||||
image-ID signature instead of re-reading every embedding, so big libraries no
|
clusters much more quickly, while still keeping the groups nicely balanced.
|
||||||
longer stall for several seconds even when the cached result is reused.
|
- **Better tag browsing** — the Tag manager now has live search, sorting
|
||||||
- **Faster first-time visual clustering** — computing clusters for a large
|
(most-used, least-used, A-Z, and Z-A), smooth scrolling for huge tag lists, and
|
||||||
library is now much quicker: centroids are found on a representative sample and
|
it keeps your filter/sort in place while you edit.
|
||||||
every image is then assigned across all CPU cores, instead of iterating over
|
- **Safer deletion** — deleting media now asks for confirmation and clearly says
|
||||||
the whole library single-threaded. Density-aware seeding keeps the groupings
|
the file is being removed from disk. This covers gallery bulk delete and the
|
||||||
balanced, so no single cluster swallows a huge share of images.
|
Duplicate Finder.
|
||||||
- **Tag manager search and sort** — the Manage mode tag list now has a live
|
- **Clearer update progress** — Settings -> Updates now shows a real download
|
||||||
filter input and a sort dropdown (most-used / least-used / A–Z / Z–A). The
|
progress bar with a percentage instead of the old lonely "Downloading" label.
|
||||||
list is virtualised so libraries with thousands of tags scroll without lag,
|
- **Better narrow-window layout** — the toolbar, filters, search box, colour
|
||||||
and renaming or deleting a tag no longer clears the current filter/sort state
|
picker, sidebar, and lightbox info panel now adapt more gracefully when the
|
||||||
mid-edit.
|
window is short on space.
|
||||||
|
- **Tidier Explore clusters** — busier clusters get more room, dense groups
|
||||||
- **Safer deletion** — deleting media now asks for confirmation and spells out
|
overlap less, and everything should stay easier to read and click.
|
||||||
that it permanently removes the file(s) from disk. This covers the new gallery
|
- **Faster CPU tagging** — CPU-only AI tagging can now use multiple cores while
|
||||||
bulk delete and the Duplicate Finder, which previously deleted on a single
|
leaving some breathing room for the rest of the app. GPU tagging is unchanged.
|
||||||
click with no confirmation or warning.
|
- **Smoother tooltips** — Phokus now uses its custom tooltip style across more of
|
||||||
- The updater now shows a real download progress bar with a percentage in
|
the app instead of falling back to the native browser tooltip.
|
||||||
Settings → Updates (previously it only said "Downloading").
|
|
||||||
- **Smaller-screen layout** — the toolbar adapts on narrow windows: the filter
|
|
||||||
chips scroll horizontally instead of squashing (so "Similar: Folder/All" no
|
|
||||||
longer stack), the search box shrinks, and the colour-search swatches now open
|
|
||||||
in a compact popover rather than expanding inline and running off-screen. The
|
|
||||||
sidebar and the lightbox info panel also narrow to give the gallery more room.
|
|
||||||
- **Explore cluster layout** — clusters are now sized by image count (busier
|
|
||||||
clusters are larger and stack on top) and repositioned to avoid overlapping, so
|
|
||||||
every cluster stays viewable and clickable even in dense libraries.
|
|
||||||
- **Faster CPU tagging** — when AI tagging runs on the CPU provider (no usable
|
|
||||||
GPU) it now uses multiple cores instead of being pinned to one, several times
|
|
||||||
faster on multi-core machines. A couple of cores are left free so the rest of
|
|
||||||
the app stays responsive. GPU (DirectML) tagging is unchanged.
|
|
||||||
- **Goodbye Native Tooltips** — Hello modern tooltip! Replaced the boring, native
|
|
||||||
tooltips for the custom tooltip across the board
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- **Stale Explore results on folder switch** — switching folders (or re-entering
|
- **Explore no longer flashes the last folder** — switching folders now clears
|
||||||
Explore) no longer briefly shows the previous folder's clusters/tags with no
|
the old clusters/tags and shows a loading state while the new folder catches
|
||||||
loading indicator; the view now clears and shows a loading state until the new
|
up.
|
||||||
folder's data arrives.
|
- **Ratings keep your search order** — rating or favoriting an image no longer
|
||||||
- **Rating no longer scrambles search results** — rating or favoriting an image
|
reshuffles similar-image, region, semantic, tag, or album results.
|
||||||
while viewing similar-image, region, semantic, tag, or album results no longer
|
- **Update progress comes back when you need it** — if you dismiss the update
|
||||||
re-sorts the view back into the default order; the current result ordering is
|
prompt and later start the update from the title bar or Settings, the progress
|
||||||
preserved.
|
toast now reappears instead of hiding away in Settings.
|
||||||
- The update download/install progress toast now reappears when you start an
|
- **Subtle Light cleanup** — fixed dark or hard-to-read surfaces, hover states,
|
||||||
update from the title-bar indicator or Settings after dismissing the earlier
|
dialogs, updater buttons, onboarding controls, and green action buttons in the
|
||||||
"Update available" prompt — previously progress only showed in Settings.
|
light theme.
|
||||||
- Subtle Light theme — fixed several surfaces and buttons that stayed dark or
|
- **No more self-indexing loops** — if you add a broad folder like your user
|
||||||
became unreadable on hover, including new dialogs and the green action buttons
|
profile, Phokus now skips its own app-data directory instead of indexing its
|
||||||
across the updater and onboarding.
|
thumbnail cache forever.
|
||||||
- **Endless self-indexing loop** — adding a folder that contains Phokus's own
|
- **Background tasks show the active work first** — when one folder is paused and
|
||||||
app-data directory (for example, your whole user profile) no longer makes the
|
another is processing, the active folder gets the main spot in the background
|
||||||
app index its own thumbnail cache, generate thumbnails of those thumbnails, and
|
tasks bar.
|
||||||
loop forever. That directory is now skipped by both the folder scanner and the
|
- **First launch fits smaller screens** — fresh installs now clamp the window to
|
||||||
filesystem watcher.
|
the usable monitor area, so 1366x768-style displays do not lose part of the app
|
||||||
- **Background tasks now lead with the active folder** — when one folder's
|
below the taskbar.
|
||||||
workers are paused while another is actively processing, the active folder
|
- **Explore is clearer in Subtle Light** — cluster captions, buttons, cloud
|
||||||
takes the main slot in the background-tasks bar instead of the paused one.
|
words, hover glows, and the new connection lines now use stronger light-theme
|
||||||
- **Oversized window on smaller screens** — on a fresh install the window opened
|
colours.
|
||||||
at a fixed size that was taller than the usable area on displays like
|
- **Explore got a few sharp edges sanded down** — Cluster Cloud uses the in-app
|
||||||
1366×768, leaving part of it off-screen. It now clamps to the monitor's work
|
tooltip, singular counts now say "1 image", and the folder-scope dropdown no
|
||||||
area (excluding the taskbar) and centres there when it would overflow; larger
|
longer hides behind cluster cards.
|
||||||
displays and any remembered size/position are left untouched.
|
- **AI tagging stays responsive** — GPU tagging now works in smaller bursts with
|
||||||
- **Explore readability in Subtle Light** — fixed washed-out and unreadable
|
brief pauses between them, so the UI keeps moving and the first batch starts
|
||||||
elements in the Explore view on the light theme. Cluster cards now use a soft
|
sooner.
|
||||||
dark caption scrim (the previous cream overlay fogged the photos), with a
|
- **Noisy AI tags get cleaned up** — generic low-signal tags from WD and JoyTag
|
||||||
legible label, count, and "Open" button; Cluster Cloud words use darker accents and
|
are filtered before they are saved, and matching older generated tags are
|
||||||
a higher opacity floor instead of near-invisible pale text.
|
cleaned up on startup. Your manually-added tags are left alone.
|
||||||
- **Explore polish** — the Cluster Cloud now uses the in-app tooltip instead of the
|
- **Selected Folders starts empty** — choosing "Selected Folders" for AI tagging
|
||||||
native browser one (and reads "1 image", not "1 images"), and the folder-scope
|
no longer pre-selects the first folder. You decide exactly what gets queued.
|
||||||
dropdown no longer opens behind the cluster cards.
|
|
||||||
- **AI tagging no longer freezes the app** — tagging now runs inference in small
|
|
||||||
GPU micro-batches with a brief yield between them, instead of one wide batch
|
|
||||||
that monopolised the GPU (and with it the whole UI) for seconds at a time. The
|
|
||||||
app stays responsive while tagging runs, throughput is steadier (the old wide
|
|
||||||
batches caused periodic slowdowns), and the first batch after launch starts
|
|
||||||
faster.
|
|
||||||
- **Noisy AI tags filtered automatically** — a built-in removal list strips
|
|
||||||
generic or low-signal tags (e.g. "simple background") from WD and JoyTag
|
|
||||||
output before they are stored. Previously-generated tags matching the list are
|
|
||||||
cleaned up at startup. Manual user-added tags are never touched.
|
|
||||||
- **Explore Tag Cloud hover glow in Subtle Light** — the radial glow that
|
|
||||||
appears under a tag word on hover was invisible on the light background
|
|
||||||
(white on cream). It now uses a warm dark tone matching the rest of the
|
|
||||||
light-theme palette, and the atlas connection-line gradient adapts to the
|
|
||||||
theme as well.
|
|
||||||
- **AI Workspace "Selected Folders" no longer pre-selects a folder** — switching
|
|
||||||
the tagging queue scope to "Selected Folders" previously auto-selected the
|
|
||||||
first folder in the list. It now starts with nothing selected so you can
|
|
||||||
choose exactly which folders to target.
|
|
||||||
- **Colour filtering** — Colour filtering now works within tag search results,
|
|
||||||
you can search/select a tag and then narrow that result set by colour.
|
|
||||||
|
|
||||||
## [0.1.1] — 2026-06-23
|
## [0.1.1] — 2026-06-23
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,26 @@ function normalizePath(path: string): string {
|
|||||||
return path.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
return path.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanAddressInput(path: string): string {
|
||||||
|
const trimmed = path.trim();
|
||||||
|
if (trimmed.length >= 2) {
|
||||||
|
const first = trimmed[0];
|
||||||
|
const last = trimmed[trimmed.length - 1];
|
||||||
|
if ((first === "\"" && last === "\"") || (first === "'" && last === "'")) {
|
||||||
|
return trimmed.slice(1, -1).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function friendlyDirectoryError(error: unknown): string {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
if (/cannot find the path|os error 3|not found|no such file/i.test(message)) {
|
||||||
|
return "Folder not found. Check the path and try again.";
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
function folderName(path: string): string {
|
function folderName(path: string): string {
|
||||||
const trimmed = path.replace(/[\\/]+$/, "");
|
const trimmed = path.replace(/[\\/]+$/, "");
|
||||||
const parts = trimmed.split(/[\\/]+/).filter(Boolean);
|
const parts = trimmed.split(/[\\/]+/).filter(Boolean);
|
||||||
@@ -207,12 +227,15 @@ export function FolderPickerModal() {
|
|||||||
|
|
||||||
const [listing, setListing] = useState<DirListing | null>(null);
|
const [listing, setListing] = useState<DirListing | null>(null);
|
||||||
const [currentPath, setCurrentPath] = useState<string | null>(null);
|
const [currentPath, setCurrentPath] = useState<string | null>(null);
|
||||||
|
const [addressDraft, setAddressDraft] = useState("");
|
||||||
|
const [addressEditing, setAddressEditing] = useState(false);
|
||||||
const [stagedPaths, setStagedPaths] = useState<string[]>([]);
|
const [stagedPaths, setStagedPaths] = useState<string[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [adding, setAdding] = useState(false);
|
const [adding, setAdding] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [results, setResults] = useState<FolderAddResult[] | null>(null);
|
const [results, setResults] = useState<FolderAddResult[] | null>(null);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const addressInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const libraryPaths = useMemo(() => new Set(folders.map((folder) => normalizePath(folder.path))), [folders]);
|
const libraryPaths = useMemo(() => new Set(folders.map((folder) => normalizePath(folder.path))), [folders]);
|
||||||
const stagedSet = useMemo(() => new Set(stagedPaths.map(normalizePath)), [stagedPaths]);
|
const stagedSet = useMemo(() => new Set(stagedPaths.map(normalizePath)), [stagedPaths]);
|
||||||
@@ -234,12 +257,14 @@ export function FolderPickerModal() {
|
|||||||
.then((nextListing) => {
|
.then((nextListing) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setListing(nextListing);
|
setListing(nextListing);
|
||||||
|
setAddressDraft(nextListing.current ?? "");
|
||||||
|
setAddressEditing(false);
|
||||||
scrollRef.current?.scrollTo({ top: 0, left: 0 });
|
scrollRef.current?.scrollTo({ top: 0, left: 0 });
|
||||||
})
|
})
|
||||||
.catch((loadError) => {
|
.catch((loadError) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setListing({ current: currentPath, parent: null, entries: [] });
|
setListing({ current: currentPath, parent: null, entries: [] });
|
||||||
setError(loadError instanceof Error ? loadError.message : String(loadError));
|
setError(friendlyDirectoryError(loadError));
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (!cancelled) setLoading(false);
|
if (!cancelled) setLoading(false);
|
||||||
@@ -252,15 +277,32 @@ export function FolderPickerModal() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!folderPickerOpen) return;
|
if (!folderPickerOpen) return;
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === "Escape") setFolderPickerOpen(false);
|
if (event.key === "Escape") {
|
||||||
|
if (addressEditing) {
|
||||||
|
setAddressDraft(listing?.current ?? "");
|
||||||
|
setAddressEditing(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFolderPickerOpen(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener("keydown", handleKeyDown);
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [folderPickerOpen, setFolderPickerOpen]);
|
}, [addressEditing, folderPickerOpen, listing?.current, setFolderPickerOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!addressEditing) return;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
addressInputRef.current?.focus();
|
||||||
|
addressInputRef.current?.select();
|
||||||
|
});
|
||||||
|
}, [addressEditing]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (folderPickerOpen) return;
|
if (folderPickerOpen) return;
|
||||||
setCurrentPath(null);
|
setCurrentPath(null);
|
||||||
|
setAddressDraft("");
|
||||||
|
setAddressEditing(false);
|
||||||
setListing(null);
|
setListing(null);
|
||||||
setStagedPaths([]);
|
setStagedPaths([]);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -271,6 +313,10 @@ export function FolderPickerModal() {
|
|||||||
if (!folderPickerOpen) return null;
|
if (!folderPickerOpen) return null;
|
||||||
|
|
||||||
const entries = listing?.entries ?? [];
|
const entries = listing?.entries ?? [];
|
||||||
|
const addressPath = cleanAddressInput(addressEditing ? addressDraft : (listing?.current ?? ""));
|
||||||
|
const normalizedAddressPath = addressPath ? normalizePath(addressPath) : "";
|
||||||
|
const addressAlreadyAdded = normalizedAddressPath ? libraryPaths.has(normalizedAddressPath) : false;
|
||||||
|
const addressAlreadyStaged = normalizedAddressPath ? stagedSet.has(normalizedAddressPath) : false;
|
||||||
|
|
||||||
const togglePath = (path: string) => {
|
const togglePath = (path: string) => {
|
||||||
const normalized = normalizePath(path);
|
const normalized = normalizePath(path);
|
||||||
@@ -282,6 +328,40 @@ export function FolderPickerModal() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stagePath = (path: string) => {
|
||||||
|
const cleaned = cleanAddressInput(path);
|
||||||
|
if (!cleaned) {
|
||||||
|
setError("Enter a folder path first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = normalizePath(cleaned);
|
||||||
|
if (libraryPaths.has(normalized)) {
|
||||||
|
setError("That folder is already in your library.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stagedSet.has(normalized)) {
|
||||||
|
setError("That folder is already selected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
setResults(null);
|
||||||
|
setStagedPaths((current) => [...current, cleaned]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToAddress = () => {
|
||||||
|
const cleaned = cleanAddressInput(addressDraft);
|
||||||
|
setResults(null);
|
||||||
|
setError(null);
|
||||||
|
setCurrentPath(cleaned || null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const beginAddressEdit = () => {
|
||||||
|
setAddressDraft(listing?.current ?? "");
|
||||||
|
setAddressEditing(true);
|
||||||
|
};
|
||||||
|
|
||||||
const removeStagedPath = (path: string) => {
|
const removeStagedPath = (path: string) => {
|
||||||
const normalized = normalizePath(path);
|
const normalized = normalizePath(path);
|
||||||
setResults(null);
|
setResults(null);
|
||||||
@@ -354,7 +434,40 @@ export function FolderPickerModal() {
|
|||||||
>
|
>
|
||||||
Up
|
Up
|
||||||
</button>
|
</button>
|
||||||
<nav className="flex min-w-0 flex-1 items-center gap-1 overflow-hidden rounded-md border border-white/10 bg-white/[0.025] px-2 py-1.5 light-theme:border-gray-300/70 light-theme:bg-gray-900">
|
{addressEditing ? (
|
||||||
|
<form
|
||||||
|
className="flex min-w-0 flex-1 items-center gap-2"
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
navigateToAddress();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<label className="sr-only" htmlFor="folder-picker-address">Folder path</label>
|
||||||
|
<input
|
||||||
|
ref={addressInputRef}
|
||||||
|
id="folder-picker-address"
|
||||||
|
className="min-w-0 flex-1 rounded-md border border-white/10 bg-white/[0.035] px-3 py-1.5 font-mono text-xs text-gray-200 placeholder-gray-600 outline-none transition-colors focus:border-white/25 focus:bg-white/[0.055] light-theme:border-gray-700/50 light-theme:bg-gray-900 light-theme:text-white light-theme:placeholder-gray-500 light-theme:focus:bg-gray-800"
|
||||||
|
value={addressDraft}
|
||||||
|
onChange={(event) => {
|
||||||
|
setAddressDraft(event.target.value);
|
||||||
|
setResults(null);
|
||||||
|
}}
|
||||||
|
placeholder="Paste or type a folder path"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="rounded-md border border-white/10 bg-white/[0.035] px-2.5 py-1.5 text-xs text-gray-300 transition-colors hover:bg-white/[0.07] hover:text-white disabled:cursor-not-allowed disabled:opacity-40 light-theme:border-gray-700/50 light-theme:bg-gray-900 light-theme:text-white light-theme:hover:bg-gray-800"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Go
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="flex min-w-0 flex-1 items-center gap-1 overflow-hidden rounded-md border border-white/10 bg-white/[0.025] px-2 py-1.5 light-theme:border-gray-300/70 light-theme:bg-gray-900"
|
||||||
|
>
|
||||||
|
<nav className="flex min-w-0 items-center gap-1 overflow-hidden">
|
||||||
{breadcrumbs.map((crumb, index) => (
|
{breadcrumbs.map((crumb, index) => (
|
||||||
<span key={`${crumb.path ?? "root"}-${index}`} className="flex min-w-0 items-center gap-1">
|
<span key={`${crumb.path ?? "root"}-${index}`} className="flex min-w-0 items-center gap-1">
|
||||||
{index > 0 ? <span className="text-gray-700 light-theme:text-gray-400">/</span> : null}
|
{index > 0 ? <span className="text-gray-700 light-theme:text-gray-400">/</span> : null}
|
||||||
@@ -362,7 +475,10 @@ export function FolderPickerModal() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="max-w-40 truncate rounded px-1.5 py-0.5 text-xs text-gray-400 transition-colors hover:bg-white/[0.06] hover:text-white light-theme:text-gray-500 light-theme:hover:bg-gray-800 light-theme:hover:text-white"
|
className="max-w-40 truncate rounded px-1.5 py-0.5 text-xs text-gray-400 transition-colors hover:bg-white/[0.06] hover:text-white light-theme:text-gray-500 light-theme:hover:bg-gray-800 light-theme:hover:text-white"
|
||||||
onClick={() => setCurrentPath(crumb.path)}
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setCurrentPath(crumb.path);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{crumb.label}
|
{crumb.label}
|
||||||
</button>
|
</button>
|
||||||
@@ -370,6 +486,22 @@ export function FolderPickerModal() {
|
|||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="min-w-10 flex-1 self-stretch cursor-text rounded px-1"
|
||||||
|
onClick={beginAddressEdit}
|
||||||
|
aria-label="Edit folder path"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded-md border border-emerald-400/35 bg-emerald-500/15 px-2.5 py-1.5 text-xs text-emerald-200 transition-colors hover:bg-emerald-500/25 disabled:cursor-not-allowed disabled:opacity-45"
|
||||||
|
onClick={() => stagePath(addressPath)}
|
||||||
|
disabled={!addressPath || addressAlreadyAdded || addressAlreadyStaged}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error ? (
|
{error ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user