feat(SearchAssembly64): add AQL search help dialog and terms reference
This commit is contained in:
parent
ed5a222fd6
commit
ddbf0d2df2
24
AGENTS.md
24
AGENTS.md
|
|
@ -39,7 +39,20 @@ src/
|
|||
NetworkPage.tsx # Network settings, WiFi scan/connect (via WS)
|
||||
OtherPage.tsx # Misc settings
|
||||
ToolsPage.tsx # Tools
|
||||
SearchOverlay.tsx
|
||||
SearchPane.tsx # Swipeable search shell: spring slide-up, tab bar (Local | Assembly64),
|
||||
# X close button, horizontal snap-x scroll container; initialTab prop
|
||||
# (0=Local, 1=Assembly64); onScroll updates active tab indicator
|
||||
SearchLocal.tsx # Local file search panel (panel inside SearchPane, no fixed positioning);
|
||||
# TOSEC/No-Intro tag parsing (PAL/NTSC, system, ISO 639-1 language);
|
||||
# wildcard search (* → %, ? → _); MediaEntry rows with badges + path;
|
||||
# faceted filter chips; module-level _store persists state across unmounts;
|
||||
# actions dialog: "Mount on virtual drive" + "Open containing folder"
|
||||
SearchAssembly64.tsx # Assembly64 Leet API browser panel (panel inside SearchPane);
|
||||
# fetches /search/categories + /search/aql/presets on mount;
|
||||
# AQL search with pagination; category filter chips; ContentItem result rows;
|
||||
# item tap → /search/entries → file list with Download + Mount actions;
|
||||
# Download saves to /sd/downloads/ via putFileContents;
|
||||
# client-id: meatloaf-config header; module-level _store
|
||||
WiFiScanOverlay.tsx # WiFi scan results; Connect sends "connect <ssid> [<pass>]" via WS
|
||||
MediaManager.tsx # WebDAV file browser: file icons by type, kebab actions,
|
||||
# Configure Folder (.config editor), base_url mount logic,
|
||||
|
|
@ -72,6 +85,8 @@ src/
|
|||
figma/ # Figma-generated components
|
||||
ui/
|
||||
lazy-loader.tsx # Animated progress bar for Suspense fallbacks (staged steps)
|
||||
marquee-text.tsx # <MarqueeText> — ping-pong scroll when text overflows; per-instance
|
||||
# @keyframes injected into <head> via ResizeObserver; module-level _seq
|
||||
confirm-dialog.tsx
|
||||
# … other shadcn/Radix UI wrappers
|
||||
vendor/
|
||||
|
|
@ -122,12 +137,14 @@ Header: fullscreen toggle, search, apps grid, profile button (→ ProfilePage).
|
|||
|
||||
Grid of app cards grouped by category, each navigates to a stub `AppPage` unless implemented:
|
||||
|
||||
- **Management**: Media Manager, Print Manager, **Serial Console** (implemented), Short Codes
|
||||
- **Management**: Media Manager, **Assembly64** (implemented — opens SearchPane at Assembly64 tab), Print Manager, **Serial Console** (implemented), Short Codes
|
||||
- **Disk**: RAM/ROM Explorer, BAM Editor, Directory Editor, Sector Editor, Disk Visualizer, Dump/Write Disk Image
|
||||
- **Cartridge**: PRG to CRT, Magic Desk Cart Builder, Easy Flash Cart Builder
|
||||
- **Development**: Basic Editor, Assembler, Sprite Editor, Character Set Editor, Petscii Editor
|
||||
- **Display**: Idle Animation, Loading Animation, **Reality Override** (implemented), **Override Admin** (implemented)
|
||||
|
||||
Header search icon opens SearchPane at tab 0 (Local). "Assembly64" AppCard opens SearchPane at tab 1.
|
||||
|
||||
## Work to Date (chronological)
|
||||
|
||||
1. **Initial commit** — project scaffolded with basic pages
|
||||
|
|
@ -189,6 +206,8 @@ Grid of app cards grouped by category, each navigates to a stub `AppPage` unless
|
|||
57. **Toast visibility improvements** — `<Toaster>` in `App.tsx` gains `richColors` (vivid green/red/amber/blue for success/error/warning/loading), `closeButton` (explicit ✕), `duration={5000}` (up from 4 s default), and `toastOptions` with `fontSize: 0.9rem`, `fontWeight: 500`, `padding: 14px 16px`
|
||||
58. **MediaManager: Duplicate action** — `duplicateEntry()` generates unique `"name copy.ext"` / `"name copy 2.ext"` etc. via sequential `fileExists` checks, then calls `copyPath`; button appears in the file Actions Dialog between Download and Rename; reloads current directory on success
|
||||
59. **`.vms` Virtual Media Stack format** — `.vms` files are like `.lst` but each line is `path,name` (comma-separated); name after comma becomes the `MediaSetEntry.name` display label; `PLAYLIST_EXTS = new Set(['lst', 'vms'])` exported from `MediaEntry.tsx` with `Layers` icon (indigo); `MediaManager` and `DeviceDetailOverlay` both parse `.vms` into `MediaSetEntry[]`; `mediaSetEntryUrl()` used for `fileExists` checks and `dev.url`
|
||||
60. **MarqueeText component** — `ui/marquee-text.tsx`: extracted from inline MediaManager code; module-level `_seq` counter generates unique animation IDs; per-instance `@keyframes` rule injected into `<head>` and cleaned up on unmount; `ResizeObserver` re-measures on resize; ping-pong (alternate) scroll with `ease-in-out` and 0.8 s delay; used by MediaManager, SearchLocal, SearchAssembly64
|
||||
61. **SearchLocal + SearchAssembly64 + SearchPane** — `SearchOverlay.tsx` split into three components: `SearchPane.tsx` (swipeable shell: `fixed inset-0 bg-white/80 backdrop-blur-md`, spring slide-up via `motion/react`, tab bar with underline indicator + X button, horizontal `snap-x snap-mandatory` scroll container, `onScroll` updates active tab); `SearchLocal.tsx` (local file search panel: TOSEC/No-Intro tag parsing for system/video/language facet chips, wildcard search via `toSqlLike()` in `locate-db.ts` converting `*`→`%` and `?`→`_`, `MediaEntry` rows with badges + path, module-level `_store` persists results/scroll/filters across unmounts, actions dialog with MarqueeText + "Mount on virtual drive" + "Open containing folder", mounted-path highlight via `base_url + url` resolution); `SearchAssembly64.tsx` (Assembly64 Leet API: AQL search via `GET /search/aql/{offset}/{limit}`, categories + presets on mount, category filter chips, paginated ContentItem results, item tap fetches file entries, Download saves to `/sd/downloads/` via `putFileContents`, Mount opens device picker dialog, `client-id: meatloaf-config` header, module-level `_store`); header search icon → tab 0, Apps "Assembly64" card → tab 1
|
||||
|
||||
## Known Issues / Open Work
|
||||
|
||||
|
|
@ -273,6 +292,7 @@ All WebDAV I/O goes through `src/app/webdav.ts`:
|
|||
| `createFolder(path)` | MKCOL |
|
||||
| `deletePath(path)` | DELETE |
|
||||
| `movePath(from, to)` | MOVE |
|
||||
| `copyPath(from, to)` | COPY |
|
||||
| `fileExists(path)` | HEAD → `boolean` |
|
||||
| `humanFileSize(bytes)` | Formatting helper |
|
||||
| `normalizePath` / `splitPath` / `joinPath` / `basename` | Path utilities |
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Search, Loader2, HardDrive, Download, ChevronRight, Trophy, Calendar, Users, RefreshCw } from 'lucide-react';
|
||||
import { Search, Loader2, HardDrive, Download, ChevronRight, Trophy, Calendar, Users, RefreshCw, HelpCircle } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { humanFileSize, basename, joinPath, putFileContents } from '../webdav';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
|
||||
|
|
@ -105,10 +105,28 @@ function RatingStars({ value, max = 10 }: { value?: number; max?: number }) {
|
|||
);
|
||||
}
|
||||
|
||||
// ─── AQL reference ───────────────────────────────────────────────────────────
|
||||
|
||||
const AQL_TERMS = [
|
||||
{ term: 'name:', label: 'Name', example: 'name:manic*', description: 'Title of the release. Supports wildcards (*).' },
|
||||
{ term: 'group:', label: 'Group', example: 'group:triad', description: 'Group or organization name.' },
|
||||
{ term: 'handle:', label: 'Handle', example: 'handle:jco', description: 'Author or creator handle.' },
|
||||
{ term: 'year:', label: 'Year', example: 'year:1983', description: 'Release year.' },
|
||||
{ term: 'event:', label: 'Event', example: 'event:assembly', description: 'Party or event name.' },
|
||||
{ term: 'country:', label: 'Country', example: 'country:SE', description: 'Country code (ISO 3166-1 alpha-2).' },
|
||||
{ term: 'category:', label: 'Category', example: 'category:1', description: 'Category ID number.' },
|
||||
{ term: 'compo:', label: 'Compo', example: 'compo:demo', description: 'Competition or compo type.' },
|
||||
{ term: 'place:', label: 'Place', example: 'place:1', description: 'Placement in competition (1 = winner).' },
|
||||
{ term: 'rating:', label: 'Rating', example: 'rating:9', description: 'Internal rating value.' },
|
||||
{ term: 'siteRating:', label: 'Site Rating', example: 'siteRating:8', description: 'Site (user) rating value.' },
|
||||
] as const;
|
||||
|
||||
// ─── Component ────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function SearchAssembly64({ config, setConfig, onClose: _onClose }: SearchAssembly64Props) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [showAqlHelp, setShowAqlHelp] = useState(false);
|
||||
|
||||
const [query, setQuery] = useState(() => _store.query);
|
||||
const [results, setResults] = useState<ContentItem[]>(() => _store.results);
|
||||
|
|
@ -264,14 +282,23 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose
|
|||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-400 pointer-events-none" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && !isSearching && handleSearch()}
|
||||
placeholder="name:manic* group:ultimate year:1983…"
|
||||
className="w-full pl-9 pr-3 py-2.5 bg-neutral-100 border-0 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-colors"
|
||||
className="w-full pl-9 pr-9 py-2.5 bg-neutral-100 border-0 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-colors"
|
||||
disabled={isSearching}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowAqlHelp(true)}
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600 transition-colors"
|
||||
title="AQL search help"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<HelpCircle className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
|
|
@ -550,6 +577,39 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose
|
|||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* AQL help dialog */}
|
||||
<Dialog open={showAqlHelp} onOpenChange={setShowAqlHelp}>
|
||||
<DialogContent className="max-w-sm flex flex-col max-h-[90dvh]">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle>AQL Search Terms</DialogTitle>
|
||||
<DialogDescription>Tap a term to insert it into the search field.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="overflow-y-auto flex-1 min-h-0 flex flex-col gap-1 py-1">
|
||||
{AQL_TERMS.map(({ term, label, example, description }) => (
|
||||
<button
|
||||
key={term}
|
||||
onClick={() => {
|
||||
setQuery(q => {
|
||||
const trimmed = q.trimEnd();
|
||||
return trimmed ? `${trimmed} ${term}` : term;
|
||||
});
|
||||
setShowAqlHelp(false);
|
||||
setTimeout(() => inputRef.current?.focus(), 50);
|
||||
}}
|
||||
className="w-full text-left px-4 py-3 rounded-lg border border-neutral-200 hover:bg-blue-50 hover:border-blue-300 transition-colors"
|
||||
>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="font-mono text-sm font-semibold text-blue-700">{term}</span>
|
||||
<span className="text-xs text-neutral-400">{label}</span>
|
||||
<span className="ml-auto font-mono text-xs text-neutral-400">{example}</span>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-500 mt-0.5">{description}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user