feat(wails): move the frontend onto v3's generated bindings
frontend/wailsjs/ is deleted and frontend/bindings/ takes its place — a real TypeScript module tree nested by Go import path, generated by wails3's static analyser rather than by building the app and running it. The @go alias absorbs the constant prefix, so a call site imports '@go/library/library.js' and the codemod over all 93 sites was a specifier rewrite plus splitting @go/models' namespaces into one import per package. The 12 SetContext bindings and the fake `context` model are gone, as Phase 2's ServiceStartup port promised: 272 methods across 12 services, none of them plumbing. @runtime/runtime is now a local shim (src/wails/runtime.ts) over @wailsio/runtime, so the 22 EventsOn imports are untouched. It unwraps v3's WailsEvent into v2's callback shape, which is exact here: nothing in backend/events passes more than one data argument, and v3 only packs arguments into a slice when there is more than one. v3 tells the truth about two things v2 lied about, and that is most of the diff. A Go nil slice really does arrive as JSON null, and a Go named string type really is an enum; v2 typed them as T[] and string. utils/binding.ts states the app's actual contract — an absent list is an empty list — once, at the boundary where it is true, and also drops the CancellablePromise the app never cancels. Four test fixtures widen an enum field back to its value union. Not done, and Phase 5's to fix: frontend/test/support/wails-fake.ts still fakes window.go, which v3 does not have, so `make ui-test` is broken and harness.test.ts fails to compile on EventsEmit. That test also asserts v2 ordering that no longer holds — v3's Events.Emit calls the backend and does not notify in-page listeners at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm
This commit is contained in:
@@ -4,13 +4,13 @@ import {
|
||||
property,
|
||||
state,
|
||||
} from 'lit/decorators.js';
|
||||
import { library } from '@go/models';
|
||||
import * as library from '@go/library/models.js';
|
||||
import { LibraryController } from '@store/controllers/library-controller';
|
||||
import {
|
||||
GetArtistImageURL,
|
||||
GetArtistImageCachedPath,
|
||||
GetArtistMBID,
|
||||
} from '@go/explore/Service';
|
||||
} from '@go/explore/service.js';
|
||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||
import '@components/cover-grid/cover-grid.js';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
GetAlbumsByArtist,
|
||||
GetAlbumsByArtistByLibrary,
|
||||
GetFilePathsByAlbums,
|
||||
} from '@go/library/Library';
|
||||
import { library } from '@go/models';
|
||||
} from '@go/library/library.js';
|
||||
import * as library from '@go/library/models.js';
|
||||
import { LibraryController } from '@store/controllers/library-controller';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import { SearchController } from '@store/controllers/search-controller';
|
||||
@@ -36,6 +36,7 @@ import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
|
||||
import '@components/playlist-picker/playlist-picker.js';
|
||||
import { dict, list } from '@utils/binding';
|
||||
|
||||
/** Pixels to change card width per scroll tick. */
|
||||
const ZOOM_STEP = 16;
|
||||
@@ -1055,18 +1056,21 @@ export class ArtistsView
|
||||
const libId =
|
||||
this.libraryCtrl.selectedLibraryId;
|
||||
|
||||
const albums = libId !== null
|
||||
? await GetAlbumsByArtistByLibrary(
|
||||
artist.ID,
|
||||
libId,
|
||||
)
|
||||
: await GetAlbumsByArtist(artist.ID);
|
||||
const albums = await list(
|
||||
libId !== null
|
||||
? GetAlbumsByArtistByLibrary(
|
||||
artist.ID,
|
||||
libId,
|
||||
)
|
||||
: GetAlbumsByArtist(artist.ID),
|
||||
);
|
||||
|
||||
const byAlbum =
|
||||
await GetFilePathsByAlbums(
|
||||
const byAlbum = await dict(
|
||||
GetFilePathsByAlbums(
|
||||
albums.map((a) => a.ID),
|
||||
libId ?? 0,
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
const allPaths: string[] = [];
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
ClearCompletedEntries,
|
||||
SearchCandidates,
|
||||
SelectSearchCandidate,
|
||||
} from '@go/autotagservice/Service';
|
||||
import type { autotagservice } from '@go/models';
|
||||
} from '@go/autotagservice/service.js';
|
||||
import type * as autotagservice from '@go/autotagservice/models.js';
|
||||
import { EventsOn } from '@runtime/runtime';
|
||||
import { Events } from '../../events';
|
||||
import { inlineDiff, normalizeStrict, isCosmeticDiff } from '../../utils/text-diff';
|
||||
@@ -29,6 +29,7 @@ import { nameDialogsIn } from '../../utils/name-dialog';
|
||||
import { ViewLifecycleMixin } from '../../utils/view-lifecycle';
|
||||
import { confirmAction } from '../confirm-dialog/confirm-dialog';
|
||||
import '@awesome.me/webawesome/dist/components/dialog/dialog.js';
|
||||
import { list } from '@utils/binding';
|
||||
|
||||
type PendingItem = autotagservice.PendingItem;
|
||||
type ScoreView = autotagservice.ScoreView;
|
||||
@@ -1483,8 +1484,10 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
this.searchError = '';
|
||||
this.searchRan = true;
|
||||
try {
|
||||
this.searchResults = await SearchCandidates(
|
||||
this.searchKind, query, this.searchArtist.trim(),
|
||||
this.searchResults = await list(
|
||||
SearchCandidates(
|
||||
this.searchKind, query, this.searchArtist.trim(),
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('autotag: candidate search failed', err);
|
||||
@@ -1755,12 +1758,12 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
}
|
||||
|
||||
private topScore(): number {
|
||||
if (!this.score || this.score.candidates.length === 0) return 0;
|
||||
return this.score.candidates[0]?.score ?? 0;
|
||||
if (!this.score || (this.score.candidates ?? []).length === 0) return 0;
|
||||
return (this.score.candidates ?? [])[0]?.score ?? 0;
|
||||
}
|
||||
|
||||
private async onApply(): Promise<void> {
|
||||
if (!this.current || !this.score || this.score.candidates.length === 0) return;
|
||||
if (!this.current || !this.score || (this.score.candidates ?? []).length === 0) return;
|
||||
if (!this.hasLibraryWarningBeenAcked(this.current.libraryId)) {
|
||||
await this.confirmWarningThenApply();
|
||||
|
||||
@@ -1773,7 +1776,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
private async executeApply(): Promise<void> {
|
||||
if (!this.current || !this.score) return;
|
||||
|
||||
const cand = this.score.candidates[this.selectedCandidateIdx];
|
||||
const cand = (this.score.candidates ?? [])[this.selectedCandidateIdx];
|
||||
if (!cand) {
|
||||
this.errorMessage = 'No candidate selected.';
|
||||
return;
|
||||
@@ -1781,7 +1784,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
|
||||
const groupKey = this.current.groupKey;
|
||||
const mbid = cand.releaseMbid || cand.releaseGroupMbid;
|
||||
const total = this.score.localTracks.length;
|
||||
const total = (this.score.localTracks ?? []).length;
|
||||
|
||||
// Pre-mark the folder as running so the sidebar icon flips
|
||||
// to the progress ring immediately — the Started event will
|
||||
@@ -1955,16 +1958,16 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
*/
|
||||
private currentCandidate(): CandidateView | null {
|
||||
if (!this.score) return null;
|
||||
return this.score.candidates[this.selectedCandidateIdx] ?? null;
|
||||
return (this.score.candidates ?? [])[this.selectedCandidateIdx] ?? null;
|
||||
}
|
||||
|
||||
private async selectCandidateByIdx(idx: number): Promise<void> {
|
||||
if (!this.score || idx < 0 || idx >= this.score.candidates.length) return;
|
||||
if (!this.score || idx < 0 || idx >= (this.score.candidates ?? []).length) return;
|
||||
this.selectedCandidateIdx = idx;
|
||||
// Lazy-load cover art for non-top candidates. The backend
|
||||
// populates art for the top one eagerly; everything else
|
||||
// arrives empty until the user picks it.
|
||||
const cand = this.score.candidates[idx]!;
|
||||
const cand = (this.score.candidates ?? [])[idx]!;
|
||||
if (!cand.coverArtUrl) {
|
||||
try {
|
||||
const url = await GetCandidateCoverArt(
|
||||
@@ -2252,7 +2255,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
if (!cluster || cluster.candidates.length <= 1) return nothing;
|
||||
|
||||
const editions = cluster.candidates
|
||||
.map((c) => ({ cand: c, idx: this.score?.candidates.indexOf(c) ?? -1 }))
|
||||
.map((c) => ({ cand: c, idx: (this.score?.candidates ?? []).indexOf(c) ?? -1 }))
|
||||
.filter((e) => e.idx >= 0);
|
||||
|
||||
return html`
|
||||
@@ -2316,11 +2319,11 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
// the score forgives, the UI mutes.
|
||||
const SUBTLE_LENGTH_MAX_MS = 5000;
|
||||
|
||||
for (const a of cand.alignments) {
|
||||
for (const a of (cand.alignments ?? [])) {
|
||||
if (a.status === 'matched' || a.status === 'mismatched') {
|
||||
paired++;
|
||||
const local = a.localIndex >= 0
|
||||
? this.score?.localTracks[a.localIndex] ?? null
|
||||
? (this.score?.localTracks ?? [])[a.localIndex] ?? null
|
||||
: null;
|
||||
if (local) {
|
||||
if (local.title !== a.candidateTitle) {
|
||||
@@ -2374,7 +2377,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
missingTitles.push(a.candidateTitle || '(untitled)');
|
||||
} else if (a.status === 'unmatched') {
|
||||
const local = a.localIndex >= 0
|
||||
? this.score?.localTracks[a.localIndex] ?? null
|
||||
? (this.score?.localTracks ?? [])[a.localIndex] ?? null
|
||||
: null;
|
||||
extraTitles.push(a.localTitle || local?.title || '(untitled)');
|
||||
}
|
||||
@@ -2659,7 +2662,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
}
|
||||
|
||||
private renderLowConfidenceBanner(clusters: VersionCluster[]) {
|
||||
const top = this.score?.candidates[0];
|
||||
const top = (this.score?.candidates ?? [])[0];
|
||||
if (!top) return nothing;
|
||||
if (top.score >= LOW_CONFIDENCE_THRESHOLD) return nothing;
|
||||
if (clusters.length < 2) return nothing;
|
||||
@@ -2695,18 +2698,18 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
* renderMatchDetails, so each column shows its own values plainly.
|
||||
*/
|
||||
private renderComparison(cand: CandidateView, clusters: VersionCluster[] = []) {
|
||||
const locals = this.score?.localTracks ?? [];
|
||||
const locals = (this.score?.localTracks ?? []) ?? [];
|
||||
|
||||
// localIndex -> its alignment, so a folder row knows whether it
|
||||
// paired and (if so) how confidently.
|
||||
const alignByLocal = new Map<number, AlignmentView>();
|
||||
for (const a of cand.alignments) {
|
||||
for (const a of (cand.alignments ?? [])) {
|
||||
if (a.localIndex >= 0) alignByLocal.set(a.localIndex, a);
|
||||
}
|
||||
|
||||
// Candidate side: every alignment that has a candidate track
|
||||
// (paired or missing-from-folder), in candidate order.
|
||||
const candRows = cand.alignments
|
||||
const candRows = (cand.alignments ?? [])
|
||||
.filter((a) => a.status !== 'unmatched' && a.candidatePosition > 0)
|
||||
.slice()
|
||||
.sort((x, y) =>
|
||||
@@ -2856,7 +2859,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
${clusters.map((cluster) => {
|
||||
const best = cluster.candidates[cluster.bestIdx]!;
|
||||
const active = cluster.candidates.includes(activeCand);
|
||||
const idx = this.score?.candidates.indexOf(best) ?? -1;
|
||||
const idx = (this.score?.candidates ?? []).indexOf(best) ?? -1;
|
||||
return html`
|
||||
<div class="cand-chip ${active ? 'selected' : ''}"
|
||||
title=${cluster.label}
|
||||
@@ -2949,7 +2952,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
if (!this.score || this.score.candidates.length === 0) {
|
||||
if (!this.score || (this.score.candidates ?? []).length === 0) {
|
||||
return html`
|
||||
<div class="main">
|
||||
<div class="empty">
|
||||
@@ -2965,7 +2968,7 @@ export class AutotagView extends ViewLifecycleMixin(LitElement) {
|
||||
return html`<div class="main"><div class="empty">Candidate index out of range.</div></div>`;
|
||||
}
|
||||
|
||||
const clusters = this.clusterVersions(this.score.candidates);
|
||||
const clusters = this.clusterVersions((this.score.candidates ?? []));
|
||||
|
||||
return html`
|
||||
<div class="main">
|
||||
|
||||
@@ -2,14 +2,14 @@ import { LitElement, html, css, nothing } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { EventsOn } from '@runtime/runtime';
|
||||
import type { explore } from '@go/models';
|
||||
import type * as explore from '@go/explore/models.js';
|
||||
import {
|
||||
AddLibrary,
|
||||
RenameLibrary,
|
||||
RemoveLibrary,
|
||||
GetRemovalImpact,
|
||||
GetAllLibrariesWithTrackCounts,
|
||||
} from '@go/library/Library';
|
||||
} from '@go/library/library.js';
|
||||
import {
|
||||
GetScanConcurrency,
|
||||
SetScanConcurrency,
|
||||
@@ -17,17 +17,17 @@ import {
|
||||
SetDefaultPage,
|
||||
GetQueueFallback,
|
||||
SetQueueFallback,
|
||||
} from '@go/config/Config';
|
||||
import { GetIndexStatus } from '@go/explore/Service';
|
||||
import { DirectoryPicker } from '@go/frontendutil/FrontendUtil';
|
||||
} from '@go/config/config.js';
|
||||
import { GetIndexStatus } from '@go/explore/service.js';
|
||||
import { DirectoryPicker } from '@go/frontendutil/frontendutil.js';
|
||||
import { notificationStore } from '@store/notification-store';
|
||||
import { describeError, explainError } from '@utils/describe-error';
|
||||
import type { library } from '@go/models';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import { ThemeController } from '@store/controllers/theme-controller';
|
||||
import { TrackListController } from '@store/controllers/tracklist-controller';
|
||||
import { FavoritesController } from '@store/controllers/favorites-controller';
|
||||
import { GetAllPlaylists } from '@go/playlist/Service';
|
||||
import type { playlist } from '@go/models';
|
||||
import { GetAllPlaylists } from '@go/playlist/service.js';
|
||||
import type * as playlist from '@go/playlist/models.js';
|
||||
import { Events } from '../../events';
|
||||
import {
|
||||
SHORTCUT_CATEGORIES,
|
||||
@@ -49,6 +49,7 @@ import './shortcut-capture';
|
||||
import { confirmAction } from '../confirm-dialog/confirm-dialog';
|
||||
import { shortcutsStore } from '../../store/shortcuts-store';
|
||||
import { ShortcutsController } from '../../store/controllers/shortcuts-controller';
|
||||
import { list } from '@utils/binding';
|
||||
|
||||
const SCROLL_STORAGE_KEY = 'yj-now-playing-scroll-mode';
|
||||
const SCROLL_CHANGE_EVENT = 'yj-scroll-mode-changed';
|
||||
@@ -1012,9 +1013,9 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
||||
const ok = await confirmAction({
|
||||
title: 'Remove library',
|
||||
message: `Remove “${libName}”?`,
|
||||
impact: `This deletes ${impact.trackCount} tracks, affects `
|
||||
+ `${impact.playlistsAffected} playlists and removes `
|
||||
+ `${impact.queueItemCount} queue items.`,
|
||||
impact: `This deletes ${impact?.trackCount ?? 0} tracks, affects `
|
||||
+ `${impact?.playlistsAffected ?? 0} playlists and removes `
|
||||
+ `${impact?.queueItemCount ?? 0} queue items.`,
|
||||
confirmLabel: 'Remove',
|
||||
danger: true,
|
||||
});
|
||||
@@ -1187,8 +1188,7 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
||||
|
||||
private async loadPlaylists(): Promise<void> {
|
||||
try {
|
||||
this.playlists =
|
||||
await GetAllPlaylists();
|
||||
this.playlists = await list(GetAllPlaylists());
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Failed to load playlists:',
|
||||
@@ -1456,10 +1456,10 @@ export class ConfigPage extends ViewLifecycleMixin(LitElement) {
|
||||
<span class="index-stat">updated ${this.timeAgo(s.lastBuilt)}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
${s.tiers?.length > 0 && s.tiers.some((t) => t.state === 'running' || t.state === 'pending' || t.state === 'error')
|
||||
${(s.tiers?.length ?? 0) > 0 && (s.tiers ?? []).some((t) => t.state === 'running' || t.state === 'pending' || t.state === 'error')
|
||||
? html`
|
||||
<div class="index-tiers">
|
||||
${s.tiers.map(
|
||||
${(s.tiers ?? []).map(
|
||||
(t) => html`
|
||||
<div class="index-tier">
|
||||
<span class="tier-icon">${this.tierIcon(t.state)}</span>
|
||||
|
||||
@@ -14,10 +14,12 @@ import type {
|
||||
ProviderField,
|
||||
} from '@store/download-store';
|
||||
import { downloadStore } from '@store/download-store';
|
||||
import { DirectoryPicker } from '@go/frontendutil/FrontendUtil';
|
||||
import { GetDownloadPreferences, SetDownloadPreferences } from '@go/config/Config';
|
||||
import { SetPreferences } from '@go/download/Service';
|
||||
import type { download } from '@go/models';
|
||||
import { DirectoryPicker } from '@go/frontendutil/frontendutil.js';
|
||||
import { GetDownloadPreferences, SetDownloadPreferences } from '@go/config/config.js';
|
||||
import { SetPreferences } from '@go/download/service.js';
|
||||
import type * as download from '@go/download/models.js';
|
||||
import { Format } from '@go/download/models.js';
|
||||
import { compact } from '@utils/binding';
|
||||
import { describeError, explainError } from '@utils/describe-error';
|
||||
import { confirmAction } from '@components/confirm-dialog/confirm-dialog';
|
||||
import './config-section';
|
||||
@@ -28,15 +30,15 @@ import './config-section';
|
||||
* deliberately excluded — it names "no format detected", not a format a
|
||||
* user could opt into.
|
||||
*/
|
||||
const AUTO_DOWNLOAD_FORMATS: { value: string; label: string }[] = [
|
||||
{ value: 'flac', label: 'FLAC' },
|
||||
{ value: 'alac', label: 'ALAC' },
|
||||
{ value: 'wav', label: 'WAV' },
|
||||
{ value: 'mp3', label: 'MP3' },
|
||||
{ value: 'aac', label: 'AAC' },
|
||||
{ value: 'ogg', label: 'OGG' },
|
||||
{ value: 'opus', label: 'Opus' },
|
||||
{ value: 'wma', label: 'WMA' },
|
||||
const AUTO_DOWNLOAD_FORMATS: { value: Format; label: string }[] = [
|
||||
{ value: Format.FormatFLAC, label: 'FLAC' },
|
||||
{ value: Format.FormatALAC, label: 'ALAC' },
|
||||
{ value: Format.FormatWAV, label: 'WAV' },
|
||||
{ value: Format.FormatMP3, label: 'MP3' },
|
||||
{ value: Format.FormatAAC, label: 'AAC' },
|
||||
{ value: Format.FormatOGG, label: 'OGG' },
|
||||
{ value: Format.FormatOpus, label: 'Opus' },
|
||||
{ value: Format.FormatWMA, label: 'WMA' },
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -607,7 +609,7 @@ export class DownloadClients extends LitElement {
|
||||
// Secrets are never sent back to the frontend, so their fields
|
||||
// start blank; a blank secret on save means "leave it alone"
|
||||
// rather than "clear it".
|
||||
this.draft = { ...(provider.settings ?? {}) };
|
||||
this.draft = compact(provider.settings);
|
||||
}
|
||||
|
||||
private cancelEdit = () => {
|
||||
@@ -748,7 +750,7 @@ export class DownloadClients extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private toggleFormat(format: string, checked: boolean): void {
|
||||
private toggleFormat(format: Format, checked: boolean): void {
|
||||
const current = this.prefs.allowedFormats ?? [];
|
||||
const allowedFormats = checked
|
||||
? [...current, format]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LitElement, html, svg, css } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import type { library } from '@go/models';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import { PlayerController } from '@store/controllers/player-controller';
|
||||
import { FavoritesController } from '@store/controllers/favorites-controller';
|
||||
import { formatMilliseconds } from '@utils/time';
|
||||
|
||||
@@ -2,9 +2,10 @@ import {
|
||||
GetAlbumTracks,
|
||||
GetAlbumTracksByLibrary,
|
||||
GetFilePathsByAlbums,
|
||||
} from '@go/library/Library';
|
||||
} from '@go/library/library.js';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import type { library } from '@go/models';
|
||||
import { dict, list } from '@utils/binding';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import type { CoverArtUrls } from '@components/track-details/track-details.js';
|
||||
|
||||
/**
|
||||
@@ -58,9 +59,11 @@ export class AlbumSelectionManager {
|
||||
const libId =
|
||||
libraryStore.getSelectedLibraryId();
|
||||
|
||||
return libId !== null
|
||||
? GetAlbumTracksByLibrary(albumId, libId)
|
||||
: GetAlbumTracks(albumId);
|
||||
return list(
|
||||
libId !== null
|
||||
? GetAlbumTracksByLibrary(albumId, libId)
|
||||
: GetAlbumTracks(albumId),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +88,7 @@ export class AlbumSelectionManager {
|
||||
const libId =
|
||||
libraryStore.getSelectedLibraryId();
|
||||
|
||||
return GetFilePathsByAlbums(ids, libId ?? 0);
|
||||
return dict(GetFilePathsByAlbums(ids, libId ?? 0));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { library } from '@go/models';
|
||||
import type * as library from '@go/library/models.js';
|
||||
|
||||
/**
|
||||
* Discriminated context menu target so we know whether the
|
||||
|
||||
@@ -14,8 +14,8 @@ import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
|
||||
import {
|
||||
GetAlbumTracks,
|
||||
GetAlbumTracksByLibrary,
|
||||
} from '@go/library/Library';
|
||||
import { library } from '@go/models';
|
||||
} from '@go/library/library.js';
|
||||
import * as library from '@go/library/models.js';
|
||||
import { LibraryController } from '@store/controllers/library-controller';
|
||||
import { SearchController } from '@store/controllers/search-controller';
|
||||
import { ViewLifecycleMixin } from '@utils/view-lifecycle';
|
||||
@@ -74,6 +74,7 @@ import type {
|
||||
GridEntry,
|
||||
SortDirection,
|
||||
} from './cover-grid-types.js';
|
||||
import { list } from '@utils/binding';
|
||||
|
||||
@customElement('cover-grid')
|
||||
export class CoverGrid
|
||||
@@ -933,12 +934,14 @@ export class CoverGrid
|
||||
const libId =
|
||||
this.libraryCtrl.selectedLibraryId;
|
||||
|
||||
const tracks = libId !== null
|
||||
? await GetAlbumTracksByLibrary(
|
||||
album.ID,
|
||||
libId,
|
||||
)
|
||||
: await GetAlbumTracks(album.ID);
|
||||
const tracks = await list(
|
||||
libId !== null
|
||||
? GetAlbumTracksByLibrary(
|
||||
album.ID,
|
||||
libId,
|
||||
)
|
||||
: GetAlbumTracks(album.ID),
|
||||
);
|
||||
|
||||
if (this.expandedAlbumId === album.ID) {
|
||||
this.expandedTracks = tracks;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { LitElement } from 'lit';
|
||||
import type { LitVirtualizer } from '@lit-labs/virtualizer';
|
||||
import type { library } from '@go/models';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import type { LibraryController } from '@store/controllers/library-controller';
|
||||
|
||||
import type { GridEntry } from './cover-grid-types.js';
|
||||
|
||||
@@ -7,7 +7,7 @@ import '@awesome.me/webawesome/dist/components/callout/callout.js';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
import type { DownloadCandidate } from '@store/download-store';
|
||||
import { downloadStore } from '@store/download-store';
|
||||
import type { download } from '@go/models';
|
||||
import type * as download from '@go/download/models.js';
|
||||
import './candidate-row';
|
||||
import { explainError } from '@utils/describe-error';
|
||||
import { nameDialogsIn } from '@utils/name-dialog';
|
||||
|
||||
@@ -686,7 +686,7 @@ export class DownloadsView extends ViewLifecycleMixin(LitElement) {
|
||||
/** Provider/progress summary for a download's second line. */
|
||||
private downloadDetail(view: DownloadRecord): string {
|
||||
const providers = [
|
||||
...new Set(view.items.map((item) => item.candidate?.origin).filter(Boolean)),
|
||||
...new Set((view.items ?? []).map((item) => item.candidate?.origin).filter(Boolean)),
|
||||
];
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
@@ -3,7 +3,7 @@ import { customElement, state, query } from 'lit/decorators.js';
|
||||
import '@awesome.me/webawesome/dist/components/dialog/dialog.js';
|
||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||
import '@awesome.me/webawesome/dist/components/switch/switch.js';
|
||||
import { AddTracksToPlaylist } from '@go/playlist/Service';
|
||||
import { AddTracksToPlaylist } from '@go/playlist/service.js';
|
||||
import { formatMilliseconds } from '@utils/time';
|
||||
import { nameDialogsIn } from '@utils/name-dialog';
|
||||
|
||||
|
||||
@@ -6,15 +6,16 @@ import {
|
||||
LookupReleaseGroup,
|
||||
BrowseReleases,
|
||||
GetThumbnail,
|
||||
} from '@go/explore/Service';
|
||||
} from '@go/explore/service.js';
|
||||
import {
|
||||
GetAlbumTracks,
|
||||
GetAlbumCompleteness,
|
||||
GetFilePathsByAlbums,
|
||||
GetFilePathsByRecordingMBIDs,
|
||||
} from '@go/library/Library';
|
||||
import { library } from '@go/models';
|
||||
import type { download, explore } from '@go/models';
|
||||
} from '@go/library/library.js';
|
||||
import * as library from '@go/library/models.js';
|
||||
import type * as download from '@go/download/models.js';
|
||||
import type * as explore from '@go/explore/models.js';
|
||||
type MBReleaseGroup = explore.MBReleaseGroup;
|
||||
type MBRelease = explore.MBRelease;
|
||||
type MBTrack = explore.MBTrack;
|
||||
@@ -46,6 +47,7 @@ import type { ContextMenuHost } from '@utils/context-menu-controller.js';
|
||||
import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
|
||||
import { dict, dictByName } from '@utils/binding';
|
||||
|
||||
/**
|
||||
* The region the album header's own failures are rendered in.
|
||||
@@ -2248,9 +2250,8 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
|
||||
// to an empty set and the Play button silently does nothing.
|
||||
// That is exactly what the first version of this did.
|
||||
if (this.localAlbumId > 0) {
|
||||
const byAlbum = await GetFilePathsByAlbums(
|
||||
[this.localAlbumId],
|
||||
libraryID,
|
||||
const byAlbum = await dict(
|
||||
GetFilePathsByAlbums([this.localAlbumId], libraryID),
|
||||
);
|
||||
|
||||
return byAlbum[this.localAlbumId] ?? [];
|
||||
@@ -2266,7 +2267,9 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
|
||||
|
||||
if (mbids.length === 0) return [];
|
||||
|
||||
const byMBID = await GetFilePathsByRecordingMBIDs(mbids, libraryID);
|
||||
const byMBID = await dictByName(
|
||||
GetFilePathsByRecordingMBIDs(mbids, libraryID),
|
||||
);
|
||||
|
||||
// Walked in tracklist order rather than flattened, because the
|
||||
// grouping is what lets the caller keep its own order. A
|
||||
@@ -2349,7 +2352,9 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost {
|
||||
if (!track.inLibrary || !track.mbid) return null;
|
||||
|
||||
const libraryID = libraryStore.getSelectedLibraryId() ?? 0;
|
||||
const byMBID = await GetFilePathsByRecordingMBIDs([track.mbid], libraryID);
|
||||
const byMBID = await dictByName(
|
||||
GetFilePathsByRecordingMBIDs([track.mbid], libraryID),
|
||||
);
|
||||
|
||||
return byMBID[track.mbid]?.[0] ?? null;
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
GetTrackThumbnails,
|
||||
ResolveReleaseGroupMBIDs,
|
||||
PrefetchReleases,
|
||||
} from '@go/explore/Service';
|
||||
import type { explore } from '@go/models';
|
||||
} from '@go/explore/service.js';
|
||||
import type * as explore from '@go/explore/models.js';
|
||||
type MBArtist = explore.MBArtist;
|
||||
type MBReleaseGroup = explore.MBReleaseGroup;
|
||||
type LBTopRecording = explore.LBTopRecording;
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
GetAlbumsByArtist,
|
||||
GetFilePathsByAlbums,
|
||||
GetFilePathsByRecordingMBIDs,
|
||||
} from '@go/library/Library';
|
||||
} from '@go/library/library.js';
|
||||
import { EventsOn } from '@runtime/runtime';
|
||||
import { Events } from '../../events';
|
||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||
@@ -55,6 +55,7 @@ import type { ContextMenuHost } from '@utils/context-menu-controller.js';
|
||||
import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
|
||||
import { dict, dictByName } from '@utils/binding';
|
||||
|
||||
/* ── Constants ── */
|
||||
|
||||
@@ -1423,7 +1424,7 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost
|
||||
let mapping: Record<string, string> = {};
|
||||
if (caaMbids.length > 0) {
|
||||
try {
|
||||
mapping = (await ResolveReleaseGroupMBIDs(caaMbids)) || {};
|
||||
mapping = await dictByName(ResolveReleaseGroupMBIDs(caaMbids));
|
||||
} catch (err) {
|
||||
console.warn('[explore-artist] failed to resolve release groups for tracks:', err);
|
||||
}
|
||||
@@ -1475,7 +1476,7 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost
|
||||
// Phase 1: batched cached lookup.
|
||||
let cached: Record<string, string> = {};
|
||||
try {
|
||||
cached = (await GetTrackThumbnails(requests)) || {};
|
||||
cached = await dictByName(GetTrackThumbnails(requests));
|
||||
if (Object.keys(cached).length > 0) {
|
||||
const updated = new Map(this.trackThumbnails);
|
||||
for (const [key, url] of Object.entries(cached)) {
|
||||
@@ -1684,7 +1685,7 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost
|
||||
artistName: i.artistName,
|
||||
}));
|
||||
|
||||
cached = (await GetThumbnails(requests)) || {};
|
||||
cached = await dictByName(GetThumbnails(requests));
|
||||
if (Object.keys(cached).length > 0) {
|
||||
const updated = new Map(this.thumbnailURLs);
|
||||
for (const [mbid, url] of Object.entries(cached)) {
|
||||
@@ -1927,7 +1928,7 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost
|
||||
if (albumIds.length === 0) return [];
|
||||
|
||||
const libraryID = libraryStore.getSelectedLibraryId() ?? 0;
|
||||
const byAlbum = await GetFilePathsByAlbums(albumIds, libraryID);
|
||||
const byAlbum = await dict(GetFilePathsByAlbums(albumIds, libraryID));
|
||||
const paths: string[] = [];
|
||||
|
||||
for (const id of albumIds) paths.push(...(byAlbum[id] ?? []));
|
||||
@@ -1996,7 +1997,9 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost
|
||||
if (!(track.inLibrary || track.localId) || !track.recordingMbid) return null;
|
||||
|
||||
const libraryID = libraryStore.getSelectedLibraryId() ?? 0;
|
||||
const byMBID = await GetFilePathsByRecordingMBIDs([track.recordingMbid], libraryID);
|
||||
const byMBID = await dictByName(
|
||||
GetFilePathsByRecordingMBIDs([track.recordingMbid], libraryID),
|
||||
);
|
||||
|
||||
return byMBID[track.recordingMbid]?.[0] ?? null;
|
||||
}
|
||||
@@ -2145,7 +2148,9 @@ export class ExploreArtistDetails extends LitElement implements ContextMenuHost
|
||||
if (release.localId <= 0) return [];
|
||||
|
||||
const libraryID = libraryStore.getSelectedLibraryId() ?? 0;
|
||||
const byAlbum = await GetFilePathsByAlbums([release.localId], libraryID);
|
||||
const byAlbum = await dict(
|
||||
GetFilePathsByAlbums([release.localId], libraryID),
|
||||
);
|
||||
|
||||
return byAlbum[release.localId] ?? [];
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import { classMap } from 'lit/directives/class-map.js';
|
||||
import '@components/page-header/page-header';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
import { srOnly } from '../../styles/sr-only.css';
|
||||
import { SearchLocal, SearchLyrics, GetThumbnail, GetThumbnails, GetArtistImageURL, GetArtistImagesCachedPaths, GetExploreShelves, RecordSearchClick } from '@go/explore/Service';
|
||||
import { GetFilePathsByAlbums, GetFilePathsByRecordingMBIDs } from '@go/library/Library';
|
||||
import { SearchLocal, SearchLyrics, GetThumbnail, GetThumbnails, GetArtistImageURL, GetArtistImagesCachedPaths, GetExploreShelves, RecordSearchClick } from '@go/explore/service.js';
|
||||
import { GetFilePathsByAlbums, GetFilePathsByRecordingMBIDs } from '@go/library/library.js';
|
||||
import { EventsOn } from '@runtime/runtime';
|
||||
import { Events } from '../../events';
|
||||
import { libraryStore } from '../../store/library-store';
|
||||
@@ -21,7 +21,7 @@ import { describeError } from '../../utils/describe-error';
|
||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||
import '../library-status-indicator/library-status-indicator.js';
|
||||
import '../top-results-row/top-results-row.js';
|
||||
import { explore } from '@go/models';
|
||||
import * as explore from '@go/explore/models.js';
|
||||
import { ViewLifecycleMixin } from '../../utils/view-lifecycle';
|
||||
import { registerCacheProbe } from '../../utils/cache-stats';
|
||||
import { LRUMap } from '../../utils/lru-map';
|
||||
@@ -34,6 +34,7 @@ import type { ContextMenuHost } from '@utils/context-menu-controller.js';
|
||||
import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
|
||||
import { dict, dictByName } from '@utils/binding';
|
||||
|
||||
/** The region explore's own action failures (play/queue) are rendered in. */
|
||||
export const ExploreRegion = 'explore';
|
||||
@@ -833,10 +834,10 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte
|
||||
// a state this view already renders honestly. A *rejected*
|
||||
// call still reaches the catch below.
|
||||
if (!page || !Array.isArray(page.shelves)) {
|
||||
this.shelves = explore.ShelfPage.createFrom({
|
||||
this.shelves = {
|
||||
shelves: [],
|
||||
state: 'no-index',
|
||||
});
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -853,10 +854,10 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte
|
||||
// Inline, and quietly: the search box above still works, so
|
||||
// this is a panel that could not fill itself rather than
|
||||
// something the user asked for and did not get.
|
||||
this.shelves = explore.ShelfPage.createFrom({
|
||||
this.shelves = {
|
||||
shelves: [],
|
||||
state: 'no-index',
|
||||
});
|
||||
};
|
||||
} finally {
|
||||
this.shelvesPending = false;
|
||||
}
|
||||
@@ -1043,13 +1044,12 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte
|
||||
// index has no hits, in which case we keep the owned-library
|
||||
// matches already displayed.
|
||||
const result =
|
||||
(await SearchLocal(query)) ??
|
||||
explore.MBSearchResult.createFrom({
|
||||
(await SearchLocal(query)) ?? {
|
||||
artists: [],
|
||||
releaseGroups: [],
|
||||
recordings: [],
|
||||
topResults: [],
|
||||
});
|
||||
};
|
||||
|
||||
// Discard stale response
|
||||
if (version !== this.searchVersion) {
|
||||
@@ -1131,14 +1131,16 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte
|
||||
*/
|
||||
private async albumFilePaths(localId: number): Promise<string[]> {
|
||||
const libraryID = libraryStore.getSelectedLibraryId() ?? 0;
|
||||
const byAlbum = await GetFilePathsByAlbums([localId], libraryID);
|
||||
const byAlbum = await dict(GetFilePathsByAlbums([localId], libraryID));
|
||||
|
||||
return byAlbum[localId] ?? [];
|
||||
}
|
||||
|
||||
private async recordingFilePath(mbid: string): Promise<string | null> {
|
||||
const libraryID = libraryStore.getSelectedLibraryId() ?? 0;
|
||||
const byMBID = await GetFilePathsByRecordingMBIDs([mbid], libraryID);
|
||||
const byMBID = await dictByName(
|
||||
GetFilePathsByRecordingMBIDs([mbid], libraryID),
|
||||
);
|
||||
|
||||
return byMBID[mbid]?.[0] ?? null;
|
||||
}
|
||||
@@ -2018,7 +2020,7 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte
|
||||
// of nothing.
|
||||
if (!page) return nothing;
|
||||
|
||||
if (page.shelves.length === 0) {
|
||||
if (!page.shelves?.length) {
|
||||
return html`
|
||||
<div class="shelves-empty">
|
||||
<wa-icon
|
||||
@@ -2048,7 +2050,7 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte
|
||||
to come.
|
||||
</p>`
|
||||
: nothing}
|
||||
${page.shelves.map((shelf) =>
|
||||
${(page.shelves ?? []).map((shelf) =>
|
||||
shelf.artists?.length
|
||||
? this.renderArtistsSection(
|
||||
shelf.artists,
|
||||
|
||||
@@ -5,8 +5,8 @@ import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||
import {
|
||||
AddLibrary,
|
||||
GetAllLibrariesWithTrackCounts,
|
||||
} from '@go/library/Library';
|
||||
import { DirectoryPicker } from '@go/frontendutil/FrontendUtil';
|
||||
} from '@go/library/library.js';
|
||||
import { DirectoryPicker } from '@go/frontendutil/frontendutil.js';
|
||||
import { describeError, explainError } from '@utils/describe-error';
|
||||
import { nameDialogsIn } from '@utils/name-dialog';
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import {
|
||||
property,
|
||||
state,
|
||||
} from 'lit/decorators.js';
|
||||
import { library } from '@go/models';
|
||||
import * as library from '@go/library/models.js';
|
||||
import {
|
||||
GetTracksByGenre,
|
||||
GetTracksByGenreByLibrary,
|
||||
} from '@go/library/Library';
|
||||
} from '@go/library/library.js';
|
||||
import { EventsOn } from '@runtime/runtime';
|
||||
import { Events } from '../../events';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
@@ -16,6 +16,7 @@ import { describeError } from '@utils/describe-error';
|
||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||
import '@components/track-list/track-list.js';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
import { list } from '@utils/binding';
|
||||
|
||||
@customElement('genre-details')
|
||||
export class GenreDetails extends LitElement {
|
||||
@@ -207,14 +208,14 @@ export class GenreDetails extends LitElement {
|
||||
const libId =
|
||||
libraryStore.getSelectedLibraryId();
|
||||
|
||||
this.tracks = libId !== null
|
||||
? await GetTracksByGenreByLibrary(
|
||||
this.genreName,
|
||||
libId,
|
||||
)
|
||||
: await GetTracksByGenre(
|
||||
this.genreName,
|
||||
);
|
||||
this.tracks = await list(
|
||||
libId !== null
|
||||
? GetTracksByGenreByLibrary(
|
||||
this.genreName,
|
||||
libId,
|
||||
)
|
||||
: GetTracksByGenre(this.genreName),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error loading genre tracks:', error);
|
||||
this.tracks = [];
|
||||
|
||||
@@ -12,8 +12,8 @@ import type {
|
||||
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
|
||||
import {
|
||||
GetFilePathsByGenres,
|
||||
} from '@go/library/Library';
|
||||
import type { library } from '@go/models';
|
||||
} from '@go/library/library.js';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import { LibraryController } from '@store/controllers/library-controller';
|
||||
import { SearchController } from '@store/controllers/search-controller';
|
||||
import '@components/page-header/page-header';
|
||||
@@ -33,6 +33,7 @@ import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
|
||||
import '@components/playlist-picker/playlist-picker.js';
|
||||
import { dictByName } from '@utils/binding';
|
||||
|
||||
/** Pixels to change card width per scroll tick. */
|
||||
const ZOOM_STEP = 16;
|
||||
@@ -831,9 +832,8 @@ export class GenresView
|
||||
// read off them — 6 MB over the IPC for five
|
||||
// genres of a 50 000-track library.
|
||||
const names = Array.from(genreNames);
|
||||
const byGenre = await GetFilePathsByGenres(
|
||||
names,
|
||||
libId ?? 0,
|
||||
const byGenre = await dictByName(
|
||||
GetFilePathsByGenres(names, libId ?? 0),
|
||||
);
|
||||
|
||||
// Still de-duplicated here: a track with two of
|
||||
|
||||
@@ -2,9 +2,10 @@ import { LitElement, html, css, nothing } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||
import '@awesome.me/webawesome/dist/components/button/button.js';
|
||||
import { GetShelves } from '@go/home/Service';
|
||||
import { GetAlbumTracks } from '@go/library/Library';
|
||||
import type { home, library } from '@go/models';
|
||||
import { GetShelves } from '@go/home/service.js';
|
||||
import { GetAlbumTracks } from '@go/library/library.js';
|
||||
import type * as home from '@go/home/models.js';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import { queueStore } from '@store/queue-store';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import { EventsOn } from '@runtime/runtime';
|
||||
@@ -311,7 +312,7 @@ export class HomeView extends ViewLifecycleMixin(LitElement) {
|
||||
</div>
|
||||
<p class="shelf-sub">${shelf.subtitle}</p>
|
||||
<div class="row">
|
||||
${shelf.albums.map((album) => this.renderCard(album))}
|
||||
${(shelf.albums ?? []).map((album) => this.renderCard(album))}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
ScanLibrary,
|
||||
ScanAllLibraries,
|
||||
FullRescan,
|
||||
} from '@go/library/Library';
|
||||
import type { library } from '@go/models';
|
||||
} from '@go/library/library.js';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import { EventsOn } from '@runtime/runtime';
|
||||
import { Events } from '../../events';
|
||||
import { jobStore } from '@store/job-store';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { LitElement, html, css } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { LibraryController } from '@store/controllers/library-controller';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import type { library } from '@go/models';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,10 +13,11 @@ import {
|
||||
SearchLibrary,
|
||||
ResolvePhantomTracks,
|
||||
RemovePhantomTracks,
|
||||
} from '@go/playlist/Service';
|
||||
import type { playlist } from '@go/models';
|
||||
} from '@go/playlist/service.js';
|
||||
import type * as playlist from '@go/playlist/models.js';
|
||||
import { formatMilliseconds } from '@utils/time';
|
||||
import { nameDialogsIn } from '@utils/name-dialog';
|
||||
import { list } from '@utils/binding';
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 400;
|
||||
|
||||
@@ -143,11 +144,12 @@ export class PhantomResolver extends LitElement {
|
||||
this.candidatesLoading = true;
|
||||
|
||||
try {
|
||||
this.candidates =
|
||||
await GetPhantomCandidates(
|
||||
this.candidates = await list(
|
||||
GetPhantomCandidates(
|
||||
this.playlistId,
|
||||
this.selectedPhantom,
|
||||
);
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Failed to load candidates:',
|
||||
@@ -171,8 +173,7 @@ export class PhantomResolver extends LitElement {
|
||||
this.searching = true;
|
||||
|
||||
try {
|
||||
this.searchResults =
|
||||
await SearchLibrary(query);
|
||||
this.searchResults = await list(SearchLibrary(query));
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Library search failed:',
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
RemoveTracksFromPlaylist,
|
||||
RemovePhantomTracks,
|
||||
FindDuplicateTracksInPlaylist,
|
||||
} from '@go/playlist/Service';
|
||||
import type { playlist } from '@go/models';
|
||||
} from '@go/playlist/service.js';
|
||||
import type * as playlist from '@go/playlist/models.js';
|
||||
import { EventsOn } from '@runtime/runtime';
|
||||
import { Events } from '../../events';
|
||||
import { queueStore } from '@store/queue-store';
|
||||
@@ -67,6 +67,7 @@ import {
|
||||
exploreLinkStyles,
|
||||
} from '@utils/explore-link';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
import { list } from '@utils/binding';
|
||||
|
||||
/** One playlist row: the track and its position in the *playlist*,
|
||||
* which is not its position in the filtered view. */
|
||||
@@ -276,8 +277,8 @@ export class PlaylistDetails
|
||||
if (!this.playlistId) return;
|
||||
|
||||
try {
|
||||
this.tracks = await GetPlaylistTracks(
|
||||
this.playlistId,
|
||||
this.tracks = await list(
|
||||
GetPlaylistTracks(this.playlistId),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
@@ -294,8 +295,8 @@ export class PlaylistDetails
|
||||
if (!this.playlistId) return;
|
||||
|
||||
try {
|
||||
this.tracks = await GetPlaylistTracks(
|
||||
this.playlistId,
|
||||
this.tracks = await list(
|
||||
GetPlaylistTracks(this.playlistId),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
|
||||
@@ -10,13 +10,14 @@ import {
|
||||
AddTracksToPlaylist,
|
||||
CreatePlaylistWithTracks,
|
||||
FindDuplicateTracksInPlaylist,
|
||||
} from '@go/playlist/Service';
|
||||
} from '@go/playlist/service.js';
|
||||
import { Events } from '../../events';
|
||||
import type { playlist } from '@go/models';
|
||||
import type * as playlist from '@go/playlist/models.js';
|
||||
import '@components/duplicate-tracks-dialog/duplicate-tracks-dialog.js';
|
||||
import { notificationStore } from '@store/notification-store';
|
||||
import { describeError } from '@utils/describe-error';
|
||||
import type { DuplicateTracksDialog } from '@components/duplicate-tracks-dialog/duplicate-tracks-dialog.js';
|
||||
import { list } from '@utils/binding';
|
||||
|
||||
/**
|
||||
* A reusable playlist picker that displays existing playlists
|
||||
@@ -156,7 +157,7 @@ export class PlaylistPicker extends LitElement {
|
||||
|
||||
private async loadPlaylists() {
|
||||
try {
|
||||
this.playlists = await GetAllPlaylists();
|
||||
this.playlists = await list(GetAllPlaylists());
|
||||
} catch (err) {
|
||||
console.error('Failed to load playlists:', err);
|
||||
this.playlists = [];
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
RenamePlaylist,
|
||||
ImportPlaylists,
|
||||
FindDuplicateTracksInPlaylist,
|
||||
} from '@go/playlist/Service';
|
||||
import { PlaylistFilePicker } from '@go/frontendutil/FrontendUtil';
|
||||
import type { playlist } from '@go/models';
|
||||
} from '@go/playlist/service.js';
|
||||
import { PlaylistFilePicker } from '@go/frontendutil/frontendutil.js';
|
||||
import type * as playlist from '@go/playlist/models.js';
|
||||
import { PlaylistController } from '@store/controllers/playlist-controller';
|
||||
import { SearchController } from '@store/controllers/search-controller';
|
||||
import {
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
removeDragImage,
|
||||
} from '@utils/drag-image';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import type { library } from '@go/models';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import { loadTrackDetails } from '@utils/lazy-track-details.js';
|
||||
import { tracksByFilePath } from '@utils/track-index.js';
|
||||
import type { TrackDetails } from '@components/track-details/track-details.js';
|
||||
|
||||
@@ -5,13 +5,13 @@ import {
|
||||
state,
|
||||
query,
|
||||
} from 'lit/decorators.js';
|
||||
import type { playlist } from '@go/models';
|
||||
import type * as playlist from '@go/playlist/models.js';
|
||||
import {
|
||||
GetSmartPlaylistTracks,
|
||||
RefreshSmartPlaylist,
|
||||
GetSmartPlaylistRules,
|
||||
UpdateSmartPlaylistRules,
|
||||
} from '@go/playlist/Service';
|
||||
} from '@go/playlist/service.js';
|
||||
import { EventsOn } from '@runtime/runtime';
|
||||
import { Events } from '../../events';
|
||||
import { queueStore } from '@store/queue-store';
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
} from '@utils/explore-link';
|
||||
import '@components/smart-playlist-editor/smart-playlist-editor.js';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
import { list } from '@utils/binding';
|
||||
|
||||
|
||||
/**
|
||||
@@ -700,8 +701,8 @@ export class SmartPlaylistDetails
|
||||
if (!this.playlistId) return;
|
||||
|
||||
try {
|
||||
this.tracks = await GetSmartPlaylistTracks(
|
||||
this.playlistId,
|
||||
this.tracks = await list(
|
||||
GetSmartPlaylistTracks(this.playlistId),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LitElement, html, css, nothing } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { library } from '@go/models';
|
||||
import { PreviewSmartPlaylist } from '@go/playlist/Service';
|
||||
import * as library from '@go/library/models.js';
|
||||
import { PreviewSmartPlaylist } from '@go/playlist/service.js';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import { describeError } from '@utils/describe-error';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { LitElement, html, css, nothing } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
import type { explore } from '@go/models';
|
||||
import type * as explore from '@go/explore/models.js';
|
||||
import {
|
||||
GetArtistImageURL,
|
||||
GetThumbnail,
|
||||
RecordSearchClick,
|
||||
} from '@go/explore/Service';
|
||||
} from '@go/explore/service.js';
|
||||
import '../library-status-indicator/library-status-indicator.js';
|
||||
import type { LibraryStatus } from '../library-status-indicator/library-status-indicator.js';
|
||||
import { artistLink, exploreLinkStyles } from '../../utils/explore-link';
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
state,
|
||||
query,
|
||||
} from 'lit/decorators.js';
|
||||
import type { library } from '@go/models';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import {
|
||||
formatSampleRate,
|
||||
formatBitDepth,
|
||||
@@ -14,14 +14,14 @@ import {
|
||||
} from '@utils/format';
|
||||
import { formatMilliseconds } from '@utils/time';
|
||||
import { nameDialogsIn } from '@utils/name-dialog';
|
||||
import { WriteTrackTagsByPath } from '@go/tagwriter/TagWriter';
|
||||
import { WriteTrackTagsByPath } from '@go/tagwriter/tagwriter.js';
|
||||
import {
|
||||
BatchWriteTrackTags,
|
||||
CancelBatchWrite,
|
||||
} from '@go/tagwriter/TagWriter';
|
||||
import { GetTrackMBIDs } from '@go/library/Library';
|
||||
} from '@go/tagwriter/tagwriter.js';
|
||||
import { GetTrackMBIDs } from '@go/library/library.js';
|
||||
type TrackMBIDs = library.TrackMBIDs;
|
||||
import { ImageFilePicker, ReadFile } from '@go/frontendutil/FrontendUtil';
|
||||
import { ImageFilePicker, ReadFile } from '@go/frontendutil/frontendutil.js';
|
||||
import { libraryStore } from '../../store/library-store';
|
||||
import { EventsOn } from '@runtime/runtime';
|
||||
import { Events } from '../../events';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { library } from '@go/models';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import {
|
||||
formatSampleRate,
|
||||
formatBitDepth,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { library } from '@go/models';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import { html } from 'lit';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { library } from '@go/models';
|
||||
import * as library from '@go/library/models.js';
|
||||
import { LitElement, html, svg, css, nothing } from 'lit';
|
||||
import { designTokens } from '../../styles/tokens.css';
|
||||
import { srOnly } from '../../styles/sr-only.css';
|
||||
@@ -65,7 +65,7 @@ import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
||||
import { describeError } from '@utils/describe-error';
|
||||
import { notificationStore } from '@store/notification-store';
|
||||
import { confirmAction } from '@components/confirm-dialog/confirm-dialog';
|
||||
import { RemoveFromLibrary } from '@go/library/Library';
|
||||
import { RemoveFromLibrary } from '@go/library/library.js';
|
||||
import { loadTrackDetails } from '@utils/lazy-track-details.js';
|
||||
import { tracksByFilePath, tracksForPaths } from '@utils/track-index.js';
|
||||
import '@components/playlist-picker/playlist-picker.js';
|
||||
|
||||
@@ -14,7 +14,7 @@ import { shortcutsStore } from '@store/shortcuts-store';
|
||||
import { ambientShortcutScope } from './shortcut-scope';
|
||||
import { playerStore } from '@store/player-store';
|
||||
import { queueStore } from '@store/queue-store';
|
||||
import * as Player from '@go/player/Player';
|
||||
import * as Player from '@go/player/player.js';
|
||||
import type { SearchBar } from '@components/search-bar/search-bar';
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
||||
import type { library } from '@go/models';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import { libraryStore } from '../library-store';
|
||||
|
||||
type ViewName = 'tracks' | 'albums' | 'artists' | 'genres';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
||||
import type { playlist } from '@go/models';
|
||||
import type * as playlist from '@go/playlist/models.js';
|
||||
import { playlistStore } from '../playlist-store';
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
StartDownload,
|
||||
TestProvider,
|
||||
UpdateProvider,
|
||||
} from '@go/download/Service';
|
||||
import type { download } from '@go/models';
|
||||
} from '@go/download/service.js';
|
||||
import type * as download from '@go/download/models.js';
|
||||
import { Events } from '../events';
|
||||
|
||||
export type DownloadCandidate = download.Candidate;
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ToggleDefaultPlaylistTrack,
|
||||
AddToDefaultPlaylist,
|
||||
RemoveFromDefaultPlaylist,
|
||||
} from '@go/playlist/Service';
|
||||
} from '@go/playlist/service.js';
|
||||
import {
|
||||
GetFavoritesIconStyle,
|
||||
GetFavoritesPlaylistID,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
SetFavoritesIconStyle,
|
||||
SetFavoritesPlaylistID,
|
||||
SetPinDefaultPlaylist,
|
||||
} from '@go/config/Config';
|
||||
} from '@go/config/config.js';
|
||||
import { Events } from '../events';
|
||||
import { describeError } from '@utils/describe-error';
|
||||
import { notificationStore } from './notification-store';
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
CancelJob,
|
||||
DismissJob,
|
||||
ClearFinishedJobs,
|
||||
} from '@go/jobs/Service';
|
||||
import type { jobs } from '@go/models';
|
||||
} from '@go/jobs/service.js';
|
||||
import type * as jobs from '@go/jobs/models.js';
|
||||
import { Events } from '../events';
|
||||
|
||||
export type Job = jobs.Job;
|
||||
|
||||
@@ -11,8 +11,9 @@ import {
|
||||
GetAllGenresWithCountsByLibrary,
|
||||
GetAlbumsByArtistByLibrary,
|
||||
GetAllLibrariesWithTrackCounts,
|
||||
} from '@go/library/Library';
|
||||
import type { library } from '@go/models';
|
||||
} from '@go/library/library.js';
|
||||
import type * as library from '@go/library/models.js';
|
||||
import { list } from '@utils/binding';
|
||||
import { Events } from '../events';
|
||||
|
||||
type ViewName = 'tracks' | 'albums' | 'artists' | 'genres';
|
||||
@@ -231,7 +232,7 @@ class LibraryStore {
|
||||
|
||||
return this.track(
|
||||
'tracks',
|
||||
id !== null ? GetAllTracksByLibrary(id) : GetAllTracks(),
|
||||
list(id !== null ? GetAllTracksByLibrary(id) : GetAllTracks()),
|
||||
(tracks) => {
|
||||
this.tracks = tracks;
|
||||
},
|
||||
@@ -252,7 +253,7 @@ class LibraryStore {
|
||||
|
||||
return this.track(
|
||||
'albums',
|
||||
id !== null ? GetAllAlbumsByLibrary(id) : GetAllAlbums(),
|
||||
list(id !== null ? GetAllAlbumsByLibrary(id) : GetAllAlbums()),
|
||||
(albums) => {
|
||||
this.albums = albums;
|
||||
},
|
||||
@@ -273,7 +274,7 @@ class LibraryStore {
|
||||
|
||||
return this.track(
|
||||
'artists',
|
||||
id !== null ? GetAllArtistsByLibrary(id) : GetAllArtists(),
|
||||
list(id !== null ? GetAllArtistsByLibrary(id) : GetAllArtists()),
|
||||
(artists) => {
|
||||
this.artists = artists;
|
||||
},
|
||||
@@ -294,9 +295,11 @@ class LibraryStore {
|
||||
|
||||
return this.track(
|
||||
'genres',
|
||||
id !== null
|
||||
? GetAllGenresWithCountsByLibrary(id)
|
||||
: GetAllGenresWithCounts(),
|
||||
list(
|
||||
id !== null
|
||||
? GetAllGenresWithCountsByLibrary(id)
|
||||
: GetAllGenresWithCounts(),
|
||||
),
|
||||
(genres) => {
|
||||
this.genres = genres;
|
||||
},
|
||||
@@ -309,9 +312,11 @@ class LibraryStore {
|
||||
): Promise<library.Album[]> {
|
||||
const id = this.selectedLibraryIdValue;
|
||||
|
||||
return id !== null
|
||||
? GetAlbumsByArtistByLibrary(artistID, id)
|
||||
: GetAlbumsByArtist(artistID);
|
||||
return list(
|
||||
id !== null
|
||||
? GetAlbumsByArtistByLibrary(artistID, id)
|
||||
: GetAlbumsByArtist(artistID),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -410,8 +415,7 @@ class LibraryStore {
|
||||
return this.libraries;
|
||||
}
|
||||
|
||||
const libs =
|
||||
await GetAllLibrariesWithTrackCounts();
|
||||
const libs = await list(GetAllLibrariesWithTrackCounts());
|
||||
|
||||
this.libraries = libs;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EventsOn } from '@runtime/runtime';
|
||||
import { Events } from '../events';
|
||||
import * as Player from '@go/player/Player';
|
||||
import * as Player from '@go/player/player.js';
|
||||
import { notificationStore } from './notification-store';
|
||||
|
||||
/** The region `<inline-notice>` renders these in: the player bar. */
|
||||
|
||||
@@ -3,8 +3,8 @@ import {
|
||||
GetAllPlaylists,
|
||||
GetAllPlaylistsWithTracks,
|
||||
GetPlaylistTracks,
|
||||
} from '@go/playlist/Service';
|
||||
import type { playlist } from '@go/models';
|
||||
} from '@go/playlist/service.js';
|
||||
import type * as playlist from '@go/playlist/models.js';
|
||||
import { Events } from '../events';
|
||||
|
||||
type Subscriber = () => void;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EventsOn } from '@runtime/runtime';
|
||||
import { Events } from '../events';
|
||||
import * as Queue from '@go/queue/Queue';
|
||||
import * as Queue from '@go/queue/queue.js';
|
||||
|
||||
// Types
|
||||
export interface QueueTrack {
|
||||
|
||||
@@ -4,8 +4,9 @@ import {
|
||||
SetShortcut,
|
||||
SetShortcuts,
|
||||
ResetShortcuts,
|
||||
} from '@go/config/Config';
|
||||
} from '@go/config/config.js';
|
||||
import { Events } from '../events';
|
||||
import { dictByName } from '@utils/binding';
|
||||
|
||||
export interface ShortcutsState {
|
||||
bindings: Map<string, string>; // action → key combo
|
||||
@@ -56,7 +57,7 @@ class ShortcutsStore {
|
||||
|
||||
private async loadFromBackend(): Promise<void> {
|
||||
try {
|
||||
const raw = await GetShortcuts();
|
||||
const raw = await dictByName(GetShortcuts());
|
||||
this.state = {
|
||||
bindings: new Map(Object.entries(raw)),
|
||||
loaded: true,
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
GetThemeBackgroundShade,
|
||||
SetThemeAccentColor,
|
||||
SetThemeBackgroundShade,
|
||||
} from '@go/config/Config';
|
||||
} from '@go/config/config.js';
|
||||
import { Events } from '../events';
|
||||
|
||||
export type BackgroundShade = 'darker' | 'dark' | 'light';
|
||||
|
||||
@@ -2,10 +2,11 @@ import { EventsOn } from '@runtime/runtime';
|
||||
import {
|
||||
GetTrackListColumns,
|
||||
SetTrackListColumns,
|
||||
} from '@go/config/Config';
|
||||
import { tracklist } from '@go/models';
|
||||
} from '@go/config/config.js';
|
||||
import * as tracklist from '@go/tracklist/models.js';
|
||||
import { Events } from '../events';
|
||||
import { DEFAULT_COLUMN_IDS } from '@components/track-list/columns';
|
||||
import { list } from '@utils/binding';
|
||||
|
||||
export interface TrackListState {
|
||||
/** Ordered list of visible column IDs. */
|
||||
@@ -47,12 +48,10 @@ class TrackListStore {
|
||||
|
||||
private async loadFromBackend(): Promise<void> {
|
||||
try {
|
||||
const columns = await GetTrackListColumns();
|
||||
const columns = await list(GetTrackListColumns());
|
||||
|
||||
this.update({
|
||||
columnIds: columns.map(
|
||||
(c: tracklist.Column) => c.id,
|
||||
),
|
||||
columnIds: columns.map((c) => c.id),
|
||||
});
|
||||
} catch {
|
||||
// Use defaults on failure.
|
||||
@@ -72,12 +71,9 @@ class TrackListStore {
|
||||
// ===============================================================
|
||||
|
||||
async setColumns(columnIds: string[]): Promise<void> {
|
||||
const columns = columnIds.map((id) => {
|
||||
const col = new tracklist.Column();
|
||||
col.id = id;
|
||||
|
||||
return col;
|
||||
});
|
||||
const columns: tracklist.Column[] = columnIds.map((id) => ({
|
||||
id: id as tracklist.ColumnID,
|
||||
}));
|
||||
|
||||
await SetTrackListColumns(columns);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* The boundary between a Go return value and the app's own types.
|
||||
*
|
||||
* v2's binding generator typed a `[]T` return as `T[]`, which was a
|
||||
* lie: a nil slice marshals to JSON `null`, and every list in this app
|
||||
* has always been able to arrive that way. v3 types it honestly as
|
||||
* `T[] | null`, which surfaced ~50 sites that were relying on the lie.
|
||||
*
|
||||
* The app's contract is the one it has always behaved as if it had —
|
||||
* *an absent list is an empty list* — so it is stated once here rather
|
||||
* than as `?? []` at every call site, and stated at the only place it
|
||||
* is true: the moment a value crosses from Go.
|
||||
*
|
||||
* These helpers also return a plain `Promise`. v3 bindings return a
|
||||
* `CancellablePromise`, and nothing in this app cancels one; letting
|
||||
* that type leak inward would put a Wails type in the signature of
|
||||
* every store method for a capability none of them use.
|
||||
*/
|
||||
|
||||
/**
|
||||
* list awaits a binding returning a Go slice and yields `[]` for nil.
|
||||
*/
|
||||
export async function list<T>(
|
||||
request: PromiseLike<T[] | null>,
|
||||
): Promise<T[]> {
|
||||
return (await request) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* dict awaits a binding returning a Go map and yields `{}` for nil.
|
||||
*
|
||||
* Two shapes of the same lie are undone here. The generator types a
|
||||
* `map[int64]T` with a template-literal key (`` `${number}` ``), which
|
||||
* cannot be indexed by a `number` even though every such key is one;
|
||||
* and it types each value as nullable, because a map of slices can
|
||||
* hold a nil one. A null-valued key is dropped rather than kept,
|
||||
* which loses nothing: `noUncheckedIndexedAccess` already makes every
|
||||
* read `V | undefined`, so an absent key and a nil value are
|
||||
* indistinguishable to every consumer.
|
||||
*/
|
||||
export async function dict<V>(
|
||||
request: PromiseLike<Record<string, V | null | undefined> | null>,
|
||||
): Promise<Record<number, V>> {
|
||||
const raw = (await request) ?? {};
|
||||
const out: Record<number, V> = {};
|
||||
|
||||
for (const [key, val] of Object.entries(raw)) {
|
||||
if (val != null) out[Number(key)] = val;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* dictByName is dict for a Go map keyed by something that is already a
|
||||
* string — an MBID, a file path, a genre name.
|
||||
*/
|
||||
export async function dictByName<V>(
|
||||
request: PromiseLike<Record<string, V | null | undefined> | null>,
|
||||
): Promise<Record<string, V>> {
|
||||
return compact(await request);
|
||||
}
|
||||
|
||||
/**
|
||||
* compact is dictByName for a map that arrived as a *field* rather
|
||||
* than as a return value — a nested `map[string]string`, which the
|
||||
* generator types with optional values because a JSON object need not
|
||||
* carry every key.
|
||||
*/
|
||||
export function compact<V>(
|
||||
map: Record<string, V | null | undefined> | null | undefined,
|
||||
): Record<string, V> {
|
||||
const out: Record<string, V> = {};
|
||||
|
||||
for (const [key, val] of Object.entries(map ?? {})) {
|
||||
if (val != null) out[key] = val;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* value awaits a binding whose result is used as-is, dropping only the
|
||||
* cancellation the app never asks for.
|
||||
*/
|
||||
export async function value<T>(request: PromiseLike<T>): Promise<T> {
|
||||
return await request;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { downloadStore } from '@store/download-store';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import type { download } from '@go/models';
|
||||
import type * as download from '@go/download/models.js';
|
||||
import type { LibraryStatus } from '../components/library-status-indicator/library-status-indicator';
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* given array and never again.
|
||||
*/
|
||||
|
||||
import type { library } from '@go/models';
|
||||
import type * as library from '@go/library/models.js';
|
||||
|
||||
const byArray = new WeakMap<
|
||||
readonly library.Track[],
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* The `@runtime/runtime` seam.
|
||||
*
|
||||
* 22 files import `EventsOn` from here and nothing else, so rather than
|
||||
* rewriting 22 imports to v3's `Events.On` the alias points at this
|
||||
* module — which is also what gives the Vitest fake exactly one seam to
|
||||
* intercept instead of a `window.runtime` global that v3 does not have.
|
||||
* (Plan 009, D4.)
|
||||
*
|
||||
* The one real difference between the two runtimes is the callback
|
||||
* shape: v2 spread an event's data across the callback's arguments,
|
||||
* v3 hands over a single `WailsEvent` object. Unwrapping `.data` here
|
||||
* reproduces v2's shape for the single-value case, which is every emit
|
||||
* in this tree — `backend/events` has no call site passing more than
|
||||
* one data argument, and v3's `EventManager.Emit` only packs arguments
|
||||
* into a slice when there is more than one, so there is nothing to
|
||||
* un-spread.
|
||||
*/
|
||||
|
||||
import { Events } from '@wailsio/runtime';
|
||||
|
||||
/**
|
||||
* EventsOn registers a listener for a backend event and returns the
|
||||
* function that unregisters it.
|
||||
*/
|
||||
export function EventsOn(
|
||||
eventName: string,
|
||||
callback: (...data: any[]) => void,
|
||||
): () => void {
|
||||
return Events.On(eventName, (ev) => {
|
||||
callback(ev.data);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user