Compare commits

...
Author SHA1 Message Date
bvandeusen c9fa18ecb7 Merge pull request 'Notifications inbox (M489), Discover taste-arm fix, web tooling majors' (#151) from dev into main
release / govulncheck (push) Successful in 16s
release / web (push) Successful in 1m8s
release / Build signed APK (releases and dev) (push) Skipped
release / go (push) Successful in 1m27s
release / integration (push) Successful in 5m1s
release / android (push) Successful in 5m35s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m34s
release / Verify release artifacts (tag releases only) (push) Skipped
2026-10-08 11:11:08 -04:00
bvandeusenandClaude Opus 5.5 ca9917d8cd chore(web): clear kit 3's alias and Vite's config-loader deprecations (#5021)
release / govulncheck (push) Successful in 23s
release / web (push) Successful in 1m22s
release / go (push) Successful in 1m41s
release / integration (push) Successful in 4m49s
release / android (push) Successful in 5m32s
release / Build signed APK (releases and dev) (push) Successful in 5m39s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 28s
release / Verify release artifacts (tag releases only) (push) Skipped
CI run 8789 warned on both:

- kit 3 deprecates `config.alias`. The $test-utils alias becomes a
  `#test-utils/*` subpath import, matching #lib, and its 43 import sites
  move with it.
- Vite's coming native config loader needs the extension on
  vitest.config.ts's import of vite.config.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:03:38 -04:00
bvandeusenandClaude Opus 5.5 b1b10b0c77 fix(web): run Tailwind 4 through @tailwindcss/vite, not PostCSS (#5021)
release / govulncheck (push) Successful in 18s
release / web (push) Successful in 56s
release / go (push) Successful in 2m22s
release / integration (push) Successful in 4m56s
release / android (push) Successful in 5m43s
release / Build signed APK (releases and dev) (push) Successful in 5m54s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m14s
release / Verify release artifacts (tag releases only) (push) Skipped
The image build on fecd030b failed in `vite build`:

  [postcss] ENOENT: no such file or directory, open '/web/tailwindcss'

With a PostCSS config present, Vite's own @import inliner resolves
`@import 'tailwindcss'` before @tailwindcss/postcss sees it, and reads it
as a relative file. svelte-check and Vitest never build CSS, so only the
image job caught it. The Vite plugin is Tailwind's documented setup for
Vite projects and handles the import itself, so postcss.config.cjs and
the direct postcss and @tailwindcss/postcss dependencies go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:55:48 -04:00
bvandeusenandClaude Opus 5.5 94a9c8cbe3 chore(deps): SvelteKit 3 with adapter-static 4 and TypeScript 6, full-tree npm audit (#5021)
Merges Renovate's kit 3 (PR #149) and adapter-static 4 (PR #148) bumps,
plus the migration they need. The mechanical part is `sv migrate
sveltekit-3`, run one task at a time and reviewed:

- svelte.config.js is gone. Its options move into sveltekit() in
  vite.config.ts, exported as kitOptions so vitest.config.ts runs the
  same kit setup, including the $test-utils alias the tests import.
- $lib becomes #lib through package.json "imports". There is no
  src/lib/index, so only the "#lib/*" entry is kept.
- tsconfig extends $app/tsconfig.
- Peer floors raised to kit 3's requirements: svelte ^5.57.1, vite
  ^8.0.12, svelte-check ^4.7.5.

By hand, from the codemod's list of non-automated tasks:

- goto's replaceState option is now replace; keepFocus becomes
  reset: false. For the search typeahead, reset: false also stops the
  scroll-to-top, which is wanted while typing.
- The test setup mocks drop pushState/replaceState and $app/paths
  base/assets, which kit 3 removed, and mock refreshAll in place of
  invalidateAll.
- The other flagged files only read page.url or goto internal routes,
  so they needed no change.

TypeScript goes to ^6, not the ^7 Renovate offers: kit 3 declares
typescript ^6 as a peer and svelte-check 4.7 accepts ^5 || ^6. Move to
7 once both accept it.

With Tailwind 4 and kit 3 in, `npm audit` on the whole tree reports 0,
so the web lane now audits every dependency rather than only what
ships to browsers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:50:39 -04:00
bvandeusen beba5b2082 Merge remote-tracking branch 'origin/renovate/sveltejs-kit-3.x' into dev 2026-10-08 10:49:44 -04:00
bvandeusenandClaude Opus 5.5 fecd030b68 chore(deps): Tailwind 4 (#5021)
release / govulncheck (push) Successful in 42s
release / web (push) Successful in 1m39s
release / go (push) Successful in 1m53s
release / integration (push) Successful in 5m37s
release / android (push) Successful in 6m54s
release / Build signed APK (releases and dev) (push) Successful in 7m27s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Failing after 34s
release / Verify release artifacts (tag releases only) (push) Skipped
Renovate's tailwindcss bump (PR #150) plus the migration it needs:

- Theme moves from tailwind.config.js into app.css as `@theme inline`,
  mapping the same FabledSword tokens. tailwind.config.js is gone.
- PostCSS runs @tailwindcss/postcss; autoprefixer is dropped, since
  Tailwind 4 prefixes through Lightning CSS.
- Class renames from @tailwindcss/upgrade 4.3.3, reviewed: outline-none
  -> outline-hidden, focus-visible:outline -> outline-solid, shadow ->
  shadow-sm, shadow-sm -> shadow-xs, flex-shrink-0 -> shrink-0. Bare
  `rounded` stays: v4 keeps it at 0.25rem, as before.
- Three v3 preflight defaults kept in a base layer so nothing changes on
  screen: gray-200 default border colour, gray-400 placeholder text and
  the pointer cursor on buttons.
- The unused class-based dark variant is not carried over; no template
  uses `dark:`.

Clears the five high and two moderate npm audit findings that came in
through Tailwind 3 (braces, chokidar, micromatch, fast-glob,
postcss-selector-parser).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:46:07 -04:00
Renovate Bot 0202c86338 chore(deps): update dependency tailwindcss to v4
renovate/artifacts Artifact file update failure
renovate/stability-days Updates have met minimum release age requirement
2026-10-08 14:45:03 +00:00
Renovate Bot 0500f6aac5 chore(deps): update dependency @sveltejs/kit to v3
renovate/stability-days Updates have met minimum release age requirement
renovate/artifacts Artifact file update failure
2026-10-08 14:45:01 +00:00
bvandeusen 3077ed75fe Merge pull request 'Readable accent text app-wide; Android queue drag math under test' (#144) from dev into main
release / govulncheck (push) Successful in 18s
release / Build signed APK (releases and dev) (push) Skipped
release / web (push) Successful in 1m16s
release / go (push) Successful in 1m30s
release / integration (push) Successful in 4m2s
release / android (push) Successful in 4m20s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 14s
release / Verify release artifacts (tag releases only) (push) Skipped
2026-10-08 06:29:48 -04:00
bvandeusen 97e84a79eb Merge pull request 'Multi-value genres for FLAC/Ogg/MP4, readable tinted and error text, no missing-track seeds' (#143) from dev into main
release / Build signed APK (releases and dev) (push) Skipped
release / govulncheck (push) Successful in 31s
release / web (push) Successful in 1m19s
release / go (push) Successful in 1m41s
release / android (push) Successful in 4m44s
release / integration (push) Successful in 17m13s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m20s
release / Verify release artifacts (tag releases only) (push) Skipped
2026-10-07 23:53:09 -04:00
bvandeusen a29ceaaeab Merge pull request 'Similarity cache and round-robin artist arms (#5296, #5297)' (#142) from dev into main
release / android (push) Successful in 5m1s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 2m25s
release / Verify release artifacts (tag releases only) (push) Skipped
release / go (push) Successful in 2m28s
release / web (push) Successful in 57s
release / govulncheck (push) Successful in 19s
release / Build signed APK (releases and dev) (push) Skipped
release / integration (push) Successful in 5m9s
2026-10-07 20:02:59 -04:00
bvandeusen 54b2720d76 Merge pull request 'Fold stale missing tracks into their on-disk replacement (M485)' (#141) from dev into main
release / Build signed APK (releases and dev) (push) Skipped
release / govulncheck (push) Successful in 15s
release / web (push) Successful in 1m2s
release / go (push) Successful in 1m20s
release / Build + push container image (push) Successful in 14s
release / Verify release artifacts (tag releases only) (push) Skipped
release / integration (push) Successful in 3m51s
release / android (push) Successful in 4m19s
release / Attach APK to the Release (tag releases only) (push) Skipped
2026-10-07 18:17:51 -04:00
bvandeusen fe142e3451 Merge dev: request completion fix, x/text bump, full-logo tab icon (#140)
release / govulncheck (push) Successful in 22s
release / Build signed APK (releases and dev) (push) Skipped
release / web (push) Successful in 1m54s
release / go (push) Successful in 2m22s
release / integration (push) Successful in 5m7s
release / android (push) Successful in 5m47s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 17s
release / Verify release artifacts (tag releases only) (push) Skipped
2026-10-07 15:35:01 -04:00
bvandeusen 3bdfa94f39 Merge dev: M483 release-group ids for Lidarr (#139)
release / integration (push) Successful in 3m46s
release / android (push) Successful in 4m24s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build signed APK (releases and dev) (push) Skipped
release / govulncheck (push) Successful in 17s
release / web (push) Successful in 1m1s
release / go (push) Successful in 1m21s
release / Build + push container image (push) Successful in 16s
release / Verify release artifacts (tag releases only) (push) Skipped
2026-10-07 12:29:12 -04:00
bvandeusen 22ea27efff Merge pull request 'Lidarr: album adds that Lidarr accepts, artist monitoring that sticks' (#138) from dev into main
release / govulncheck (push) Successful in 27s
release / Build signed APK (releases and dev) (push) Skipped
release / web (push) Successful in 1m21s
release / go (push) Successful in 1m48s
release / integration (push) Successful in 4m24s
release / android (push) Successful in 5m16s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
2026-10-07 11:08:18 -04:00
bvandeusen 09bd71629a Merge pull request 'M401: AcoustID recording-id lookup' (#137) from dev into main
release / Build signed APK (releases and dev) (push) Skipped
release / govulncheck (push) Successful in 34s
release / web (push) Successful in 1m6s
release / go (push) Successful in 1m24s
release / integration (push) Successful in 4m3s
release / android (push) Successful in 5m17s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m26s
release / Verify release artifacts (tag releases only) (push) Skipped
2026-10-07 09:50:11 -04:00
bvandeusen c83670d216 Merge pull request 'M464 loudness leveling (steps 4–8), APK distribution and security baseline adoptions' (#136) from dev into main
release / web (push) Successful in 1m41s
release / go (push) Successful in 2m0s
release / Build signed APK (releases and dev) (push) Skipped
release / govulncheck (push) Successful in 21s
release / integration (push) Successful in 6m6s
release / android (push) Successful in 7m23s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 15s
release / Verify release artifacts (tag releases only) (push) Skipped
2026-10-06 23:11:35 -04:00
bvandeusen 1e9aed214b Merge pull request 'M462 security hardening and M464 loudness analysis (steps 1–3)' (#135) from dev into main
release / go (push) Successful in 2m19s
release / web (push) Successful in 1m43s
release / govulncheck (push) Successful in 35s
release / Build signed APK (releases and dev) (push) Skipped
release / android (push) Successful in 6m10s
release / integration (push) Successful in 19m35s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 2m24s
release / Verify release artifacts (tag releases only) (push) Skipped
2026-10-06 14:19:58 -04:00
bvandeusen 3638d1d822 Merge pull request 'M400: acoustic duplicate detection, history-preserving merge, and fingerprinting settings' (#134) from dev into main
release / Build signed APK (releases and dev) (push) Skipped
release / Build + push container image (push) Successful in 1m32s
release / Verify release artifacts (tag releases only) (push) Skipped
test-go / test (push) Successful in 1m46s
test-go / integration (push) Successful in 4m19s
test-web / test (push) Successful in 33s
2026-09-11 20:56:55 -04:00
bvandeusen 18618bd135 Backfill fingerprints for the existing library (M400 #3908) (#133)
release / Build signed APK (releases and dev) (push) Skipped
release / Build + push container image (push) Successful in 16s
release / Verify release artifacts (tag releases only) (push) Skipped
test-web / test (push) Successful in 49s
test-go / test (push) Successful in 1m11s
test-go / integration (push) Successful in 3m16s
2026-09-11 15:34:19 -04:00
bvandeusen d2985f3841 Acoustic fingerprints at ingest, a delete that can't lose history, and a reproducible candidate draw (#132)
release / Build signed APK (releases and dev) (push) Skipped
release / Build + push container image (push) Successful in 20s
release / Verify release artifacts (tag releases only) (push) Skipped
test-web / test (push) Successful in 1m0s
test-go / test (push) Successful in 1m20s
test-go / integration (push) Successful in 3m35s
android / Build + lint + test (push) Successful in 4m48s
2026-09-11 14:43:00 -04:00
bvandeusen f70df9f827 Recommendation relevance, the rollback unit, and a version that names what shipped (#131)
test-web / test (push) Successful in 1m5s
test-go / test (push) Successful in 1m33s
test-go / integration (push) Successful in 4m25s
android / Build + lint + test (push) Successful in 5m33s
release / Build signed APK (releases and dev) (push) Successful in 4m38s
release / Build + push container image (push) Successful in 16s
release / Verify release artifacts (tag releases only) (push) Successful in 2s
2026-09-10 23:37:57 -04:00
bvandeusen 439c8625d5 Fix the rebundle step: a non-matching grep must not kill it (#130)
test-go / test (push) Successful in 59s
test-go / integration (push) Successful in 3m1s
release / Build signed APK (releases and dev) (push) Successful in 4m41s
release / Build + push container image (push) Successful in 14s
release / Verify release artifacts (tag releases only) (push) Successful in 2s
2026-09-10 10:49:40 -04:00
bvandeusen 1d67c160b2 Versioning rework, a dev channel, and the miniplayer gap (#129)
release / Build signed APK (releases and dev) (push) Skipped
android / Build + lint + test (push) Failing after 4s
release / Build + push container image (push) Failing after 5s
release / Verify release artifacts (tag releases only) (push) Skipped
test-go / test (push) Successful in 1m24s
test-go / integration (push) Successful in 6m25s
2026-09-10 08:33:26 -04:00
bvandeusen 237380b122 New brand mark, and both clients stop fetching their fonts at runtime (#128)
test-web / test (push) Successful in 54s
android / Build + lint + test (push) Successful in 5m36s
release / Build signed APK (tag releases only) (push) Successful in 4m36s
release / Build + push container image (push) Successful in 24s
release / Verify release artifacts (tag releases only) (push) Successful in 2s
2026-09-09 14:36:15 -04:00
bvandeusen 4f077736b6 Merge PR #127: Sonos queue verification, cast double-download fix, stutter instrumentation
android / Build + lint + test (push) Successful in 4m16s
release / Build signed APK (tag releases only) (push) Successful in 5m25s
release / Build + push container image (push) Successful in 1m16s
release / Verify release artifacts (tag releases only) (push) Successful in 1s
2026-08-18 10:50:10 -04:00
bvandeusen 727f68950e Merge PR #126: missing-file lifecycle, UPnP stall recovery, Android browse parity, Flutter client removed
release / Build signed APK (tag releases only) (push) Skipped
test-web / test (push) Successful in 1m5s
test-go / test (push) Successful in 1m26s
release / Build + push container image (push) Successful in 1m38s
release / Verify release artifacts (tag releases only) (push) Skipped
android / Build + lint + test (push) Successful in 4m54s
test-go / integration (push) Successful in 5m19s
2026-08-17 16:28:13 -04:00
bvandeusen aa9f534f3c Merge pull request 'Genre index: sort A–Z, and repair casing damage at scan time' (#124) from dev into main
test-web / test (push) Successful in 49s
test-go / test (push) Successful in 1m9s
test-go / integration (push) Successful in 5m1s
release / Build signed APK (tag releases only) (push) Successful in 4m2s
release / Build + push container image (push) Successful in 14s
release / Verify release artifacts (tag releases only) (push) Successful in 2s
2026-08-07 21:40:33 -04:00
bvandeusen 011b4d9a9c Merge pull request 'ci(release): verify a tag release actually shipped its artifacts' (#123) from dev into main
release / Build signed APK (tag releases only) (push) Successful in 3m38s
release / Build + push container image (push) Successful in 1m31s
release / Verify release artifacts (tag releases only) (push) Successful in 2s
2026-08-07 08:32:48 -04:00
bvandeusen d5aa081157 Merge pull request 'Recommendation metrics: publish the margin of error on every delta' (#122) from dev into main
test-web / test (push) Successful in 52s
test-go / test (push) Successful in 1m10s
test-go / integration (push) Successful in 4m53s
release / Build signed APK (tag releases only) (push) Successful in 3m57s
release / Build + push container image (push) Successful in 1m37s
2026-08-06 21:50:41 -04:00
bvandeusen a99f855e98 Merge pull request 'Missing files: detect them, stop offering them, and follow them when they move' (#121) from dev into main
test-go / test (push) Successful in 56s
test-go / integration (push) Successful in 5m0s
release / Build signed APK (tag releases only) (push) Successful in 4m14s
release / Build + push container image (push) Successful in 15s
2026-08-06 20:40:39 -04:00
bvandeusen 7e4727fc49 Merge pull request 'Genre tags: read multi-value frames correctly, and repair existing rows' (#120) from dev into main
test-go / test (push) Successful in 57s
test-go / integration (push) Successful in 4m57s
release / Build signed APK (tag releases only) (push) Successful in 4m23s
release / Build + push container image (push) Successful in 1m39s
2026-08-05 22:10:41 -04:00
bvandeusen 1b7fa635d8 Merge pull request 'Silent self-update, active sessions with real client IPs, genre/year browsing, handoff fix' (#119) from dev into main
test-web / test (push) Successful in 1m3s
test-go / test (push) Successful in 1m13s
test-go / integration (push) Successful in 5m29s
android / Build + lint + test (push) Successful in 5m34s
release / Build signed APK (tag releases only) (push) Successful in 5m5s
release / Build + push container image (push) Successful in 16s
2026-08-05 15:14:48 -04:00
bvandeusen 57d2299180 Merge pull request 'Queue row gestures: album art as grab surface + swipe-to-remove' (#118) from dev into main
test-web / test (push) Successful in 48s
android / Build + lint + test (push) Successful in 5m15s
release / Build signed APK (tag releases only) (push) Successful in 4m38s
release / Build + push container image (push) Successful in 1m48s
2026-08-04 11:37:30 -04:00
bvandeusen fa7ea41ccf Merge pull request 'Minstrel gets a mark — favicon, header lockup, Android adaptive icon' (#117) from dev into main
test-web / test (push) Successful in 47s
android / Build + lint + test (push) Successful in 4m37s
release / Build signed APK (tag releases only) (push) Successful in 8m36s
release / Build + push container image (push) Successful in 1m37s
2026-08-03 20:52:27 -04:00
bvandeusen 324059b2bd Merge pull request 'Discover request surface — taste-aware, rotating, snoozable, tag-targeted (milestone #268)' (#116) from dev into main
test-web / test (push) Successful in 1m5s
test-go / test (push) Successful in 1m30s
android / Build + lint + test (push) Successful in 5m1s
test-go / integration (push) Successful in 5m29s
release / Build signed APK (tag releases only) (push) Successful in 4m21s
release / Build + push container image (push) Successful in 17s
2026-08-03 08:38:24 -04:00
bvandeusen 1138d75a45 Merge pull request 'Playlist-track atomic replace + ci-requirements true-up' (#115) from dev into main
release / Build signed APK (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m33s
android / Build + lint + test (push) Successful in 4m30s
2026-08-01 12:23:37 -04:00
195 changed files with 1750 additions and 2145 deletions
+7 -6
View File
@@ -295,12 +295,13 @@ jobs:
- name: Install deps
run: npm ci
# What ships to browsers: `dependencies` and the runtime they pull in
# (svelte, devalue). Build and test tooling (vite, vitest, tailwind,
# kit's dev server) is left out because none of it reaches a user, and
# its open advisories need major-version upgrades tracked separately.
- name: npm audit (shipped dependencies)
run: npm audit --omit=dev --audit-level=moderate
# The whole tree, build and test tooling included. Until #5021 this
# audited only what ships to browsers (`--omit=dev`), because vite,
# vitest, tailwind and kit carried advisories that needed major
# upgrades. Those upgrades landed and the full tree audits clean, so
# the tooling that builds the shipped bundle is held to the same bar.
- name: npm audit (all dependencies)
run: npm audit --audit-level=moderate
- name: Type-check + svelte-check
run: npm run check
+1 -1
View File
@@ -94,7 +94,7 @@ Keystore, so a copy of the app's files doesn't yield a usable session.
Nothing is published unless every check passes: the Go, integration, web and
Android test suites, `govulncheck` against the toolchain that builds the
image, and `npm audit` on the packages that ship to the browser. See
image, and `npm audit` on every web dependency, build tooling included. See
`.gitea/workflows/release.yml`.
## Reporting a problem
+668 -1062
View File
File diff suppressed because it is too large Load Diff
+11 -8
View File
@@ -13,24 +13,27 @@
},
"devDependencies": {
"@sveltejs/adapter-static": "^4.0.0",
"@sveltejs/kit": "^2.8.0",
"@sveltejs/kit": "^3.0.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@tailwindcss/vite": "^4.3.3",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/svelte": "^5.3.1",
"@types/node": "^25.9.1",
"autoprefixer": "^10.5.0",
"jsdom": "^25.0.1",
"postcss": "^8.5.15",
"svelte": "^5.1.9",
"svelte-check": "^4.4.8",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3",
"vite": "^8.0.0",
"svelte": "^5.57.1",
"svelte-check": "^4.7.5",
"tailwindcss": "^4.3.3",
"typescript": "^6.0.0",
"vite": "^8.0.12",
"vitest": "^5.0.0"
},
"dependencies": {
"@neodrag/svelte": "^2.3.3",
"@tanstack/svelte-query": "^5.90.2",
"lucide-svelte": "^1.0.1"
},
"imports": {
"#lib/*": "./src/lib/*",
"#test-utils/*": "./src/test-utils/*"
}
}
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
+71 -4
View File
@@ -1,8 +1,75 @@
@import './lib/styles/tokens.generated.css';
@import './lib/styles/tokens.generated.css' layer(base);
@tailwind base;
@tailwind components;
@tailwind utilities;
@import 'tailwindcss';
/* The FabledSword tokens as Tailwind's theme. `inline` makes each utility emit
* the var(--fs-*) itself, so a theme swap that redefines the tokens reaches the
* utilities without going through a second variable. */
@theme inline {
--color-background: var(--fs-obsidian);
--color-surface: var(--fs-iron);
--color-surface-hover: var(--fs-slate);
--color-border: var(--fs-pewter);
--color-text-primary: var(--fs-parchment);
--color-text-secondary: var(--fs-vellum);
--color-text-muted: var(--fs-ash);
--color-action-primary: var(--fs-moss);
--color-action-secondary: var(--fs-bronze);
--color-action-destructive: var(--fs-oxblood);
--color-action-fg: var(--fs-on-action);
--color-accent: var(--fs-accent);
--color-accent-tint: color-mix(in srgb, var(--fs-accent) 12%, transparent);
--color-accent-fg: var(--fs-accent-fg);
--color-warning: var(--fs-warning);
--color-warning-fg: var(--fs-warning-fg);
--color-error: var(--fs-error);
--color-error-fg: var(--fs-error-fg);
--color-info: var(--fs-info);
--color-info-fg: var(--fs-info-fg);
--radius-sm: var(--fs-radius-sm);
--radius-md: var(--fs-radius-md);
--radius-lg: var(--fs-radius-lg);
--radius-xl: var(--fs-radius-xl);
--font-display: var(--fs-font-display);
--font-sans: var(--fs-font-body);
--font-mono: var(--fs-font-mono);
}
/* Tailwind 3's preflight defaults that 4 changed (#5021). Kept so the upgrade
* changes nothing on screen; each can go once the elements relying on it name
* the value themselves.
* - A border with no colour utility was gray-200; 4 makes it currentcolor.
* - Placeholder text was gray-400; 4 makes it the text colour at 50%.
* - Buttons had a pointer cursor; 4 leaves the browser default. */
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
input::placeholder,
textarea::placeholder {
color: var(--color-gray-400);
}
button:not(:disabled),
[role='button']:not(:disabled) {
cursor: pointer;
}
}
:root {
color-scheme: dark;
+2 -2
View File
@@ -83,7 +83,7 @@ describe('api.get/post/del', () => {
describe('apiFetch 401 interceptor', () => {
test('401 response triggers auth.logout({silent:true}) and still throws', async () => {
const logoutSpy = vi.fn();
vi.doMock('$lib/auth/store.svelte', () => ({
vi.doMock('#lib/auth/store.svelte.js', () => ({
logout: logoutSpy,
login: vi.fn(),
bootstrap: vi.fn(),
@@ -97,6 +97,6 @@ describe('apiFetch 401 interceptor', () => {
status: 401
});
expect(logoutSpy).toHaveBeenCalledWith({ silent: true });
vi.doUnmock('$lib/auth/store.svelte');
vi.doUnmock('#lib/auth/store.svelte.js');
});
});
+1 -1
View File
@@ -44,7 +44,7 @@ export async function apiFetch(path: string, init?: RequestInit): Promise<unknow
// Lazy import: auth/store imports from this file, so a top-level
// import would be circular. By the time any 401 actually happens
// at runtime, both modules have finished loading.
const { logout } = await import('$lib/auth/store.svelte');
const { logout } = await import('#lib/auth/store.svelte.js');
await logout({ silent: true });
}
// Two error envelope shapes ship in this codebase today:
+4 -4
View File
@@ -1,10 +1,10 @@
import { api, type User, type LoginResponse } from '$lib/api/client';
import { putMyTimezone } from '$lib/api/me';
import { queryClient } from '$lib/query/client';
import { api, type User, type LoginResponse } from '#lib/api/client.js';
import { putMyTimezone } from '#lib/api/me.js';
import { queryClient } from '#lib/query/client.js';
import { signalSessionEnd } from './sessionEnd.svelte';
import { user, setUser } from './user.svelte';
// Re-export so existing `import { user } from '$lib/auth/store.svelte'`
// Re-export so existing `import { user } from '#lib/auth/store.svelte'`
// callers keep working. New code can import directly from auth/user.svelte.
export { user };
+4 -4
View File
@@ -1,14 +1,14 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { apiClientMock } from '../../test-utils/mocks/client';
vi.mock('$lib/api/client', () => apiClientMock());
vi.mock('#lib/api/client.js', () => apiClientMock());
vi.mock('$lib/query/client', () => ({
vi.mock('#lib/query/client.js', () => ({
queryClient: { clear: vi.fn() }
}));
import { api } from '$lib/api/client';
import { queryClient } from '$lib/query/client';
import { api } from '#lib/api/client.js';
import { queryClient } from '#lib/query/client.js';
import { bootstrap, login, logout, user } from './store.svelte';
beforeEach(() => {
+1 -1
View File
@@ -4,7 +4,7 @@
// component that needs the current user can import `user` from either
// this module or auth/store — auth/store re-exports it for backward
// compatibility.
import type { User } from '$lib/api/client';
import type { User } from '#lib/api/client.js';
let _user = $state<User | null>(null);
+6 -6
View File
@@ -7,9 +7,9 @@
updateAcoustIDSettings,
type AcoustIDOverview,
type AcoustIDUnsettled
} from '$lib/api/admin';
import { errMessage } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/admin.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// AcoustID recording-id lookup (M401): fills the MusicBrainz ids that
// untagged tracks lack, so the similarity arms can see them. Off until the
@@ -125,7 +125,7 @@
const rowClass = 'flex items-center justify-between gap-4 py-2';
const inputClass =
'rounded-md border border-border bg-background px-3 py-1.5 text-sm text-text-primary ' +
'placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent';
'placeholder:text-text-muted focus:outline-hidden focus:ring-2 focus:ring-accent';
</script>
<section class="space-y-4 rounded-xl border border-border bg-surface p-5" data-testid="acoustid-card">
@@ -238,7 +238,7 @@
<button
type="button"
class="rounded-md bg-action-secondary px-4 py-2 text-sm text-action-fg hover:opacity-90
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent
disabled:cursor-not-allowed disabled:opacity-50"
disabled={!dirty || saving || !scoreOk}
onclick={save}
@@ -293,7 +293,7 @@
<button
type="button"
class="rounded-md border border-border px-4 py-2 text-sm text-text-primary hover:bg-background
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent
disabled:cursor-not-allowed disabled:opacity-50"
disabled={!overview.ready || overview.status.running || starting}
title={overview.ready ? undefined : 'Switch the lookup on and add a key first'}
+4 -4
View File
@@ -1,15 +1,15 @@
import { afterEach, describe, expect, test, vi } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/svelte";
import type { AcoustIDOverview } from "$lib/api/admin";
import type { AcoustIDOverview } from "#lib/api/admin.js";
vi.mock("$lib/api/admin", () => ({
vi.mock("#lib/api/admin.js", () => ({
getAcoustID: vi.fn(),
updateAcoustIDSettings: vi.fn(),
runAcoustIDLookup: vi.fn(),
listUnsettledAcoustID: vi.fn(),
}));
vi.mock("$lib/stores/toast.svelte", () => ({ pushToast: vi.fn() }));
vi.mock("#lib/stores/toast.svelte.js", () => ({ pushToast: vi.fn() }));
import AcoustIDCard from "./AcoustIDCard.svelte";
import {
@@ -17,7 +17,7 @@ import {
listUnsettledAcoustID,
runAcoustIDLookup,
updateAcoustIDSettings,
} from "$lib/api/admin";
} from "#lib/api/admin.js";
const base: AcoustIDOverview = {
settings: { enabled: true, min_score: 0.85, api_key_set: true },
+5 -5
View File
@@ -6,9 +6,9 @@
revokeSession,
revokeOtherSessions,
type ActiveSession
} from '$lib/api/me';
import { errCode } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/me.js';
import { errCode } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// Self-contained: nothing else in the app reads this data, so it holds its
// own state and reloads explicitly rather than joining the query cache.
@@ -143,7 +143,7 @@
<li class="flex items-start gap-3 py-3">
<MonitorSmartphone
size={18}
class="mt-0.5 flex-shrink-0 text-text-secondary"
class="mt-0.5 shrink-0 text-text-secondary"
aria-hidden="true"
/>
<div class="min-w-0 flex-1">
@@ -173,7 +173,7 @@
{#if !s.current}
<button
type="button"
class="flex-shrink-0 rounded border border-border px-2 py-1 text-sm
class="shrink-0 rounded border border-border px-2 py-1 text-sm
hover:bg-surface-hover focus-visible:ring-2 focus-visible:ring-accent
disabled:opacity-50"
disabled={busy}
@@ -6,13 +6,13 @@ const listSessions = vi.fn();
const revokeSession = vi.fn();
const revokeOtherSessions = vi.fn();
vi.mock('$lib/api/me', () => ({
vi.mock('#lib/api/me.js', () => ({
listSessions: (...a: unknown[]) => listSessions(...a),
revokeSession: (...a: unknown[]) => revokeSession(...a),
revokeOtherSessions: (...a: unknown[]) => revokeOtherSessions(...a)
}));
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
type Row = {
id: string;
@@ -1,12 +1,12 @@
<script lang="ts">
import { Plus } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import { createPlaylistsQuery, appendTracks, createPlaylist } from '$lib/api/playlists';
import { qk } from '$lib/api/queries';
import { errMessage } from '$lib/api/errors';
import { createPlaylistsQuery, appendTracks, createPlaylist } from '#lib/api/playlists.js';
import { qk } from '#lib/api/queries.js';
import { errMessage } from '#lib/api/errors.js';
import TrackMenuItem from './TrackMenuItem.svelte';
import TrackMenuDivider from './TrackMenuDivider.svelte';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
let {
tracks,
@@ -1,7 +1,7 @@
import { describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import { readable } from 'svelte/store';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
const playlistsData = vi.hoisted(() => ({
owned: [
@@ -43,11 +43,11 @@ const playlistsData = vi.hoisted(() => ({
public: [] as unknown[]
}));
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: { value: { id: 'u-self', username: 'me', is_admin: false } }
}));
vi.mock('$lib/api/playlists', () => ({
vi.mock('#lib/api/playlists.js', () => ({
createPlaylistsQuery: () =>
readable({ data: playlistsData, isPending: false, isError: false }),
appendTracks: vi.fn().mockResolvedValue({ id: 'p1', tracks: [] }),
@@ -86,7 +86,7 @@ describe('AddToPlaylistMenu', () => {
test('clicking a playlist appends and closes', async () => {
const onClose = vi.fn();
const { appendTracks } = await import('$lib/api/playlists');
const { appendTracks } = await import('#lib/api/playlists.js');
render(AddToPlaylistMenu, { props: { tracks: [track], onClose } });
await fireEvent.click(screen.getByRole('menuitem', { name: /A-list/ }));
await waitFor(() => expect(onClose).toHaveBeenCalled());
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, test, vi } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import { pageUrlModule } from '$test-utils/mocks/appState';
import { pageUrlModule } from '#test-utils/mocks/appState.js';
const state = vi.hoisted(() => ({
pageUrl: new URL('http://localhost/admin')
+4 -4
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import type { AlbumRef, AlbumDetail } from '$lib/api/types';
import { api } from '$lib/api/client';
import { enqueueTracks, playQueue } from '$lib/player/store.svelte';
import type { AlbumRef, AlbumDetail } from '#lib/api/types.js';
import { api } from '#lib/api/client.js';
import { enqueueTracks, playQueue } from '#lib/player/store.svelte.js';
import { Play } from 'lucide-svelte';
import AlbumMenu from './AlbumMenu.svelte';
import CardActionCluster from './CardActionCluster.svelte';
@@ -31,7 +31,7 @@
>
<div
class="art-wrap relative aspect-square w-full overflow-hidden rounded-md
shadow-sm transition-all duration-150
shadow-xs transition-all duration-150
group-hover:shadow-lg group-hover:ring-1 group-hover:ring-accent/40"
>
<Cover
+10 -10
View File
@@ -1,27 +1,27 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import type { AlbumRef, AlbumDetail, TrackRef } from '$lib/api/types';
import { FALLBACK_COVER } from '$lib/media/covers';
import type { AlbumRef, AlbumDetail, TrackRef } from '#lib/api/types.js';
import { FALLBACK_COVER } from '#lib/media/covers.js';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { apiClientMock } from '../../test-utils/mocks/client';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
vi.mock('$lib/api/client', () => apiClientMock());
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/api/client.js', () => apiClientMock());
vi.mock('#lib/player/store.svelte.js', () => ({
enqueueTracks: vi.fn(),
playQueue: vi.fn()
}));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
vi.mock('$lib/api/albums', async (orig) => {
vi.mock('#lib/api/albums.js', async (orig) => {
const actual = (await orig()) as Record<string, unknown>;
return { ...actual, listAlbumTracks: vi.fn().mockResolvedValue([]) };
});
vi.mock('$lib/api/playlists', async (orig) => {
vi.mock('#lib/api/playlists.js', async (orig) => {
const { readable } = await import('svelte/store');
const actual = (await orig()) as Record<string, unknown>;
return {
@@ -34,8 +34,8 @@ vi.mock('$lib/api/playlists', async (orig) => {
});
import AlbumCard from './AlbumCard.svelte';
import { api } from '$lib/api/client';
import { enqueueTracks, playQueue } from '$lib/player/store.svelte';
import { api } from '#lib/api/client.js';
import { enqueueTracks, playQueue } from '#lib/player/store.svelte.js';
const album: AlbumRef = {
id: 'xyz',
+4 -4
View File
@@ -2,13 +2,13 @@
import { MoreVertical, Heart, HeartOff, ListMusic, Plus, Disc3 } from 'lucide-svelte';
import { goto } from '$app/navigation';
import { useQueryClient } from '@tanstack/svelte-query';
import { createLikedIdsQuery, likeEntity, unlikeEntity } from '$lib/api/likes';
import { listAlbumTracks } from '$lib/api/albums';
import { enqueueTracks } from '$lib/player/store.svelte';
import { createLikedIdsQuery, likeEntity, unlikeEntity } from '#lib/api/likes.js';
import { listAlbumTracks } from '#lib/api/albums.js';
import { enqueueTracks } from '#lib/player/store.svelte.js';
import AddToPlaylistMenu from './AddToPlaylistMenu.svelte';
import TrackMenuItem from './TrackMenuItem.svelte';
import TrackMenuDivider from './TrackMenuDivider.svelte';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
let {
albumId,
+9 -9
View File
@@ -2,28 +2,28 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { emptyPlaylistsMock } from '../../test-utils/mocks/playlists';
import type { TrackRef } from '$lib/api/types';
import { makeTracks } from '$test-utils/fixtures/track';
import type { TrackRef } from '#lib/api/types.js';
import { makeTracks } from '#test-utils/fixtures/track.js';
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$lib/api/albums', async (orig) => {
vi.mock('#lib/api/albums.js', async (orig) => {
const actual = (await orig()) as Record<string, unknown>;
return { ...actual, listAlbumTracks: vi.fn().mockResolvedValue([]) };
});
vi.mock('$lib/api/playlists', () => emptyPlaylistsMock());
vi.mock('#lib/api/playlists.js', () => emptyPlaylistsMock());
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
enqueueTracks: vi.fn()
}));
import AlbumMenu from './AlbumMenu.svelte';
import { enqueueTracks } from '$lib/player/store.svelte';
import { listAlbumTracks } from '$lib/api/albums';
import { likeEntity } from '$lib/api/likes';
import { enqueueTracks } from '#lib/player/store.svelte.js';
import { listAlbumTracks } from '#lib/api/albums.js';
import { likeEntity } from '#lib/api/likes.js';
import { goto } from '$app/navigation';
const TRACKS: TrackRef[] = makeTracks(2, {
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import type { ApiError } from '$lib/api/client';
import type { ApiError } from '#lib/api/client.js';
let {
error,
+4 -4
View File
@@ -1,12 +1,12 @@
<script lang="ts">
import type { ArtistRef, TrackRef } from '$lib/api/types';
import { api } from '$lib/api/client';
import { playQueue, enqueueTracks } from '$lib/player/store.svelte';
import type { ArtistRef, TrackRef } from '#lib/api/types.js';
import { api } from '#lib/api/client.js';
import { playQueue, enqueueTracks } from '#lib/player/store.svelte.js';
import { Play } from 'lucide-svelte';
import ArtistMenu from './ArtistMenu.svelte';
import CardActionCluster from './CardActionCluster.svelte';
import Cover from './Cover.svelte';
import { listArtistTracks } from '$lib/api/artists';
import { listArtistTracks } from '#lib/api/artists.js';
let { artist }: { artist: ArtistRef } = $props();
+10 -10
View File
@@ -2,21 +2,21 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { apiClientMock } from '../../test-utils/mocks/client';
import type { ArtistRef, TrackRef } from '$lib/api/types';
import { makeTrack, makeTracks } from '$test-utils/fixtures/track';
import type { ArtistRef, TrackRef } from '#lib/api/types.js';
import { makeTrack, makeTracks } from '#test-utils/fixtures/track.js';
vi.mock('$lib/api/client', () => apiClientMock());
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/api/client.js', () => apiClientMock());
vi.mock('#lib/player/store.svelte.js', () => ({
playQueue: vi.fn(),
enqueueTracks: vi.fn()
}));
vi.mock('$lib/api/artists', () => ({
vi.mock('#lib/api/artists.js', () => ({
listArtistTracks: vi.fn().mockResolvedValue([])
}));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
import ArtistCard from './ArtistCard.svelte';
import { api } from '$lib/api/client';
import { playQueue } from '$lib/player/store.svelte';
import { api } from '#lib/api/client.js';
import { playQueue } from '#lib/player/store.svelte.js';
const artist: ArtistRef = {
id: 'art-1',
@@ -82,7 +82,7 @@ describe('ArtistCard', () => {
const fakeTracks = [
makeTrack({ artist_id: 'art-1', artist_name: 'BoC' })
];
const { listArtistTracks } = await import('$lib/api/artists');
const { listArtistTracks } = await import('#lib/api/artists.js');
(listArtistTracks as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce(fakeTracks);
render(ArtistCard, { props: { artist } });
const btn = screen.getByRole('button', { name: /Add Boards of Canada to queue/i });
@@ -90,7 +90,7 @@ describe('ArtistCard', () => {
// Allow promise microtask to flush.
await new Promise((r) => setTimeout(r, 0));
expect(listArtistTracks).toHaveBeenCalledWith('art-1');
const { enqueueTracks } = await import('$lib/player/store.svelte');
const { enqueueTracks } = await import('#lib/player/store.svelte.js');
expect(enqueueTracks).toHaveBeenCalledWith(fakeTracks);
});
});
+4 -4
View File
@@ -1,13 +1,13 @@
<script lang="ts">
import { MoreVertical, Heart, HeartOff, ListMusic, Plus } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import { createLikedIdsQuery, likeEntity, unlikeEntity } from '$lib/api/likes';
import { listArtistTracks } from '$lib/api/artists';
import { enqueueTracks } from '$lib/player/store.svelte';
import { createLikedIdsQuery, likeEntity, unlikeEntity } from '#lib/api/likes.js';
import { listArtistTracks } from '#lib/api/artists.js';
import { enqueueTracks } from '#lib/player/store.svelte.js';
import AddToPlaylistMenu from './AddToPlaylistMenu.svelte';
import TrackMenuItem from './TrackMenuItem.svelte';
import TrackMenuDivider from './TrackMenuDivider.svelte';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
let {
artistId,
+9 -9
View File
@@ -2,27 +2,27 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { emptyPlaylistsMock } from '../../test-utils/mocks/playlists';
import type { TrackRef } from '$lib/api/types';
import { makeTracks } from '$test-utils/fixtures/track';
import type { TrackRef } from '#lib/api/types.js';
import { makeTracks } from '#test-utils/fixtures/track.js';
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$lib/api/artists', () => ({
vi.mock('#lib/api/artists.js', () => ({
listArtistTracks: vi.fn().mockResolvedValue([])
}));
vi.mock('$lib/api/playlists', () => emptyPlaylistsMock());
vi.mock('#lib/api/playlists.js', () => emptyPlaylistsMock());
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
enqueueTracks: vi.fn()
}));
import ArtistMenu from './ArtistMenu.svelte';
import { enqueueTracks } from '$lib/player/store.svelte';
import { listArtistTracks } from '$lib/api/artists';
import { likeEntity, unlikeEntity } from '$lib/api/likes';
import { enqueueTracks } from '#lib/player/store.svelte.js';
import { listArtistTracks } from '#lib/api/artists.js';
import { likeEntity, unlikeEntity } from '#lib/api/likes.js';
const TRACKS: TrackRef[] = makeTracks(2, {
album_id: 'al1', artist_id: 'a1', artist_name: 'Test'
@@ -1,8 +1,8 @@
<script lang="ts">
import { Plus } from 'lucide-svelte';
import type { TrackRef } from '$lib/api/types';
import { playQueue, enqueueTrack } from '$lib/player/store.svelte';
import { coverUrl } from '$lib/media/covers';
import type { TrackRef } from '#lib/api/types.js';
import { playQueue, enqueueTrack } from '#lib/player/store.svelte.js';
import { coverUrl } from '#lib/media/covers.js';
import TrackMenu from './TrackMenu.svelte';
import LikeButton from './LikeButton.svelte';
import Cover from './Cover.svelte';
@@ -43,7 +43,7 @@
class="flex w-full items-center gap-2 rounded-md p-1 pr-24 text-left
hover:bg-surface-hover focus-visible:ring-2 focus-visible:ring-accent"
>
<div class="h-12 w-12 flex-shrink-0 overflow-hidden rounded bg-surface-hover">
<div class="h-12 w-12 shrink-0 overflow-hidden rounded bg-surface-hover">
<Cover src={cover} />
</div>
<div class="min-w-0 flex-1">
@@ -1,31 +1,31 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { emptyPlaylistsMock } from '../../test-utils/mocks/playlists';
import { emptyQuarantineMock } from '../../test-utils/mocks/quarantine';
import { makeTracks } from '$test-utils/fixtures/track';
import { makeTracks } from '#test-utils/fixtures/track.js';
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$lib/api/quarantine', () => emptyQuarantineMock());
vi.mock('#lib/api/quarantine.js', () => emptyQuarantineMock());
vi.mock('$lib/api/playlists', () => emptyPlaylistsMock());
vi.mock('#lib/api/playlists.js', () => emptyPlaylistsMock());
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: { get value() { return { id: 'u1', username: 'u', is_admin: false }; } }
}));
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
playQueue: vi.fn(),
enqueueTrack: vi.fn(),
playNext: vi.fn()
}));
import CompactTrackCard from './CompactTrackCard.svelte';
import { playQueue, enqueueTrack } from '$lib/player/store.svelte';
import { playQueue, enqueueTrack } from '#lib/player/store.svelte.js';
const titles = ['First', 'Second', 'Third'];
const tracks: TrackRef[] = makeTracks(3, { artist_name: 'Artist' }).map((t, i) => ({
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { FALLBACK_COVER } from '$lib/media/covers';
import { FALLBACK_COVER } from '#lib/media/covers.js';
import { Disc3 } from 'lucide-svelte';
// Shared cover-artwork renderer. The web counterpart of Android's
@@ -86,7 +86,7 @@
<button
type="button"
aria-label={`Bring ${title} back`}
class="rounded-md border border-border px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-accent"
class="rounded-md border border-border px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover focus:outline-hidden focus:ring-2 focus:ring-accent"
onclick={() => onUnsnooze?.()}
>
Undo
@@ -112,7 +112,7 @@
type="button"
aria-label={`Not right now — hide ${title} for a while`}
title="Not right now"
class="rounded-md border border-border p-1.5 text-text-secondary hover:bg-surface-hover hover:text-text-primary focus:outline-none focus:ring-2 focus:ring-accent"
class="rounded-md border border-border p-1.5 text-text-secondary hover:bg-surface-hover hover:text-text-primary focus:outline-hidden focus:ring-2 focus:ring-accent"
onclick={() => onSnooze?.()}
>
<Clock size={16} strokeWidth={1} />
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, test, vi } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import { pageUrlModule } from '$test-utils/mocks/appState';
import { pageUrlModule } from '#test-utils/mocks/appState.js';
const state = vi.hoisted(() => ({
pageUrl: new URL('http://localhost/discover')
@@ -5,9 +5,9 @@
getFingerprintSettings,
updateFingerprintSettings,
type FingerprintSettings
} from '$lib/api/admin';
import { errMessage } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/admin.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// How Minstrel fingerprints tracks and when it looks for duplicates (#3913).
// Lives on the Duplicates page, beside the results these settings shape.
@@ -83,7 +83,7 @@
const inputClass =
'mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm text-text-primary ' +
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent';
'focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent';
</script>
<section class="space-y-4 rounded-xl border border-border bg-surface p-5">
@@ -183,7 +183,7 @@
class="flex items-start gap-2 rounded-md bg-surface-hover px-3 py-2 text-xs text-text-secondary"
data-testid="length-warning"
>
<TriangleAlert size={14} class="mt-0.5 flex-shrink-0 text-action-destructive" aria-hidden="true" />
<TriangleAlert size={14} class="mt-0.5 shrink-0 text-action-destructive" aria-hidden="true" />
<span>
Saving re-fingerprints {libraryTotal > 0
? `all ${libraryTotal.toLocaleString()} tracks`
@@ -206,7 +206,7 @@
<button
type="button"
class="rounded-md bg-action-secondary px-4 py-2 text-sm text-action-fg hover:opacity-90
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent
disabled:cursor-not-allowed disabled:opacity-50"
disabled={!dirty || saving || problems.length > 0}
onclick={save}
@@ -1,18 +1,18 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import type { FingerprintSettings } from '$lib/api/admin';
import { ERROR_COPY } from '$lib/api/error-copy';
import type { FingerprintSettings } from '#lib/api/admin.js';
import { ERROR_COPY } from '#lib/api/error-copy.js';
vi.mock('$lib/api/admin', () => ({
vi.mock('#lib/api/admin.js', () => ({
getFingerprintSettings: vi.fn(),
updateFingerprintSettings: vi.fn()
}));
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
import FingerprintSettingsCard from './FingerprintSettingsCard.svelte';
import { getFingerprintSettings, updateFingerprintSettings } from '$lib/api/admin';
import { pushToast } from '$lib/stores/toast.svelte';
import { getFingerprintSettings, updateFingerprintSettings } from '#lib/api/admin.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
const base: FingerprintSettings = {
enabled: true,
+6 -6
View File
@@ -2,10 +2,10 @@
import { untrack } from 'svelte';
import { Flag } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import { flagTrack } from '$lib/api/quarantine';
import { errCode } from '$lib/api/errors';
import { qk } from '$lib/api/queries';
import type { TrackRef, LidarrQuarantineReason } from '$lib/api/types';
import { flagTrack } from '#lib/api/quarantine.js';
import { errCode } from '#lib/api/errors.js';
import { qk } from '#lib/api/queries.js';
import type { TrackRef, LidarrQuarantineReason } from '#lib/api/types.js';
let {
track,
@@ -59,7 +59,7 @@
<span class="block text-xs text-text-secondary">Reason</span>
<select
bind:value={reason}
class="mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent"
class="mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm text-text-primary focus:outline-hidden focus:ring-2 focus:ring-accent"
>
<option value="bad_rip">Bad rip</option>
<option value="wrong_file">Wrong file</option>
@@ -74,7 +74,7 @@
bind:value={notes}
maxlength="200"
placeholder="What's wrong with it?"
class="mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-accent"
class="mt-1 w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm text-text-primary placeholder:text-text-muted focus:outline-hidden focus:ring-2 focus:ring-accent"
rows="2"
></textarea>
</label>
+3 -3
View File
@@ -1,7 +1,7 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import FlagPopover from './FlagPopover.svelte';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
const invalidateMock = vi.fn();
vi.mock('@tanstack/svelte-query', async (orig) => {
@@ -12,11 +12,11 @@ vi.mock('@tanstack/svelte-query', async (orig) => {
};
});
vi.mock('$lib/api/quarantine', () => ({
vi.mock('#lib/api/quarantine.js', () => ({
flagTrack: vi.fn().mockResolvedValue({ track_id: 't1', reason: 'bad_rip' })
}));
import { flagTrack } from '$lib/api/quarantine';
import { flagTrack } from '#lib/api/quarantine.js';
const track = makeTrack();
+5 -5
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import type { HistoryEvent } from '$lib/api/history';
import { playQueue } from '$lib/player/store.svelte';
import { coverUrl } from '$lib/media/covers';
import type { HistoryEvent } from '#lib/api/history.js';
import { playQueue } from '#lib/player/store.svelte.js';
import { coverUrl } from '#lib/media/covers.js';
let { event } = $props<{ event: HistoryEvent }>();
@@ -49,7 +49,7 @@
<img
src={coverUrl(event.track.album_id)}
alt=""
class="h-11 w-11 flex-shrink-0 rounded object-cover"
class="h-11 w-11 shrink-0 rounded object-cover"
loading="lazy"
/>
<div class="min-w-0 flex-1">
@@ -58,7 +58,7 @@
{event.track.artist_name} · {event.track.album_title}
</div>
</div>
<span title={tooltip} class="flex-shrink-0 text-xs text-text-secondary">
<span title={tooltip} class="shrink-0 text-xs text-text-secondary">
{timestamp}
</span>
</button>
+3 -3
View File
@@ -1,12 +1,12 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import HistoryRow from './HistoryRow.svelte';
import type { HistoryEvent } from '$lib/api/history';
import { makeTrack } from '$test-utils/fixtures/track';
import type { HistoryEvent } from '#lib/api/history.js';
import { makeTrack } from '#test-utils/fixtures/track.js';
const playQueue = vi.fn();
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
playQueue: (...args: unknown[]) => playQueue(...args)
}));
@@ -67,7 +67,7 @@
each shelf a clear chapter break without adding chrome. -->
<h2
class="font-display text-2xl font-medium text-text-primary
flex items-baseline gap-3 after:h-[2px] after:w-12 after:flex-shrink-0
flex items-baseline gap-3 after:h-[2px] after:w-12 after:shrink-0
after:rounded-full after:bg-accent/60 after:content-['']"
>{title}</h2>
{:else}
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { useQueryClient } from '@tanstack/svelte-query';
import { createLikedIdsQuery, likeEntity, unlikeEntity, type EntityKind } from '$lib/api/likes';
import { createLikedIdsQuery, likeEntity, unlikeEntity, type EntityKind } from '#lib/api/likes.js';
let {
entityType,
+2 -2
View File
@@ -6,14 +6,14 @@ const cacheState = vi.hoisted(() => ({
data: { track_ids: ['t1'] as string[], album_ids: [] as string[], artist_ids: [] as string[] }
}));
vi.mock('$lib/api/likes', () => ({
vi.mock('#lib/api/likes.js', () => ({
createLikedIdsQuery: () => readable({ data: cacheState.data, isPending: false, isError: false }),
likeEntity: vi.fn().mockResolvedValue(undefined),
unlikeEntity: vi.fn().mockResolvedValue(undefined)
}));
import LikeButton from './LikeButton.svelte';
import { likeEntity, unlikeEntity } from '$lib/api/likes';
import { likeEntity, unlikeEntity } from '#lib/api/likes.js';
afterEach(() => {
vi.clearAllMocks();
@@ -6,9 +6,9 @@
updateLoudnessSettings,
type LoudnessCoverage,
type LoudnessSettings
} from '$lib/api/admin';
import { errMessage } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/admin.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// Loudness analysis (#4995): the background measurement that loudness
// normalization levels playback from, its progress, and its knobs. The
@@ -79,7 +79,7 @@
const inputClass =
'mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm text-text-primary ' +
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent';
'focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent';
</script>
<section class="space-y-4 rounded border border-border bg-surface p-4">
@@ -185,7 +185,7 @@
<button
type="button"
class="rounded-md bg-action-secondary px-4 py-2 text-sm text-action-fg hover:opacity-90
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent
disabled:cursor-not-allowed disabled:opacity-50"
disabled={!dirty || saving || !valid}
onclick={save}
@@ -1,18 +1,18 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import type { LoudnessCoverage, LoudnessSettings } from '$lib/api/admin';
import type { LoudnessCoverage, LoudnessSettings } from '#lib/api/admin.js';
vi.mock('$lib/api/admin', () => ({
vi.mock('#lib/api/admin.js', () => ({
getLoudnessSettings: vi.fn(),
updateLoudnessSettings: vi.fn(),
getLoudnessCoverage: vi.fn()
}));
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
import LoudnessSettingsCard from './LoudnessSettingsCard.svelte';
import { getLoudnessCoverage, getLoudnessSettings, updateLoudnessSettings } from '$lib/api/admin';
import { pushToast } from '$lib/stores/toast.svelte';
import { getLoudnessCoverage, getLoudnessSettings, updateLoudnessSettings } from '#lib/api/admin.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
const base: LoudnessSettings = { enabled: true, backfill_concurrency: 2, leveled_cache_mb: 2048 };
const coverage: LoudnessCoverage = {
@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Smartphone } from 'lucide-svelte';
import { api } from '$lib/api/client';
import { api } from '#lib/api/client.js';
// Polls /api/client/version once on mount. Renders nothing on 404
// (no bundled APK — graceful degradation in dev / pre-CI images)
@@ -5,8 +5,8 @@
getNetworkSettings,
updateNetworkSettings,
type NetworkSettings
} from '$lib/api/admin';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/admin.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
let settings = $state<NetworkSettings | null>(null);
let hops = $state(1);
@@ -77,7 +77,7 @@
max={settings.max_hops}
bind:value={hops}
class="w-24 rounded border border-border bg-background px-2 py-1
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent"
/>
</label>
<button
@@ -116,7 +116,7 @@
<div class="space-y-2 rounded border border-border bg-background p-3 text-sm">
<p class="flex items-start gap-2 text-text-secondary">
<TriangleAlert size={14} class="mt-0.5 flex-shrink-0 text-action-destructive" aria-hidden="true" />
<TriangleAlert size={14} class="mt-0.5 shrink-0 text-action-destructive" aria-hidden="true" />
<span>
Count your proxies — don't guess high. This number tells Minstrel how much of the
<span class="font-mono">X-Forwarded-For</span> header to believe, and that header is
@@ -5,12 +5,12 @@ import NetworkSettingsCard from './NetworkSettingsCard.svelte';
const getNetworkSettings = vi.fn();
const updateNetworkSettings = vi.fn();
vi.mock('$lib/api/admin', () => ({
vi.mock('#lib/api/admin.js', () => ({
getNetworkSettings: () => getNetworkSettings(),
updateNetworkSettings: (hops: number) => updateNetworkSettings(hops)
}));
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
function settings(over: Record<string, unknown> = {}) {
return {
@@ -4,13 +4,13 @@
NORMALIZATION_MODES,
NORMALIZATION_TARGETS,
type NormalizationPrefs
} from '$lib/api/normalization';
} from '#lib/api/normalization.js';
import {
normalization,
loadNormalization,
setNormalization
} from '$lib/stores/normalization.svelte';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/stores/normalization.svelte.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// Volume leveling (M464 #4998). The choice is the user's, stored on the
// server, so it follows them to the Android app and to casts.
@@ -1,17 +1,17 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
vi.mock('$lib/api/normalization', async (importOriginal) => ({
...(await importOriginal<typeof import('$lib/api/normalization')>()),
vi.mock('#lib/api/normalization.js', async (importOriginal) => ({
...(await importOriginal<typeof import('#lib/api/normalization.js')>()),
getNormalization: vi.fn(),
putNormalization: vi.fn()
}));
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
import NormalizationSetting from './NormalizationSetting.svelte';
import { getNormalization, putNormalization } from '$lib/api/normalization';
import { normalization } from '$lib/stores/normalization.svelte';
import { pushToast } from '$lib/stores/toast.svelte';
import { getNormalization, putNormalization } from '#lib/api/normalization.js';
import { normalization } from '#lib/stores/normalization.svelte.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
afterEach(() => {
vi.clearAllMocks();
@@ -9,9 +9,9 @@
markAllNotificationsRead,
markNotificationRead,
type NotificationItem
} from '$lib/api/notifications';
import { relativeTime } from '$lib/utils/relativeTime';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/notifications.js';
import { relativeTime } from '#lib/utils/relativeTime.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// The notifications inbox (M489): a bell in the header with an unread badge,
// opening a panel of what happened while you were away.
@@ -67,7 +67,7 @@
<svelte:window onclick={handleWindowClick} onkeydown={(e) => e.key === 'Escape' && (open = false)} />
<div class="relative flex-shrink-0">
<div class="relative shrink-0">
<button
bind:this={buttonRef}
type="button"
@@ -92,7 +92,7 @@
{#if open}
<div
bind:this={panelRef}
class="absolute right-0 z-50 mt-1 flex max-h-[70vh] w-[min(22rem,calc(100vw-2rem))] flex-col rounded border border-border bg-surface shadow"
class="absolute right-0 z-50 mt-1 flex max-h-[70vh] w-[min(22rem,calc(100vw-2rem))] flex-col rounded border border-border bg-surface shadow-sm"
role="dialog"
aria-label="Notifications"
>
@@ -123,7 +123,7 @@
onclick={() => openItem(item)}
>
<span
class="mt-1.5 h-2 w-2 flex-shrink-0 rounded-full {item.read_at ? '' : 'bg-text-primary'}"
class="mt-1.5 h-2 w-2 shrink-0 rounded-full {item.read_at ? '' : 'bg-text-primary'}"
aria-hidden="true"
></span>
<span class="min-w-0 flex-1">
@@ -8,7 +8,7 @@ vi.mock('@tanstack/svelte-query', async (orig) => {
return { ...actual, useQueryClient: () => ({ invalidateQueries: invalidateMock }) };
});
vi.mock('$lib/api/notifications', async (orig) => {
vi.mock('#lib/api/notifications.js', async (orig) => {
const actual = (await orig()) as Record<string, unknown>;
return {
badgeLabel: actual.badgeLabel,
@@ -19,7 +19,7 @@ vi.mock('$lib/api/notifications', async (orig) => {
};
});
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
import NotificationBell from './NotificationBell.svelte';
import { goto } from '$app/navigation';
@@ -30,7 +30,7 @@ import {
markAllNotificationsRead,
markNotificationRead,
type NotificationItem
} from '$lib/api/notifications';
} from '#lib/api/notifications.js';
function item(over: Partial<NotificationItem> = {}): NotificationItem {
return {
@@ -4,9 +4,9 @@
getNotificationEmailSettings,
updateNotificationEmailSettings,
type NotificationEmailSettings
} from '$lib/api/admin';
import { errMessage } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/admin.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// When notification emails go out (M489 #5346). Nothing is emailed per
// event: new music is a daily summary, everything else a batch.
@@ -84,7 +84,7 @@
<select
bind:value={form.summary_hour}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline focus-visible:outline-2
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
>
{#each hours as h (h)}
@@ -104,7 +104,7 @@
max="1440"
bind:value={form.batch_window_minutes}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline focus-visible:outline-2
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
/>
</label>
@@ -114,7 +114,7 @@
<button
type="button"
class="rounded-md bg-action-secondary px-4 py-2 text-sm text-action-fg hover:opacity-90
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent
disabled:cursor-not-allowed disabled:opacity-50"
disabled={!dirty || saving}
onclick={save}
@@ -1,16 +1,16 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
vi.mock('$lib/api/admin', () => ({
vi.mock('#lib/api/admin.js', () => ({
getNotificationEmailSettings: vi.fn(),
updateNotificationEmailSettings: vi.fn()
}));
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
import NotificationEmailCard from './NotificationEmailCard.svelte';
import { getNotificationEmailSettings, updateNotificationEmailSettings } from '$lib/api/admin';
import { pushToast } from '$lib/stores/toast.svelte';
import { getNotificationEmailSettings, updateNotificationEmailSettings } from '#lib/api/admin.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
afterEach(() => vi.clearAllMocks());
@@ -8,10 +8,10 @@
type NotificationKindSetting,
type NotificationSettingChange,
type NotificationSettings
} from '$lib/api/notifications';
import { qk } from '$lib/api/queries';
import { user } from '$lib/auth/store.svelte';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/notifications.js';
import { qk } from '#lib/api/queries.js';
import { user } from '#lib/auth/store.svelte.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// Per-user notification settings (M489): one row per kind, a toggle per
// channel. Toggles are optimistic; a failed save puts the switch back.
@@ -8,7 +8,7 @@ vi.mock('@tanstack/svelte-query', async (orig) => {
return { ...actual, useQueryClient: () => ({ setQueryData, invalidateQueries: vi.fn() }) };
});
vi.mock('$lib/api/notifications', async (orig) => {
vi.mock('#lib/api/notifications.js', async (orig) => {
const actual = (await orig()) as Record<string, unknown>;
return {
...actual,
@@ -18,12 +18,12 @@ vi.mock('$lib/api/notifications', async (orig) => {
});
const userState = vi.hoisted(() => ({ current: { id: '1', username: 'alice', is_admin: false } }));
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: { get value() { return userState.current; } }
}));
const pushToast = vi.fn();
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: (...a: unknown[]) => pushToast(...a) }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: (...a: unknown[]) => pushToast(...a) }));
import NotificationSettings from './NotificationSettings.svelte';
import {
@@ -31,7 +31,7 @@ import {
putNotificationSettings,
type NotificationKindSetting,
type NotificationSettings as Settings
} from '$lib/api/notifications';
} from '#lib/api/notifications.js';
const requesterKinds: NotificationKindSetting[] = [
{ kind: 'request_approved', admin_only: false, inbox: true, phone: true, email: true },
+9 -9
View File
@@ -10,11 +10,11 @@
togglePlay, skipNext, skipPrev, seekTo, setVolume,
toggleShuffle, cycleRepeat, playQueue,
toggleQueueDrawer
} from '$lib/player/store.svelte';
import { formatDuration } from '$lib/media/duration';
import { FALLBACK_COVER, coverUrl } from '$lib/media/covers';
import { dominantColorFromUrl, rgbToCssString } from '$lib/media/dominantColor';
import { useSmoothPosition } from '$lib/player/smoothPosition.svelte';
} from '#lib/player/store.svelte.js';
import { formatDuration } from '#lib/media/duration.js';
import { FALLBACK_COVER, coverUrl } from '#lib/media/covers.js';
import { dominantColorFromUrl, rgbToCssString } from '#lib/media/dominantColor.js';
import { useSmoothPosition } from '#lib/player/smoothPosition.svelte.js';
import LikeButton from './LikeButton.svelte';
import TrackMenu from './TrackMenu.svelte';
@@ -118,7 +118,7 @@
<a
href="/now-playing"
aria-label="Open now playing"
class="flex min-w-0 items-center gap-2 focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
class="flex min-w-0 items-center gap-2 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent"
style="flex: 1 1 35%;"
>
<img
@@ -153,7 +153,7 @@
type="button"
aria-label={player.isPlaying ? 'Pause' : 'Play'}
onclick={togglePlay}
class="flex h-12 w-12 items-center justify-center rounded-full bg-accent text-text-primary shadow transition-transform hover:scale-105 focus-visible:ring-2 focus-visible:ring-accent"
class="flex h-12 w-12 items-center justify-center rounded-full bg-accent text-text-primary shadow-sm transition-transform hover:scale-105 focus-visible:ring-2 focus-visible:ring-accent"
>
{#if player.state === 'loading' || player.state === 'idle'}
<Loader2 data-testid="play-spinner" size={22} strokeWidth={1.5} class="animate-spin" />
@@ -281,7 +281,7 @@
<a
href="/now-playing"
aria-label="Open now playing"
class="shrink-0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
class="shrink-0 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent"
>
<img
src={coverUrl(current.album_id)}
@@ -341,7 +341,7 @@
type="button"
aria-label={player.isPlaying ? 'Pause' : 'Play'}
onclick={togglePlay}
class="flex h-12 w-12 items-center justify-center rounded-full bg-accent text-text-primary shadow transition-transform hover:scale-105 focus-visible:ring-2 focus-visible:ring-accent"
class="flex h-12 w-12 items-center justify-center rounded-full bg-accent text-text-primary shadow-sm transition-transform hover:scale-105 focus-visible:ring-2 focus-visible:ring-accent"
>
{#if player.state === 'loading' || player.state === 'idle'}
<Loader2 size={24} strokeWidth={1.5} class="animate-spin" />
+6 -6
View File
@@ -1,9 +1,9 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, within } from '@testing-library/svelte';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { emptyQuarantineMock } from '../../test-utils/mocks/quarantine';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
// Mutable state the mocked store reads from.
const state = vi.hoisted(() => ({
@@ -20,7 +20,7 @@ const state = vi.hoisted(() => ({
queueDrawerOpen: false
}));
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
player: {
get queue() { return state.queue; },
get index() { return state.index; },
@@ -46,16 +46,16 @@ vi.mock('$lib/player/store.svelte', () => ({
toggleQueueDrawer: vi.fn()
}));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$lib/api/quarantine', () => emptyQuarantineMock());
vi.mock('#lib/api/quarantine.js', () => emptyQuarantineMock());
import PlayerBar from './PlayerBar.svelte';
import {
togglePlay, skipNext, skipPrev, seekTo, setVolume,
toggleShuffle, cycleRepeat, playQueue,
toggleQueueDrawer
} from '$lib/player/store.svelte';
} from '#lib/player/store.svelte.js';
function track(): TrackRef {
return makeTrack({
+11 -11
View File
@@ -1,15 +1,15 @@
<script lang="ts">
import { Play, MoreVertical, RefreshCcw } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import type { Playlist, PlaylistTrack, TrackRef } from '$lib/api/types';
import { user } from '$lib/auth/store.svelte';
import { getPlaylist, systemShuffle, refreshSystem } from '$lib/api/playlists';
import { playlistTrackToRef } from '$lib/playlists/playlistTrackToRef';
import { systemPlaylistRefetch } from '$lib/playlists/systemRefetch';
import { errCode } from '$lib/api/errors';
import { qk } from '$lib/api/queries';
import { playQueue } from '$lib/player/store.svelte';
import { pushToast } from '$lib/stores/toast.svelte';
import type { Playlist, PlaylistTrack, TrackRef } from '#lib/api/types.js';
import { user } from '#lib/auth/store.svelte.js';
import { getPlaylist, systemShuffle, refreshSystem } from '#lib/api/playlists.js';
import { playlistTrackToRef } from '#lib/playlists/playlistTrackToRef.js';
import { systemPlaylistRefetch } from '#lib/playlists/systemRefetch.js';
import { errCode } from '#lib/api/errors.js';
import { qk } from '#lib/api/queries.js';
import { playQueue } from '#lib/player/store.svelte.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
let { playlist }: { playlist: Playlist } = $props();
@@ -184,7 +184,7 @@
>
<div
class="art-wrap relative aspect-square w-full overflow-hidden rounded-md
bg-surface-hover shadow-sm transition-all duration-150
bg-surface-hover shadow-xs transition-all duration-150
group-hover:shadow-lg group-hover:ring-1 group-hover:ring-accent/40"
>
{#if playlist.cover_url}
@@ -203,7 +203,7 @@
is often a 2x2 collage; the gradient mutes the collage so
the playlist NAME becomes the dominant element. -->
<div
class="pointer-events-none absolute inset-0 bg-gradient-to-t
class="pointer-events-none absolute inset-0 bg-linear-to-t
from-black/85 via-black/40 to-transparent"
></div>
<!-- Title burned in at the bottom of the cover. Two lines max;
+15 -15
View File
@@ -1,13 +1,13 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import PlaylistCard from './PlaylistCard.svelte';
import type { Playlist, PlaylistDetail } from '$lib/api/types';
import type { Playlist, PlaylistDetail } from '#lib/api/types.js';
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: { value: { id: 'u-self', username: 'me', is_admin: false } }
}));
vi.mock('$lib/api/playlists', () => ({
vi.mock('#lib/api/playlists.js', () => ({
getPlaylist: vi.fn().mockResolvedValue({
id: 'p-1',
user_id: 'u-self',
@@ -75,11 +75,11 @@ vi.mock('$lib/api/playlists', () => ({
refreshSystem: vi.fn().mockResolvedValue({ playlist_id: 'p-1', track_count: 5, track_ids: ['t-1'] })
}));
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
playQueue: vi.fn()
}));
vi.mock('$lib/api/queries', () => ({
vi.mock('#lib/api/queries.js', () => ({
qk: {
playlist: (id: string) => ['playlist', id],
playlists: (kind?: string) => ['playlists', { kind: kind ?? 'user' }]
@@ -162,8 +162,8 @@ describe('PlaylistCard', () => {
});
test('play button calls playQueue with tracks when clicked', async () => {
const { getPlaylist } = await import('$lib/api/playlists');
const { playQueue } = await import('$lib/player/store.svelte');
const { getPlaylist } = await import('#lib/api/playlists.js');
const { playQueue } = await import('#lib/player/store.svelte.js');
render(PlaylistCard, { props: { playlist: base } });
const playButton = screen.getByLabelText(/Play Saturday morning/i);
@@ -222,7 +222,7 @@ describe('PlaylistCard', () => {
});
test('clicking Refresh calls refreshSystem with the raw variant', async () => {
const { refreshSystem } = await import('$lib/api/playlists');
const { refreshSystem } = await import('#lib/api/playlists.js');
const discoverPlaylist: Playlist = {
...base,
kind: 'system',
@@ -237,8 +237,8 @@ describe('PlaylistCard', () => {
});
test('For-You play calls systemShuffle, not getPlaylist', async () => {
const { getPlaylist, systemShuffle } = await import('$lib/api/playlists');
const { playQueue } = await import('$lib/player/store.svelte');
const { getPlaylist, systemShuffle } = await import('#lib/api/playlists.js');
const { playQueue } = await import('#lib/player/store.svelte.js');
const forYouPlaylist: Playlist = {
...base,
kind: 'system',
@@ -261,7 +261,7 @@ describe('PlaylistCard', () => {
});
test('For-You play passes source:for_you to playQueue (no client shuffle)', async () => {
const { playQueue } = await import('$lib/player/store.svelte');
const { playQueue } = await import('#lib/player/store.svelte.js');
const forYouPlaylist: Playlist = {
...base,
kind: 'system',
@@ -281,8 +281,8 @@ describe('PlaylistCard', () => {
});
test('user-playlist play uses getPlaylist + plain playQueue (no source)', async () => {
const { getPlaylist, systemShuffle } = await import('$lib/api/playlists');
const { playQueue } = await import('$lib/player/store.svelte');
const { getPlaylist, systemShuffle } = await import('#lib/api/playlists.js');
const { playQueue } = await import('#lib/player/store.svelte.js');
render(PlaylistCard, { props: { playlist: base } });
const playButton = screen.getByLabelText(/Play Saturday morning/i);
await fireEvent.click(playButton);
@@ -294,8 +294,8 @@ describe('PlaylistCard', () => {
});
test('user-playlist play does not refresh anything', async () => {
const { refreshSystem, getPlaylist } = await import('$lib/api/playlists');
const { playQueue } = await import('$lib/player/store.svelte');
const { refreshSystem, getPlaylist } = await import('#lib/api/playlists.js');
const { playQueue } = await import('#lib/player/store.svelte.js');
render(PlaylistCard, { props: { playlist: base } });
const playButton = screen.getByLabelText(/Play Saturday morning/i);
@@ -3,9 +3,9 @@
import { draggable, type DragEventData } from '@neodrag/svelte';
import LikeButton from './LikeButton.svelte';
import TrackMenu from './TrackMenu.svelte';
import type { PlaylistTrack } from '$lib/api/types';
import { playlistTrackToRef } from '$lib/playlists/playlistTrackToRef';
import { player } from '$lib/player/store.svelte';
import type { PlaylistTrack } from '#lib/api/types.js';
import { playlistTrackToRef } from '#lib/playlists/playlistTrackToRef.js';
import { player } from '#lib/player/store.svelte.js';
import { offsetToDelta } from './queue-row-math';
let {
@@ -103,8 +103,8 @@
aria-label="Reorder track (drag or use arrow keys)"
aria-keyshortcuts="ArrowUp ArrowDown"
onkeydown={handleHandleKeydown}
class="flex-shrink-0 cursor-grab text-text-muted hover:text-text-primary
active:cursor-grabbing focus-visible:outline focus-visible:outline-2
class="shrink-0 cursor-grab text-text-muted hover:text-text-primary
active:cursor-grabbing focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent disabled:cursor-not-allowed disabled:opacity-50"
>
<GripVertical size={14} strokeWidth={1} />
@@ -129,7 +129,7 @@
</div>
</button>
<span class="flex-shrink-0 text-xs text-text-muted">{format(row.duration_sec)}</span>
<span class="shrink-0 text-xs text-text-muted">{format(row.duration_sec)}</span>
{#if !isUnavailable && liveTrack}
<LikeButton entityType="track" entityId={liveTrack.id} />
@@ -141,7 +141,7 @@
type="button"
onclick={() => onRemove(row.position)}
aria-label={`Remove ${row.title} from playlist`}
class="flex-shrink-0 rounded p-1 text-text-muted hover:bg-surface hover:text-action-destructive"
class="shrink-0 rounded p-1 text-text-muted hover:bg-surface hover:text-action-destructive"
>
<X size={14} strokeWidth={1} />
</button>
@@ -2,14 +2,14 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { emptyQuarantineMock } from '../../test-utils/mocks/quarantine';
import type { PlaylistTrack } from '$lib/api/types';
import type { PlaylistTrack } from '#lib/api/types.js';
// LikeButton + TrackMenu transitively pull in TanStack Query, the auth
// store, likes/quarantine APIs, the player store, and SvelteKit
// navigation. Mock all of them at module level so the component can be
// rendered in isolation.
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: { value: { id: 'u1', username: 'me', is_admin: false } }
}));
@@ -17,11 +17,11 @@ vi.mock('$app/navigation', () => ({
goto: vi.fn()
}));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$lib/api/quarantine', () => emptyQuarantineMock());
vi.mock('#lib/api/quarantine.js', () => emptyQuarantineMock());
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
playNext: vi.fn(),
enqueueTrack: vi.fn(),
playQueue: vi.fn(),
@@ -1,9 +1,9 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Save, TriangleAlert } from 'lucide-svelte';
import { getNetworkSettings, updatePublicUrl } from '$lib/api/admin';
import { errMessage } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
import { getNetworkSettings, updatePublicUrl } from '#lib/api/admin.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
let saved = $state<string | null>(null);
let value = $state('');
@@ -68,7 +68,7 @@
placeholder="https://music.example.com"
bind:value
class="w-full rounded border border-border bg-background px-2 py-1 font-mono
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent"
/>
</label>
{#if here && value.trim() !== here}
@@ -96,7 +96,7 @@
{#if !saved}
<p class="flex items-start gap-2 text-sm text-text-secondary" role="status">
<TriangleAlert size={14} class="mt-0.5 flex-shrink-0 text-action-destructive" aria-hidden="true" />
<TriangleAlert size={14} class="mt-0.5 shrink-0 text-action-destructive" aria-hidden="true" />
<span>
Not set, so password-reset emails are not being sent. Minstrel won't build the link from
whatever address a request claims, because anyone can claim any address.
@@ -5,13 +5,13 @@ import PublicAddressCard from './PublicAddressCard.svelte';
const getNetworkSettings = vi.fn();
const updatePublicUrl = vi.fn();
vi.mock('$lib/api/admin', () => ({
vi.mock('#lib/api/admin.js', () => ({
getNetworkSettings: () => getNetworkSettings(),
updatePublicUrl: (url: string) => updatePublicUrl(url)
}));
const pushToast = vi.fn();
vi.mock('$lib/stores/toast.svelte', () => ({
vi.mock('#lib/stores/toast.svelte.js', () => ({
pushToast: (...args: unknown[]) => pushToast(...args)
}));
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { player, closeQueueDrawer } from '$lib/player/store.svelte';
import { player, closeQueueDrawer } from '#lib/player/store.svelte.js';
import QueueList from './QueueList.svelte';
let previouslyFocused: HTMLElement | null = null;
+4 -4
View File
@@ -1,8 +1,8 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import type { TrackRef } from '$lib/api/types';
import { makeTrack } from '$test-utils/fixtures/track';
import type { TrackRef } from '#lib/api/types.js';
import { makeTrack } from '#test-utils/fixtures/track.js';
// QueueTrackRow (rendered inside QueueDrawer → QueueList) now renders a
// LikeButton, which reads createLikedIdsQuery. Stub the likes API so the
@@ -10,14 +10,14 @@ import { makeTrack } from '$test-utils/fixtures/track';
// emptyLikesMock import — must precede the QueueDrawer import below, since
// loading the component evaluates the mocked modules and the hoisted
// factories need their referenced bindings already initialized.
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
const closeQueueDrawer = vi.fn();
let queueValue: TrackRef[] = [];
let indexValue = 0;
let openValue = true;
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
closeQueueDrawer: (...args: unknown[]) => closeQueueDrawer(...args),
player: {
get queue() { return queueValue; },
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { untrack } from 'svelte';
import { X, Trash2, ArrowDown } from 'lucide-svelte';
import { player, clearQueue } from '$lib/player/store.svelte';
import { player, clearQueue } from '#lib/player/store.svelte.js';
import QueueTrackRow from './QueueTrackRow.svelte';
// onClose: when provided, renders an X button in the header so the
+7 -7
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import { GripVertical, X } from 'lucide-svelte';
import { draggable, type DragEventData } from '@neodrag/svelte';
import type { TrackRef } from '$lib/api/types';
import { playFromQueueIndex, removeFromQueue, moveQueueItem } from '$lib/player/store.svelte';
import { coverUrl, FALLBACK_COVER } from '$lib/media/covers';
import type { TrackRef } from '#lib/api/types.js';
import { playFromQueueIndex, removeFromQueue, moveQueueItem } from '#lib/player/store.svelte.js';
import { coverUrl, FALLBACK_COVER } from '#lib/media/covers.js';
import { offsetToDelta } from './queue-row-math';
import LikeButton from './LikeButton.svelte';
@@ -71,7 +71,7 @@
hover/focus so the artwork stays legible the rest of the time; the drop
shadow is what keeps the glyph readable over pale covers without one.
-->
<div class="relative h-10 w-10 flex-shrink-0">
<div class="relative h-10 w-10 shrink-0">
<img
src={coverUrl(track.album_id)}
alt=""
@@ -86,7 +86,7 @@
class="group absolute inset-0 flex cursor-grab items-center justify-center rounded
text-white/70 transition hover:bg-black/45 hover:text-white
focus-visible:bg-black/45 focus-visible:text-white
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent"
style="filter: drop-shadow(0 1px 1px rgb(0 0 0 / 0.9))"
>
<GripVertical size={16} />
@@ -101,7 +101,7 @@
>
<div class="flex items-center gap-2 min-w-0">
{#if isCurrent}
<span class="text-xs text-accent-fg uppercase tracking-wide flex-shrink-0">Now playing</span>
<span class="text-xs text-accent-fg uppercase tracking-wide shrink-0">Now playing</span>
{/if}
<span class="font-medium truncate text-text-primary">{track.title}</span>
</div>
@@ -114,7 +114,7 @@
type="button"
onclick={handleRemove}
aria-label="Remove from queue"
class="text-text-secondary hover:text-text-primary flex-shrink-0"
class="text-text-secondary hover:text-text-primary shrink-0"
>
<X size={16} />
</button>
+3 -3
View File
@@ -1,14 +1,14 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
// QueueTrackRow now renders a LikeButton, which reads createLikedIdsQuery.
// Stub the likes API so the row doesn't need a real QueryClient in context.
// The mock (and its emptyLikesMock import) must precede the component import
// below: importing QueueTrackRow transitively loads LikeButton → the mocked
// module, and the hoisted factory needs emptyLikesMock already initialized.
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
import QueueTrackRow from './QueueTrackRow.svelte';
@@ -16,7 +16,7 @@ const playFromQueueIndex = vi.fn();
const removeFromQueue = vi.fn();
const moveQueueItem = vi.fn();
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
playFromQueueIndex: (...args: unknown[]) => playFromQueueIndex(...args),
removeFromQueue: (...args: unknown[]) => removeFromQueue(...args),
moveQueueItem: (...args: unknown[]) => moveQueueItem(...args)
+1 -1
View File
@@ -58,7 +58,7 @@
oninput={onInput}
onkeydown={onKey}
class="w-full rounded border border-border bg-surface pl-7 pr-7 py-1 text-sm
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent"
/>
{#if raw}
<button
@@ -5,9 +5,9 @@
getReacquisitionSettings,
updateReacquisitionSettings,
type ReacquisitionSettings
} from '$lib/api/admin';
import { errMessage } from '$lib/api/errors';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/admin.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
// Policy for turning a missing file back into a Lidarr request
// (milestone #290). Lives on the missing-files page rather than under
@@ -127,7 +127,7 @@
max="720"
bind:value={form.grace_hours}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline focus-visible:outline-2
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
/>
</label>
@@ -144,7 +144,7 @@
max="10"
bind:value={form.max_attempts}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline focus-visible:outline-2
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
/>
</label>
@@ -157,7 +157,7 @@
max="168"
bind:value={form.backoff_base_hours}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline focus-visible:outline-2
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
/>
</label>
@@ -170,7 +170,7 @@
max="720"
bind:value={form.backoff_max_hours}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline focus-visible:outline-2
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
/>
</label>
@@ -186,7 +186,7 @@
max="200"
bind:value={form.max_per_pass}
class="mt-1 w-28 rounded border border-border bg-background px-2 py-1 text-sm
text-text-primary focus-visible:outline focus-visible:outline-2
text-text-primary focus-visible:outline-solid focus-visible:outline-2
focus-visible:outline-accent"
/>
</label>
@@ -213,7 +213,7 @@
watching those rows never get a request would reasonably conclude
the feature is broken. -->
<p class="flex items-start gap-2 rounded-md bg-surface-hover px-3 py-2 text-xs text-text-secondary">
<TriangleAlert size={14} class="mt-0.5 flex-shrink-0 text-action-destructive" aria-hidden="true" />
<TriangleAlert size={14} class="mt-0.5 shrink-0 text-action-destructive" aria-hidden="true" />
<span>
{saved.unnameable_albums}
{saved.unnameable_albums === 1 ? 'album has' : 'albums have'} missing files but no
@@ -227,7 +227,7 @@
<button
type="button"
class="rounded-md bg-action-secondary px-4 py-2 text-sm text-action-fg hover:opacity-90
focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent
focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent
disabled:cursor-not-allowed disabled:opacity-50"
disabled={!dirty || saving}
onclick={save}
@@ -1,18 +1,18 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import type { ReacquisitionSettings } from '$lib/api/admin';
import { ERROR_COPY } from '$lib/api/error-copy';
import type { ReacquisitionSettings } from '#lib/api/admin.js';
import { ERROR_COPY } from '#lib/api/error-copy.js';
vi.mock('$lib/api/admin', () => ({
vi.mock('#lib/api/admin.js', () => ({
getReacquisitionSettings: vi.fn(),
updateReacquisitionSettings: vi.fn()
}));
vi.mock('$lib/stores/toast.svelte', () => ({ pushToast: vi.fn() }));
vi.mock('#lib/stores/toast.svelte.js', () => ({ pushToast: vi.fn() }));
import ReacquisitionSettingsCard from './ReacquisitionSettingsCard.svelte';
import { getReacquisitionSettings, updateReacquisitionSettings } from '$lib/api/admin';
import { pushToast } from '$lib/stores/toast.svelte';
import { getReacquisitionSettings, updateReacquisitionSettings } from '#lib/api/admin.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
const base: ReacquisitionSettings = {
enabled: true,
+7 -7
View File
@@ -18,16 +18,16 @@
function navigate(next: string) {
const onSearchPage = page.url.pathname.startsWith('/search');
const trimmed = next.trim();
// keepFocus: SvelteKit blurs the active element on navigation by default.
// For a typeahead-style URL update we want the user to keep typing
// without re-clicking the input every 250 ms.
// reset: false: SvelteKit blurs the active element and scrolls to the top
// on navigation by default. For a typeahead-style URL update we want the
// user to keep typing without re-clicking the input every 250 ms.
if (trimmed === '') {
if (onSearchPage) goto('/search', { replaceState: true, keepFocus: true });
if (onSearchPage) goto('/search', { replace: true, reset: false });
return;
}
goto(`/search?q=${encodeURIComponent(trimmed)}`, {
replaceState: onSearchPage,
keepFocus: true,
replace: onSearchPage,
reset: false,
});
}
@@ -66,6 +66,6 @@
bind:value
oninput={onInput}
onkeydown={onKey}
class="w-full rounded border border-border bg-surface pl-7 pr-3 py-1 text-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
class="w-full rounded border border-border bg-surface pl-7 pr-3 py-1 text-sm focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent"
/>
</label>
+7 -7
View File
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import { pageUrlModule } from '$test-utils/mocks/appState';
import { pageUrlModule } from '#test-utils/mocks/appState.js';
const state = vi.hoisted(() => ({
pageUrl: new URL('http://localhost/')
@@ -40,7 +40,7 @@ describe('SearchInput', () => {
expect(goto).not.toHaveBeenCalled();
vi.advanceTimersByTime(250);
expect(goto).toHaveBeenCalledTimes(1);
expect(goto).toHaveBeenCalledWith('/search?q=h', { replaceState: false, keepFocus: true });
expect(goto).toHaveBeenCalledWith('/search?q=h', { replace: false, reset: false });
});
test('rapid keystrokes debounce to a single goto', async () => {
@@ -53,16 +53,16 @@ describe('SearchInput', () => {
await fireEvent.input(input, { target: { value: 'hel' } });
vi.advanceTimersByTime(250);
expect(goto).toHaveBeenCalledTimes(1);
expect(goto).toHaveBeenCalledWith('/search?q=hel', { replaceState: false, keepFocus: true });
expect(goto).toHaveBeenCalledWith('/search?q=hel', { replace: false, reset: false });
});
test('typing while on /search uses replaceState=true', async () => {
test('typing while on /search uses replace=true', async () => {
state.pageUrl = new URL('http://localhost/search?q=h');
render(SearchInput);
const input = screen.getByRole('searchbox') as HTMLInputElement;
await fireEvent.input(input, { target: { value: 'he' } });
vi.advanceTimersByTime(250);
expect(goto).toHaveBeenCalledWith('/search?q=he', { replaceState: true, keepFocus: true });
expect(goto).toHaveBeenCalledWith('/search?q=he', { replace: true, reset: false });
});
test('clearing input on /search calls goto("/search") (drops ?q=)', async () => {
@@ -71,7 +71,7 @@ describe('SearchInput', () => {
const input = screen.getByRole('searchbox') as HTMLInputElement;
await fireEvent.input(input, { target: { value: '' } });
vi.advanceTimersByTime(250);
expect(goto).toHaveBeenCalledWith('/search', { replaceState: true, keepFocus: true });
expect(goto).toHaveBeenCalledWith('/search', { replace: true, reset: false });
});
test('Escape clears the input value', async () => {
@@ -88,6 +88,6 @@ describe('SearchInput', () => {
const input = screen.getByRole('searchbox') as HTMLInputElement;
await fireEvent.input(input, { target: { value: 'a b&c' } });
vi.advanceTimersByTime(250);
expect(goto).toHaveBeenCalledWith('/search?q=a%20b%26c', { replaceState: false, keepFocus: true });
expect(goto).toHaveBeenCalledWith('/search?q=a%20b%26c', { replace: false, reset: false });
});
});
+3 -3
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import { X, ListPlus, Plus, ListMusic, Heart } from 'lucide-svelte';
import { useQueryClient } from '@tanstack/svelte-query';
import { selection, clearSelection } from '$lib/selection/store.svelte';
import { playNextMany, enqueueTracks } from '$lib/player/store.svelte';
import { likeEntity, createLikedIdsQuery } from '$lib/api/likes';
import { selection, clearSelection } from '#lib/selection/store.svelte.js';
import { playNextMany, enqueueTracks } from '#lib/player/store.svelte.js';
import { likeEntity, createLikedIdsQuery } from '#lib/api/likes.js';
import AddToPlaylistMenu from './AddToPlaylistMenu.svelte';
const queryClient = useQueryClient();
+7 -7
View File
@@ -2,9 +2,9 @@
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { ChevronDown, Compass, House, LibraryBig } from 'lucide-svelte';
import { user, logout } from '$lib/auth/store.svelte';
import { player } from '$lib/player/store.svelte';
import { appName } from '$lib/branding';
import { user, logout } from '#lib/auth/store.svelte.js';
import { player } from '#lib/player/store.svelte.js';
import { appName } from '#lib/branding.js';
import MinstrelMark from './MinstrelMark.svelte';
import NotificationBell from './NotificationBell.svelte';
import PlayerBar from './PlayerBar.svelte';
@@ -35,7 +35,7 @@
async function handleLogout() {
menuOpen = false;
await logout();
goto('/login', { replaceState: true });
goto('/login', { replace: true });
}
function isActive(prefix: string): boolean {
@@ -94,13 +94,13 @@
</nav>
<div class="flex items-center gap-1 md:gap-3 justify-self-end min-w-0">
<div class="hidden md:block min-w-0 max-w-xs flex-shrink">
<div class="hidden md:block min-w-0 max-w-xs shrink">
<SearchInput />
</div>
<NotificationBell />
<div class="relative flex-shrink-0">
<div class="relative shrink-0">
<button
bind:this={menuButtonRef}
type="button"
@@ -120,7 +120,7 @@
{#if menuOpen}
<div
bind:this={menuRef}
class="absolute right-0 z-50 mt-1 w-40 rounded border border-border bg-surface shadow"
class="absolute right-0 z-50 mt-1 w-40 rounded border border-border bg-surface shadow-sm"
role="menu"
tabindex="-1"
>
+4 -4
View File
@@ -19,14 +19,14 @@ const userState = vi.hoisted(() => ({
} | null
}));
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: { get value() { return userState.current; } },
logout: vi.fn().mockResolvedValue(undefined)
}));
// The bell's queries need a QueryClient the Shell tests do not provide; its own
// behaviour is covered in NotificationBell.test.ts.
vi.mock('$lib/api/notifications', async () => {
vi.mock('#lib/api/notifications.js', async () => {
const { mockQuery } = await import('../../test-utils/query');
return {
createUnreadCountQuery: () => mockQuery({ data: 0 }),
@@ -38,7 +38,7 @@ vi.mock('$lib/api/notifications', async () => {
});
import Shell from './Shell.svelte';
import { logout } from '$lib/auth/store.svelte';
import { logout } from '#lib/auth/store.svelte.js';
import { goto } from '$app/navigation';
afterEach(() => {
@@ -110,6 +110,6 @@ describe('Shell', () => {
await fireEvent.click(screen.getByRole('button', { name: /alice/i }));
await fireEvent.click(screen.getByRole('menuitem', { name: /log out/i }));
expect(logout).toHaveBeenCalledTimes(1);
expect(goto).toHaveBeenCalledWith('/login', { replaceState: true });
expect(goto).toHaveBeenCalledWith('/login', { replace: true });
});
});
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts" module>
import type { LidarrRequestStatus } from '$lib/api/types';
import type { LidarrRequestStatus } from '#lib/api/types.js';
type Tone = 'warning' | 'info' | 'success' | 'error';
+6 -6
View File
@@ -5,12 +5,12 @@
createSnoozesQuery,
snoozeSuggestion,
unsnoozeSuggestion
} from '$lib/api/suggestions';
import { createRequest } from '$lib/api/requests';
import { qk } from '$lib/api/queries';
import { pushToast } from '$lib/stores/toast.svelte';
} from '#lib/api/suggestions.js';
import { createRequest } from '#lib/api/requests.js';
import { qk } from '#lib/api/queries.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
import DiscoverResultCard from './DiscoverResultCard.svelte';
import type { ArtistSuggestion, SeedContribution, SuggestionSnooze } from '$lib/api/types';
import type { ArtistSuggestion, SeedContribution, SuggestionSnooze } from '#lib/api/types.js';
const client = useQueryClient();
const queryStore = createSuggestionsQuery();
@@ -199,7 +199,7 @@
<button
type="button"
aria-label={`Bring ${snoozed.name} back now`}
class="shrink-0 rounded-md border border-border px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-accent"
class="shrink-0 rounded-md border border-border px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover focus:outline-hidden focus:ring-2 focus:ring-accent"
onclick={() => onUnsnooze(snoozed.mbid, snoozed.name)}
>
Bring back
@@ -8,19 +8,19 @@ vi.mock('@tanstack/svelte-query', async (orig) => {
return { ...actual, useQueryClient: () => ({ invalidateQueries: invalidateMock }) };
});
vi.mock('$lib/api/suggestions', () => ({
vi.mock('#lib/api/suggestions.js', () => ({
createSuggestionsQuery: vi.fn(),
createSnoozesQuery: vi.fn(),
snoozeSuggestion: vi.fn().mockResolvedValue(undefined),
unsnoozeSuggestion: vi.fn().mockResolvedValue(undefined)
}));
vi.mock('$lib/api/requests', () => ({
vi.mock('#lib/api/requests.js', () => ({
createRequest: vi.fn().mockResolvedValue({})
}));
const pushToastMock = vi.fn();
vi.mock('$lib/stores/toast.svelte', () => ({
vi.mock('#lib/stores/toast.svelte.js', () => ({
pushToast: (...args: unknown[]) => pushToastMock(...args)
}));
@@ -30,9 +30,9 @@ import {
createSnoozesQuery,
snoozeSuggestion,
unsnoozeSuggestion
} from '$lib/api/suggestions';
import { createRequest } from '$lib/api/requests';
import type { ArtistSuggestion, SuggestionSnooze } from '$lib/api/types';
} from '#lib/api/suggestions.js';
import { createRequest } from '#lib/api/requests.js';
import type { ArtistSuggestion, SuggestionSnooze } from '#lib/api/types.js';
const oneSeed: ArtistSuggestion = {
mbid: 'mb1',
+2 -2
View File
@@ -1,8 +1,8 @@
<script lang="ts">
// Single global toast renderer. Mounted once in +layout.svelte; pages call
// pushToast() from $lib/stores/toast.svelte. The {#key} re-mount on id
// pushToast() from #lib/stores/toast.svelte. The {#key} re-mount on id
// change is what makes screen readers re-announce a replacement toast.
import { toast } from '$lib/stores/toast.svelte';
import { toast } from '#lib/stores/toast.svelte.js';
</script>
{#if toast.value}
+5 -5
View File
@@ -18,11 +18,11 @@
import AddToPlaylistMenu from './AddToPlaylistMenu.svelte';
import TrackMenuItem from './TrackMenuItem.svelte';
import TrackMenuDivider from './TrackMenuDivider.svelte';
import { createLikedIdsQuery, likeEntity, unlikeEntity } from '$lib/api/likes';
import { createMyQuarantineQuery, unflagTrack } from '$lib/api/quarantine';
import { qk } from '$lib/api/queries';
import { playNext, enqueueTrack, playRadio } from '$lib/player/store.svelte';
import type { TrackRef } from '$lib/api/types';
import { createLikedIdsQuery, likeEntity, unlikeEntity } from '#lib/api/likes.js';
import { createMyQuarantineQuery, unflagTrack } from '#lib/api/quarantine.js';
import { qk } from '#lib/api/queries.js';
import { playNext, enqueueTrack, playRadio } from '#lib/player/store.svelte.js';
import type { TrackRef } from '#lib/api/types.js';
let {
track,
+7 -7
View File
@@ -3,7 +3,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { emptyPlaylistsMock } from '../../test-utils/mocks/playlists';
import { emptyQuarantineMock } from '../../test-utils/mocks/quarantine';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
// Mutable mock-user handle so individual tests can flip is_admin / null
// without re-importing the module under test.
@@ -13,7 +13,7 @@ const userState = vi.hoisted(() => ({
| null
}));
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: { get value() { return userState.current; } }
}));
@@ -21,20 +21,20 @@ vi.mock('$app/navigation', () => ({
goto: vi.fn()
}));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$lib/api/quarantine', () => emptyQuarantineMock());
vi.mock('#lib/api/quarantine.js', () => emptyQuarantineMock());
vi.mock('$lib/api/playlists', () => emptyPlaylistsMock());
vi.mock('#lib/api/playlists.js', () => emptyPlaylistsMock());
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
playNext: vi.fn(),
enqueueTrack: vi.fn(),
playRadio: vi.fn()
}));
import TrackMenu from './TrackMenu.svelte';
import { playNext, enqueueTrack, playRadio } from '$lib/player/store.svelte';
import { playNext, enqueueTrack, playRadio } from '#lib/player/store.svelte.js';
const track = makeTrack({ title: 'Roygbiv' });
+5 -5
View File
@@ -1,13 +1,13 @@
<script lang="ts">
import type { TrackRef } from '$lib/api/types';
import { formatDuration } from '$lib/media/duration';
import { player, playQueue, enqueueTrack, playRadio } from '$lib/player/store.svelte';
import type { TrackRef } from '#lib/api/types.js';
import { formatDuration } from '#lib/media/duration.js';
import { player, playQueue, enqueueTrack, playRadio } from '#lib/player/store.svelte.js';
import { Plus, Check } from 'lucide-svelte';
import LikeButton from './LikeButton.svelte';
import TrackMenu from './TrackMenu.svelte';
import {
selection, toggleOne, selectRange
} from '$lib/selection/store.svelte';
} from '#lib/selection/store.svelte.js';
type PlayHandler = (tracks: TrackRef[], index: number) => void;
@@ -76,7 +76,7 @@
data-selected={isSelected}
onclick={onRowClick}
onkeydown={onRowKey}
class="group grid w-full cursor-pointer grid-cols-[32px_1fr_auto_auto_auto_auto_auto] items-center gap-4 px-3 py-2 text-left text-sm odd:bg-surface/50 hover:bg-surface focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent {isCurrent ? 'border-l-2 border-l-accent bg-surface-hover' : ''} {isSelected ? 'bg-surface-hover' : ''}"
class="group grid w-full cursor-pointer grid-cols-[32px_1fr_auto_auto_auto_auto_auto] items-center gap-4 px-3 py-2 text-left text-sm odd:bg-surface/50 hover:bg-surface focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-accent {isCurrent ? 'border-l-2 border-l-accent bg-surface-hover' : ''} {isSelected ? 'bg-surface-hover' : ''}"
>
<span class="relative flex h-6 w-8 items-center justify-end">
<!-- Track number: visible by default; hidden when the row is hovered
+7 -7
View File
@@ -1,24 +1,24 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
import { emptyLikesMock } from '../../test-utils/mocks/likes';
import { emptyQuarantineMock } from '../../test-utils/mocks/quarantine';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
vi.mock('$lib/player/store.svelte', () => ({
vi.mock('#lib/player/store.svelte.js', () => ({
playQueue: vi.fn(),
enqueueTrack: vi.fn(),
playRadio: vi.fn(),
player: { current: undefined }
}));
vi.mock('$lib/api/likes', () => emptyLikesMock());
vi.mock('#lib/api/likes.js', () => emptyLikesMock());
vi.mock('$lib/api/quarantine', () => emptyQuarantineMock());
vi.mock('#lib/api/quarantine.js', () => emptyQuarantineMock());
import TrackRow from './TrackRow.svelte';
import { playQueue, enqueueTrack, playRadio } from '$lib/player/store.svelte';
import { selection, clearSelection } from '$lib/selection/store.svelte';
import { playQueue, enqueueTrack, playRadio } from '#lib/player/store.svelte.js';
import { selection, clearSelection } from '#lib/selection/store.svelte.js';
const tracks: TrackRef[] = [
makeTrack({ title: 'So What', duration_sec: 545 }),
+1 -1
View File
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
import { audioLoader, setAudioLoader, __resetAudioLoader } from './audioLoader';
const sampleTrack: TrackRef = {
+1 -1
View File
@@ -9,7 +9,7 @@
// no-op (the browser handles it via the prefetch <audio> element);
// Tauri can warm its native cache here.
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
export type AudioLoader = {
resolve: (track: TrackRef) => string;
+4 -4
View File
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { flushSync } from 'svelte';
vi.mock('$lib/api/client', () => ({
vi.mock('#lib/api/client.js', () => ({
api: {
post: vi.fn().mockResolvedValue({ play_event_id: 'pe-1', session_id: 's-1' })
}
@@ -19,9 +19,9 @@ import {
reportDuration,
skipNext,
} from './store.svelte';
import { api } from '$lib/api/client';
import type { TrackRef } from '$lib/api/types';
import { makeTrack } from '$test-utils/fixtures/track';
import { api } from '#lib/api/client.js';
import type { TrackRef } from '#lib/api/types.js';
import { makeTrack } from '#test-utils/fixtures/track.js';
function track(id: string, dur = 200): TrackRef {
return makeTrack({
+2 -2
View File
@@ -1,7 +1,7 @@
import { player } from './store.svelte';
import { api } from '$lib/api/client';
import { api } from '#lib/api/client.js';
import { getOrCreateClientId } from './clientId';
import type { PlayStartedResponse, EventOkResponse } from '$lib/api/types';
import type { PlayStartedResponse, EventOkResponse } from '#lib/api/types.js';
// useEventsDispatcher installs $effect-based watchers that emit play events
// to /api/events. Call once from +layout.svelte's mount.
+3 -3
View File
@@ -1,8 +1,8 @@
import { describe, expect, test } from 'vitest';
import { dbToLinear, gainDb, playingAsAlbum, MAX_BOOST_DB } from './gain';
import type { NormalizationPrefs } from '$lib/api/normalization';
import type { TrackRef } from '$lib/api/types';
import { makeTrack } from '$test-utils/fixtures/track';
import type { NormalizationPrefs } from '#lib/api/normalization.js';
import type { TrackRef } from '#lib/api/types.js';
import { makeTrack } from '#test-utils/fixtures/track.js';
const prefs = (over: Partial<NormalizationPrefs> = {}): NormalizationPrefs => ({
mode: 'auto',
+2 -2
View File
@@ -1,8 +1,8 @@
// Loudness normalization math for the web player (M464 #4999). Pure, so the
// rules are tested without an <audio> element or an AudioContext.
import type { NormalizationPrefs, ReplayGain } from '$lib/api/normalization';
import type { TrackRef } from '$lib/api/types';
import type { NormalizationPrefs, ReplayGain } from '#lib/api/normalization.js';
import type { TrackRef } from '#lib/api/types.js';
// The server's gains are to the ReplayGain 2.0 reference.
const REFERENCE_LUFS = -18;
+5 -5
View File
@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import type { TrackRef } from '$lib/api/types';
import { makeTrack } from '$test-utils/fixtures/track';
import type { TrackRef } from '#lib/api/types.js';
import { makeTrack } from '#test-utils/fixtures/track.js';
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: {
get value() {
return { id: 'test-user' };
@@ -11,8 +11,8 @@ vi.mock('$lib/auth/store.svelte', () => ({
}));
const getReplayGain = vi.fn();
vi.mock('$lib/api/normalization', async (orig) => ({
...(await orig<typeof import('$lib/api/normalization')>()),
vi.mock('#lib/api/normalization.js', async (orig) => ({
...(await orig<typeof import('#lib/api/normalization.js')>()),
getReplayGain: (ids: string[]) => getReplayGain(ids)
}));
+2 -2
View File
@@ -10,8 +10,8 @@
// what the track sounded like before leveling existed.
import { SvelteMap } from 'svelte/reactivity';
import { getReplayGain, type ReplayGain } from '$lib/api/normalization';
import { normalization } from '$lib/stores/normalization.svelte';
import { getReplayGain, type ReplayGain } from '#lib/api/normalization.js';
import { normalization } from '#lib/stores/normalization.svelte.js';
import { player } from './store.svelte';
import { dbToLinear, gainDb, playingAsAlbum } from './gain';
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { flushSync } from 'svelte';
import type { TrackRef } from '$lib/api/types';
import { makeTrack } from '$test-utils/fixtures/track';
import type { TrackRef } from '#lib/api/types.js';
import { makeTrack } from '#test-utils/fixtures/track.js';
function track(id: string): TrackRef {
return makeTrack({
+1 -1
View File
@@ -1,4 +1,4 @@
import { coverUrl } from '$lib/media/covers';
import { coverUrl } from '#lib/media/covers.js';
import {
player,
togglePlay,
+1 -1
View File
@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { readPersistedQueue, writePersistedQueue, clearPersistedQueue } from './persisted';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
const sampleTrack = makeTrack();
+1 -1
View File
@@ -1,4 +1,4 @@
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
export type PersistedPlayerState = {
v: 1;
+2 -2
View File
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { flushSync } from 'svelte';
import type { TrackRef } from '$lib/api/types';
import { makeTrack } from '$test-utils/fixtures/track';
import type { TrackRef } from '#lib/api/types.js';
import { makeTrack } from '#test-utils/fixtures/track.js';
function track(id: string): TrackRef {
return makeTrack({
+5 -5
View File
@@ -1,9 +1,9 @@
import type { TrackRef, RadioResponse } from '$lib/api/types';
import type { TrackRef, RadioResponse } from '#lib/api/types.js';
import { untrack } from 'svelte';
import { api } from '$lib/api/client';
import { user } from '$lib/auth/user.svelte';
import { sessionEnd } from '$lib/auth/sessionEnd.svelte';
import * as storage from '$lib/util/safeLocalStorage';
import { api } from '#lib/api/client.js';
import { user } from '#lib/auth/user.svelte.js';
import { sessionEnd } from '#lib/auth/sessionEnd.svelte.js';
import * as storage from '#lib/util/safeLocalStorage.js';
import { readPersistedQueue, writePersistedQueue, clearPersistedQueue } from './persisted';
export type PlayerState = 'idle' | 'loading' | 'playing' | 'paused' | 'error';
+4 -4
View File
@@ -17,11 +17,11 @@ import {
__setShuffleRng,
restoreQueue
} from './store.svelte';
import type { TrackRef } from '$lib/api/types';
import type { TrackRef } from '#lib/api/types.js';
import { writePersistedQueue, clearPersistedQueue } from './persisted';
import { makeTrack } from '$test-utils/fixtures/track';
import { makeTrack } from '#test-utils/fixtures/track.js';
vi.mock('$lib/auth/store.svelte', () => ({
vi.mock('#lib/auth/store.svelte.js', () => ({
user: {
get value() {
return { id: 'test-user' };
@@ -345,7 +345,7 @@ import {
toggleQueueDrawer,
closeQueueDrawer
} from './store.svelte';
import * as client from '$lib/api/client';
import * as client from '#lib/api/client.js';
describe('player store — enqueue + playRadio', () => {
test('enqueueTrack appends to a non-empty queue, _index unchanged', () => {
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest';
import type { PlaylistTrack } from '$lib/api/types';
import type { PlaylistTrack } from '#lib/api/types.js';
import { playlistTrackToRef } from './playlistTrackToRef';
const row: PlaylistTrack = {
+1 -1
View File
@@ -11,7 +11,7 @@
// Callers filter nulls, which is what makes a missing file "get skipped"
// everywhere a queue is built rather than at each call site.
import type { PlaylistTrack, TrackRef } from '$lib/api/types';
import type { PlaylistTrack, TrackRef } from '#lib/api/types.js';
export function playlistTrackToRef(row: PlaylistTrack): TrackRef | null {
if (!row.track_id || row.unavailable) return null;

Some files were not shown because too many files have changed in this diff Show More