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)
|
NetworkPage.tsx # Network settings, WiFi scan/connect (via WS)
|
||||||
OtherPage.tsx # Misc settings
|
OtherPage.tsx # Misc settings
|
||||||
ToolsPage.tsx # Tools
|
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
|
WiFiScanOverlay.tsx # WiFi scan results; Connect sends "connect <ssid> [<pass>]" via WS
|
||||||
MediaManager.tsx # WebDAV file browser: file icons by type, kebab actions,
|
MediaManager.tsx # WebDAV file browser: file icons by type, kebab actions,
|
||||||
# Configure Folder (.config editor), base_url mount logic,
|
# Configure Folder (.config editor), base_url mount logic,
|
||||||
|
|
@ -72,6 +85,8 @@ src/
|
||||||
figma/ # Figma-generated components
|
figma/ # Figma-generated components
|
||||||
ui/
|
ui/
|
||||||
lazy-loader.tsx # Animated progress bar for Suspense fallbacks (staged steps)
|
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
|
confirm-dialog.tsx
|
||||||
# … other shadcn/Radix UI wrappers
|
# … other shadcn/Radix UI wrappers
|
||||||
vendor/
|
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:
|
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
|
- **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
|
- **Cartridge**: PRG to CRT, Magic Desk Cart Builder, Easy Flash Cart Builder
|
||||||
- **Development**: Basic Editor, Assembler, Sprite Editor, Character Set Editor, Petscii Editor
|
- **Development**: Basic Editor, Assembler, Sprite Editor, Character Set Editor, Petscii Editor
|
||||||
- **Display**: Idle Animation, Loading Animation, **Reality Override** (implemented), **Override Admin** (implemented)
|
- **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)
|
## Work to Date (chronological)
|
||||||
|
|
||||||
1. **Initial commit** — project scaffolded with basic pages
|
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`
|
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
|
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`
|
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
|
## Known Issues / Open Work
|
||||||
|
|
||||||
|
|
@ -273,6 +292,7 @@ All WebDAV I/O goes through `src/app/webdav.ts`:
|
||||||
| `createFolder(path)` | MKCOL |
|
| `createFolder(path)` | MKCOL |
|
||||||
| `deletePath(path)` | DELETE |
|
| `deletePath(path)` | DELETE |
|
||||||
| `movePath(from, to)` | MOVE |
|
| `movePath(from, to)` | MOVE |
|
||||||
|
| `copyPath(from, to)` | COPY |
|
||||||
| `fileExists(path)` | HEAD → `boolean` |
|
| `fileExists(path)` | HEAD → `boolean` |
|
||||||
| `humanFileSize(bytes)` | Formatting helper |
|
| `humanFileSize(bytes)` | Formatting helper |
|
||||||
| `normalizePath` / `splitPath` / `joinPath` / `basename` | Path utilities |
|
| `normalizePath` / `splitPath` / `joinPath` / `basename` | Path utilities |
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
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 { toast } from 'sonner';
|
||||||
import { humanFileSize, basename, joinPath, putFileContents } from '../webdav';
|
import { humanFileSize, basename, joinPath, putFileContents } from '../webdav';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
|
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 ────────────────────────────────────────────────────────────────
|
// ─── Component ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function SearchAssembly64({ config, setConfig, onClose: _onClose }: SearchAssembly64Props) {
|
export default function SearchAssembly64({ config, setConfig, onClose: _onClose }: SearchAssembly64Props) {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [showAqlHelp, setShowAqlHelp] = useState(false);
|
||||||
|
|
||||||
const [query, setQuery] = useState(() => _store.query);
|
const [query, setQuery] = useState(() => _store.query);
|
||||||
const [results, setResults] = useState<ContentItem[]>(() => _store.results);
|
const [results, setResults] = useState<ContentItem[]>(() => _store.results);
|
||||||
|
|
@ -264,14 +282,23 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose
|
||||||
<div className="relative flex-1">
|
<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" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-400 pointer-events-none" />
|
||||||
<input
|
<input
|
||||||
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={e => setQuery(e.target.value)}
|
onChange={e => setQuery(e.target.value)}
|
||||||
onKeyDown={e => e.key === 'Enter' && !isSearching && handleSearch()}
|
onKeyDown={e => e.key === 'Enter' && !isSearching && handleSearch()}
|
||||||
placeholder="name:manic* group:ultimate year:1983…"
|
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}
|
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>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleSearch}
|
onClick={handleSearch}
|
||||||
|
|
@ -550,6 +577,39 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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