diff --git a/src/app/os/files/page.tsx b/src/app/os/files/page.tsx index 6af1427..743b91e 100644 --- a/src/app/os/files/page.tsx +++ b/src/app/os/files/page.tsx @@ -1,8 +1,12 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { useSearchParams } from "next/navigation"; +import { useDebounce } from "use-debounce"; +import { Search, X } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; import FileSystem from "@/components/filesystem"; import { createEngine } from "@/lib/engine"; @@ -13,6 +17,9 @@ export default function Files() { const [loading, setLoading] = useState(true); const [files, setFiles] = useState([]); + const [keyword, setKeyword] = useState(""); + + const [debouncedKeyword] = useDebounce(keyword, 200); useEffect(() => { setLoading(true); @@ -22,36 +29,74 @@ export default function Files() { .finally(() => setLoading(false)); }, [group, build]); + const filtered = useMemo( + () => + files.filter((path) => + path.toLowerCase().includes(debouncedKeyword.toLowerCase()) + ), + [debouncedKeyword, files] + ); + + const isFiltering = debouncedKeyword.length > 0; + return ( -
+
+
+
+ + setKeyword(e.target.value)} + className="pl-9 pr-9" + /> + {keyword && ( + + )} +
+ {!loading && ( +
+ {isFiltering ? ( + <> + {filtered.length} of {files.length} paths + + ) : ( + <>{files.length} paths + )} +
+ )} +
+ {loading ? (
-
-
-
-
-
-
-
-
+ {Array.from({ length: 6 }).map((_, i) => ( +
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ ))} +
+ ) : filtered.length === 0 ? ( +
+ {files.length === 0 ? ( +

No paths found for this OS version.

+ ) : ( +

No paths match "{keyword}"

+ )}
) : ( - + )}
); diff --git a/src/app/os/keys/page.tsx b/src/app/os/keys/page.tsx index db1fc18..f3f31c3 100644 --- a/src/app/os/keys/page.tsx +++ b/src/app/os/keys/page.tsx @@ -1,10 +1,16 @@ "use client"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useMemo, useCallback } from "react"; import { useSearchParams } from "next/navigation"; import { useDebounce } from "use-debounce"; import Link from "next/link"; -import { Search, X, ChevronRight, ChevronDown } from "lucide-react"; +import { + Search, + X, + ChevronRight, + ChevronDown, + ChevronsUpDown, +} from "lucide-react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; @@ -62,7 +68,7 @@ function KeyBadge({ return ( {suffix ? ( @@ -83,14 +89,20 @@ function KeyGroup({ prefix, keys, os, - defaultOpen, + forceOpen, }: { prefix: string; keys: string[]; os: string; - defaultOpen: boolean; + forceOpen: boolean | null; }) { - const [open, setOpen] = useState(defaultOpen); + const [open, setOpen] = useState(forceOpen ?? keys.length <= 8); + + useEffect(() => { + if (forceOpen !== null) { + setOpen(forceOpen); + } + }, [forceOpen]); // Single standalone key - just show it inline if (keys.length === 1 && keys[0] === prefix) { @@ -120,7 +132,7 @@ function KeyGroup({ -
+
{keys.map((key) => ( ))} @@ -138,6 +150,7 @@ export default function Keys() { const [loading, setLoading] = useState(true); const [keys, setKeys] = useState([]); const [keyword, setKeyword] = useState(""); + const [forceOpen, setForceOpen] = useState(null); const [debouncedKeyword] = useDebounce(keyword, 200); @@ -168,6 +181,17 @@ export default function Keys() { ); const isFiltering = debouncedKeyword.length > 0; + const hasGroups = sortedPrefixes.some( + (p) => grouped[p].length > 1 || grouped[p][0] !== p + ); + + const handleExpandAll = useCallback(() => setForceOpen(true), []); + const handleCollapseAll = useCallback(() => setForceOpen(false), []); + + // Reset forceOpen when filter changes + useEffect(() => { + setForceOpen(isFiltering ? true : null); + }, [isFiltering]); return (
@@ -192,37 +216,75 @@ export default function Keys() { )}
- {!loading && ( -
- {isFiltering ? ( - <> - {filtered.length} of {keys.length} keys - - ) : ( - <>{keys.length} entitlement keys - )} -
- )} +
+ {!loading && hasGroups && ( +
+ + +
+ )} + {!loading && ( +
+ {isFiltering ? ( + <> + {filtered.length} of {keys.length} keys + + ) : ( + <>{keys.length} entitlement keys + )} +
+ )} +
{loading ? ( -
- {Array.from({ length: 8 }).map((_, index) => ( -
-
-
- {Array.from({ length: 3 + Math.floor(Math.random() * 4) }).map( - (_, i) => ( -
- ) - )} +
+ {[ + { prefix: 180, items: [140, 160, 120] }, + { prefix: 220, items: [180, 140, 200, 160] }, + { prefix: 160, items: [120, 180] }, + { prefix: 200, items: [160, 140, 180, 120, 200] }, + { prefix: 140, items: [100, 140, 120] }, + { prefix: 240, items: [180, 160, 200, 140] }, + ].map((group, index) => ( +
+
+
+
+
+
+
+ {group.items.map((width, i) => ( +
+ ))}
))} @@ -243,7 +305,7 @@ export default function Keys() { prefix={prefix} keys={grouped[prefix]} os={os} - defaultOpen={isFiltering || grouped[prefix].length <= 8} + forceOpen={forceOpen} /> ))}
diff --git a/src/components/filesystem.tsx b/src/components/filesystem.tsx index 306be4f..e746088 100644 --- a/src/components/filesystem.tsx +++ b/src/components/filesystem.tsx @@ -1,45 +1,63 @@ +import { useState } from "react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; - -import { Button } from "@/components/ui/button"; +import { ChevronRight, ChevronDown, FileText, Folder } from "lucide-react"; import filesToTree, { type TreeWithFullPath } from "@/lib/tree"; import Link from "next/link"; -function Tree({ item, os }: { item: TreeWithFullPath; os: string }) { +function countItems(item: TreeWithFullPath): number { + let count = 0; + for (const value of Object.values(item)) { + if (typeof value === "string") { + count++; + } else { + count += countItems(value); + } + } + return count; +} + +function Tree({ + item, + os, + depth, + expandAll, +}: { + item: TreeWithFullPath; + os: string; + depth: number; + expandAll: boolean; +}) { return ( -
    +
      {Object.entries(item).map(([key, value]) => { if (typeof value === "string") { return ( -
    • +
    • + - /{key} + {key}
    • ); } else { return ( -
    • -
        - - - - - - - - -
      -
    • + ); } })} @@ -47,17 +65,60 @@ function Tree({ item, os }: { item: TreeWithFullPath; os: string }) { ); } +function TreeFolder({ + name, + item, + os, + depth, + expandAll, +}: { + name: string; + item: TreeWithFullPath; + os: string; + depth: number; + expandAll: boolean; +}) { + const [open, setOpen] = useState(expandAll || depth < 1); + const itemCount = countItems(item); + + return ( +
    • + + + {open ? ( + + ) : ( + + )} + + {name} + + {itemCount} + + + +
      + +
      +
      +
      +
    • + ); +} + export default function FileSystem({ list, os, + expandAll = false, }: { list: string[]; os: string; + expandAll?: boolean; }) { const tree = filesToTree(list); return ( -
      - +
      +
      ); }