diff --git a/web/src/pages/Build.tsx b/web/src/pages/Build.tsx index f5a0352..8cf53eb 100644 --- a/web/src/pages/Build.tsx +++ b/web/src/pages/Build.tsx @@ -1,11 +1,11 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AudioLines, ChevronDown, ChevronRight, ChevronsDownUp, Disc3, Folder, FolderOpen, Link2, Play, Unlink } from "lucide-react"; import { api, type Pair, type TreeEntry } from "../api"; import { Alert, Badge, Button, Card, Empty, Modal, Spinner, cx, formatBytes } from "../ui"; import { useToast } from "../toast"; import { MiniPlayer, type Track } from "./Player"; -import { LetterBar } from "./Review"; +import { LetterBar, letterOf } from "./Review"; const rowID = (rel: string) => `build-${rel}`; @@ -18,6 +18,9 @@ const ancestorIn = (sel: Set, rel: string) => { export function Build({ pair }: { pair: Pair }) { // Open folders, oldest first, so Backspace closes the last one opened. const [open, setOpen] = useState([]); + // The master's top level is shown one letter at a time. + const [letter, setLetter] = useState(null); + const listRef = useRef(null); const [sel, setSel] = useState>(new Set()); const [confirm, setConfirm] = useState(null); // The mini-player keeps its own copy of the folder it started in. @@ -51,6 +54,15 @@ export function Build({ pair }: { pair: Pair }) { } return n; }); + const toggleAll = (entries: TreeEntry[], on: boolean) => + setSel((s) => { + const n = new Set(s); + for (const e of entries) { + if (on) for (const x of n) if (x.startsWith(e.rel + "/")) n.delete(x); + on ? n.add(e.rel) : n.delete(e.rel); + } + return n; + }); const toggleOpen = (rel: string) => setOpen((o) => (o.includes(rel) ? o.filter((x) => x !== rel && !x.startsWith(rel + "/")) : [...o, rel])); const play = (siblings: TreeEntry[], rel: string) => { @@ -58,7 +70,11 @@ export function Build({ pair }: { pair: Pair }) { setPlayer({ queue, index: queue.findIndex((t) => t.rel === rel) }); }; const all = tree.data ?? []; - const allSelected = all.length > 0 && all.every((e) => sel.has(e.rel)); + // Until a letter is picked, show the first one the bar offers (A–Z, then #). + const has = new Set(all.map((e) => letterOf(e.name))); + const current = letter && has.has(letter) ? letter : [..."ABCDEFGHIJKLMNOPQRSTUVWXYZ#"].find((l) => has.has(l)); + const shown = all.filter((e) => letterOf(e.name) === current); + const allSelected = shown.length > 0 && shown.every((e) => sel.has(e.rel)); // Backspace closes the folder opened last, and scrolls back to it. useEffect(() => { @@ -85,14 +101,9 @@ export function Build({ pair }: { pair: Pair }) {
-
- - - Backspace closes the last folder opened - -
+
{sel.size > 0 && {sel.size} selected}