Compare commits
4 commits
85212a12fb
...
7ee735c2db
Author | SHA1 | Date | |
---|---|---|---|
7ee735c2db | |||
ab7bc36714 | |||
ad543d67b7 | |||
2ee403ddfb |
4 changed files with 2432 additions and 18 deletions
89
AGENTS.md
89
AGENTS.md
|
@ -197,3 +197,92 @@ SELECT year, COUNT(*) FROM tracks GROUP BY year ORDER BY year;
|
|||
- When editing `index.html`/`script.js`, include inline comments explaining assumptions.
|
||||
- Verify changes against the schema above.
|
||||
|
||||
---
|
||||
|
||||
## TODOs / UX Elements Roadmap
|
||||
|
||||
Overview
|
||||
- Views are "base" (replace main content) or "overlay" (stacked dialog) via `UX.replace(...)` and `UX.openOverlay(...)` in `script.js`.
|
||||
- Each view pairs an HTML template in `index.html` (e.g., `tpl-...`) with a creator in `script.js` (e.g., `create...View(db)`), returning `{ kind, el, onShow?, destroy? }`.
|
||||
- Use Bulma form/table/pagination patterns. Keep DOM small; paginate and debounce queries.
|
||||
|
||||
Shared Tasks
|
||||
- [x] Add small table/list renderer util in `script.js` to build rows safely (uses `escapeHtml`). Implemented via `createTableRenderer`.
|
||||
- [x] Add shared pagination component (Prev/Next, page size select). Propagate `LIMIT/OFFSET`. Implemented via `createPagination`.
|
||||
- [x] Add common keyboard handlers: Enter to open selection; Esc to close overlays (already wired globally). Implemented via shared `Keyboard` helper in `script.js`.
|
||||
- [x] Add loading/empty-state helpers for lists. Implemented via `createAsyncListState` utility.
|
||||
|
||||
Primary Navigation (Hub)
|
||||
- [x] `tpl-nav` (base): Landing hub to choose "Search", "Browse Artists", "Browse Albums", "Browse Years", "Browse Genres", "Stats".
|
||||
- [x] `createNavView(db)`: Buttons/cards trigger `UX.replace(...)` to corresponding base views.
|
||||
- [x] Accessibility: initial focus on first action; arrow-key navigation across items; visible focus states.
|
||||
|
||||
Search (Existing)
|
||||
- [x] `tpl-search` (base): Input is focusable; shows results area.
|
||||
- [x] Implement query execution with FTS join; debounce 250 ms; paginate results. Wired into the new table + pagination helpers.
|
||||
- SQL: `SELECT t.id, a.name AS artist, t.title, IFNULL(al.title,'') AS album, t.year, t.genre FROM fts_tracks f JOIN tracks t ON t.id=f.rowid JOIN artists a ON a.id=t.artist_id LEFT JOIN albums al ON al.id=t.album_id WHERE f MATCH ? ORDER BY rank LIMIT ? OFFSET ?`.
|
||||
- [x] Column sorts (toggle rank vs artist,title,year).
|
||||
- [x] Row activation opens Track overlay.
|
||||
|
||||
Browse Artists
|
||||
- [x] `tpl-browse-artists` (base): Alphabetical list, A–Z quick jump, mini filter box; paginated.
|
||||
- [x] `createBrowseArtistsView(db)`: Loads pages; clicking row opens Artist overlay.
|
||||
- SQL: `SELECT id, name FROM artists ORDER BY name LIMIT ? OFFSET ?`.
|
||||
- [x] Optional prefix search using FTS prefix (`WHERE f MATCH 'artist:abc*'`) to accelerate filter. Browse Artists view now favors FTS when filters include ≥2 characters.
|
||||
|
||||
Browse Albums
|
||||
- [x] `tpl-browse-albums` (base): List or grid with title, artist badge, year; paginated and sortable by artist/year/title.
|
||||
- [x] `createBrowseAlbumsView(db)`: Clicking item opens Album overlay.
|
||||
- SQL: `SELECT al.id, al.title, al.year, a.name AS artist FROM albums al JOIN artists a ON a.id=al.artist_id ORDER BY a.name, al.year, al.title LIMIT ? OFFSET ?`.
|
||||
|
||||
Browse Years
|
||||
- [x] `tpl-browse-years` (base): Year histogram (counts) with list of tracks/albums when a year is selected; paginated.
|
||||
- [x] `createBrowseYearsView(db)`: Selecting a year shows tracks; rows open Album/Track overlays.
|
||||
- SQL (counts): `SELECT year, COUNT(*) AS cnt FROM tracks WHERE year IS NOT NULL GROUP BY year ORDER BY year`.
|
||||
- SQL (tracks by year): `SELECT t.id, t.title, a.name AS artist, IFNULL(al.title,'') AS album, t.genre FROM tracks t JOIN artists a ON a.id=t.artist_id LEFT JOIN albums al ON al.id=t.album_id WHERE t.year=? ORDER BY a.name, t.title LIMIT ? OFFSET ?`.
|
||||
|
||||
Browse Genres
|
||||
- [x] `tpl-browse-genres` (base): Genre chips with counts → selecting shows paginated tracks.
|
||||
- [x] `createBrowseGenresView(db)`: Genre list with counts; selecting lists tracks; rows open Track overlay.
|
||||
- SQL (counts): `SELECT genre, COUNT(*) AS cnt FROM tracks WHERE genre IS NOT NULL AND genre!='' GROUP BY genre ORDER BY cnt DESC, genre`.
|
||||
- SQL (tracks by genre): `SELECT t.id, t.title, a.name AS artist, IFNULL(al.title,'') AS album, t.year FROM tracks t JOIN artists a ON a.id=t.artist_id LEFT JOIN albums al ON al.id=t.album_id WHERE t.genre=? ORDER BY a.name, t.title LIMIT ? OFFSET ?`.
|
||||
|
||||
Stats
|
||||
- [x] `tpl-stats` (base): Lightweight metrics (totals, top artists, year distribution) linking into browse views.
|
||||
- [x] `createStatsView(db)`: Render summary cards; links navigate via `UX.replace(...)` with preselected filters.
|
||||
- SQL (examples): totals from `COUNT(*)` on artists/albums/tracks; top artists via `SELECT a.name, COUNT(*) cnt FROM tracks t JOIN artists a ON a.id=t.artist_id GROUP BY a.id ORDER BY cnt DESC LIMIT 20`.
|
||||
|
||||
Artist Overlay
|
||||
- [x] `tpl-artist` (overlay): Header: name + counts; tabs: Albums | Top Tracks.
|
||||
- [x] `createArtistOverlay(db, artistId)`: Load artist name, counts, then tab content.
|
||||
- SQL (albums): `SELECT id, title, year FROM albums WHERE artist_id=? ORDER BY year, title`.
|
||||
- SQL (top tracks): `SELECT id, title, year, genre FROM tracks WHERE artist_id=? ORDER BY year, title LIMIT 100`.
|
||||
- [x] Actions: clicking album opens Album overlay; clicking track opens Track overlay.
|
||||
|
||||
Album Overlay
|
||||
- [x] `tpl-album` (overlay): Header with album title, artist, year; tracklist table with `track_no`, `title`, `duration_sec`, `bitrate_kbps`.
|
||||
- [x] `createAlbumOverlay(db, albumId)`: Load album+artist header; then tracklist.
|
||||
- SQL (header): `SELECT al.title, al.year, a.name AS artist FROM albums al JOIN artists a ON a.id=al.artist_id WHERE al.id=?`.
|
||||
- SQL (tracks): `SELECT id, track_no, title, duration_sec, bitrate_kbps FROM tracks WHERE album_id=? ORDER BY track_no, title`.
|
||||
- [x] Row activation opens Track overlay.
|
||||
|
||||
Track Overlay
|
||||
- [x] `tpl-track` (overlay): Show title, artist, album, year, genre, duration, bitrate, samplerate, channels, filesize, sha1 (if present), and `relpath` with a Copy button.
|
||||
- [x] `createTrackOverlay(db, trackId)`: Load detail from join; add Copy action for `relpath`.
|
||||
- SQL: `SELECT t.*, a.name AS artist, al.title AS album FROM tracks t JOIN artists a ON a.id=t.artist_id LEFT JOIN albums al ON al.id=t.album_id WHERE t.id=?`.
|
||||
|
||||
Filters (Optional)
|
||||
- [ ] `tpl-filters` (overlay): Advanced filters (year range, min bitrate, genre multi-select) applied to current base view.
|
||||
- [ ] `createFiltersOverlay(db, onApply)`: Applies constraints and refreshes the invoking view.
|
||||
|
||||
Help/Meta Overlays
|
||||
- [ ] `tpl-keyboard-shortcuts` (overlay): " / focus search", "Esc close overlay", "j/k navigate" if list navigation is added.
|
||||
- [ ] `tpl-about` (overlay): About/help, privacy note.
|
||||
- [ ] `tpl-error` (overlay): Friendly error with retry; used by views on failure.
|
||||
|
||||
Implementation Notes
|
||||
- Use template IDs in `index.html` and instantiate via existing `instantiateTemplate` helper.
|
||||
- Overlays must add `ux-view--overlay` class (done by UX manager) and include a close button that calls `UX.closeTop()`.
|
||||
- Keep queries read-only; always `LIMIT ? OFFSET ?` for lists; avoid `SELECT *` except for single-row detail.
|
||||
- Respect accessibility: label–input associations, `aria-live` only for async status, focus returned to opener on overlay close.
|
||||
- Performance: debounce search 200–300 ms; cache prepared statements if beneficial; do not pre-render large lists.
|
||||
|
|
286
index.html
286
index.html
|
@ -44,7 +44,8 @@
|
|||
<div class="field">
|
||||
<label class="label" for="q">Search</label>
|
||||
<div class="control">
|
||||
<input id="q" name="q" class="input" type="search" placeholder="Search artist, title, album, genre…" disabled aria-disabled="true" data-ref="q" />
|
||||
<!-- Input should be interactive when the search view shows; JS manages focus/state. -->
|
||||
<input id="q" name="q" class="input" type="search" placeholder="Search artist, title, album, genre…" data-ref="q" />
|
||||
</div>
|
||||
<p class="help">Powered by in-browser SQLite FTS; no network queries.</p>
|
||||
</div>
|
||||
|
@ -56,6 +57,289 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-nav">
|
||||
<div class="ux-view fade">
|
||||
<div class="box" data-ref="nav">
|
||||
<h2 class="title is-4">Browse Library</h2>
|
||||
<p class="subtitle is-6">Pick a view to explore the catalog.</p>
|
||||
<div class="columns is-multiline" role="list">
|
||||
<div class="column is-half-tablet is-one-third-desktop" role="listitem">
|
||||
<button class="button is-link is-fullwidth" type="button" data-action="search">Search Tracks</button>
|
||||
</div>
|
||||
<div class="column is-half-tablet is-one-third-desktop" role="listitem">
|
||||
<button class="button is-fullwidth" type="button" data-action="artists">Browse Artists</button>
|
||||
</div>
|
||||
<div class="column is-half-tablet is-one-third-desktop" role="listitem">
|
||||
<button class="button is-fullwidth" type="button" data-action="albums">Browse Albums</button>
|
||||
</div>
|
||||
<div class="column is-half-tablet is-one-third-desktop" role="listitem">
|
||||
<button class="button is-fullwidth" type="button" data-action="years">Browse Years</button>
|
||||
</div>
|
||||
<div class="column is-half-tablet is-one-third-desktop" role="listitem">
|
||||
<button class="button is-fullwidth" type="button" data-action="genres">Browse Genres</button>
|
||||
</div>
|
||||
<div class="column is-half-tablet is-one-third-desktop" role="listitem">
|
||||
<button class="button is-fullwidth" type="button" data-action="stats">Library Stats</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-browse-artists">
|
||||
<div class="ux-view fade">
|
||||
<div class="box">
|
||||
<header class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<h2 class="title is-4 mb-0">Artists</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<button class="button is-text" type="button" data-action="back">Back</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<label class="is-sr-only" for="artist-filter">Filter artists</label>
|
||||
<input id="artist-filter" class="input" type="search" placeholder="Filter by name" data-ref="filter" />
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="buttons has-addons" role="group" aria-label="Quick jump">
|
||||
<button class="button" type="button" data-action="jump" data-letter="all">All</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="a">A</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="b">B</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="c">C</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="d">D</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="e">E</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="f">F</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="g">G</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="h">H</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="i">I</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="j">J</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="k">K</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="l">L</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="m">M</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="n">N</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="o">O</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="p">P</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="q">Q</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="r">R</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="s">S</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="t">T</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="u">U</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="v">V</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="w">W</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="x">X</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="y">Y</button>
|
||||
<button class="button" type="button" data-action="jump" data-letter="z">Z</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" data-ref="results">
|
||||
<p class="has-text-grey">No data yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-browse-albums">
|
||||
<div class="ux-view fade">
|
||||
<div class="box">
|
||||
<header class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<h2 class="title is-4 mb-0">Albums</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<button class="button is-text" type="button" data-action="back">Back</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="field is-grouped is-justify-content-flex-end">
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select data-ref="sort">
|
||||
<option value="artist">Artist</option>
|
||||
<option value="year">Year</option>
|
||||
<option value="title">Title</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" data-ref="results">
|
||||
<p class="has-text-grey">No data yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-browse-years">
|
||||
<div class="ux-view fade">
|
||||
<div class="box">
|
||||
<header class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<h2 class="title is-4 mb-0">Years</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<button class="button is-text" type="button" data-action="back">Back</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="columns">
|
||||
<div class="column is-one-third" data-ref="years">
|
||||
<p class="has-text-grey">No data yet.</p>
|
||||
</div>
|
||||
<div class="column" data-ref="tracks">
|
||||
<p class="has-text-grey">Select a year to view tracks.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-browse-genres">
|
||||
<div class="ux-view fade">
|
||||
<div class="box">
|
||||
<header class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<h2 class="title is-4 mb-0">Genres</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<button class="button is-text" type="button" data-action="back">Back</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content" data-ref="genres">
|
||||
<p class="has-text-grey">No data yet.</p>
|
||||
</div>
|
||||
<div class="content" data-ref="tracks" hidden>
|
||||
<h3 class="title is-5" data-ref="selected-genre"> </h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-stats">
|
||||
<div class="ux-view fade">
|
||||
<div class="box">
|
||||
<header class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<h2 class="title is-4 mb-0">Library Stats</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<button class="button is-text" type="button" data-action="back">Back</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="columns" data-ref="cards">
|
||||
<div class="column">
|
||||
<div class="notification has-text-grey">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" data-ref="lists">
|
||||
<p class="has-text-grey">Loading…</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-artist">
|
||||
<div class="ux-view fade">
|
||||
<div class="box">
|
||||
<header class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<div>
|
||||
<h2 class="title is-4" data-ref="name">Artist</h2>
|
||||
<p class="subtitle is-6" data-ref="meta"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<button class="delete" type="button" aria-label="Close" data-action="close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="tabs is-toggle" role="tablist">
|
||||
<ul>
|
||||
<li class="is-active"><a href="#" data-tab="albums">Albums</a></li>
|
||||
<li><a href="#" data-tab="tracks">Top Tracks</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div data-tabpanel="albums" role="tabpanel"></div>
|
||||
<div data-tabpanel="tracks" role="tabpanel" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-album">
|
||||
<div class="ux-view fade">
|
||||
<div class="box">
|
||||
<header class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<div>
|
||||
<h2 class="title is-4" data-ref="title">Album</h2>
|
||||
<p class="subtitle is-6" data-ref="meta"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<button class="delete" type="button" aria-label="Close" data-action="close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content" data-ref="tracks">
|
||||
<p class="has-text-grey">Loading…</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="tpl-track">
|
||||
<div class="ux-view fade">
|
||||
<div class="box">
|
||||
<header class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<div>
|
||||
<h2 class="title is-4" data-ref="title">Track</h2>
|
||||
<p class="subtitle is-6" data-ref="meta"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<button class="delete" type="button" aria-label="Close" data-action="close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<dl class="columns is-multiline" data-ref="details"></dl>
|
||||
<div class="field is-grouped is-grouped-right">
|
||||
<div class="control">
|
||||
<button class="button is-small" type="button" data-action="copy-path">Copy Path</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Third-party libs loaded from CDNs (no trackers). Pinned versions. -->
|
||||
<!-- fflate: tiny ZIP library used to unzip the downloaded DB archive client-side. -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/fflate@0.8.2/umd/index.js" crossorigin="anonymous"></script>
|
||||
|
|
14
site.css
14
site.css
|
@ -11,3 +11,17 @@
|
|||
.ux-overlays { position: absolute; inset: 0; pointer-events: none; }
|
||||
.ux-view { width: 100%; }
|
||||
.ux-view--overlay { position: absolute; inset: 0; z-index: 10; pointer-events: auto; }
|
||||
|
||||
.is-sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.table tbody tr.is-selectable-row { cursor: pointer; }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue