@freqhole/playlistz 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/config.json +11 -0
- package/.changeset/nice-wolves-thank.md +5 -0
- package/.freqhole-versions.json +4 -0
- package/.github/copilot-instructions.md +201 -0
- package/.github/workflows/changesets.yml +50 -0
- package/.github/workflows/npm-publish.yml +124 -0
- package/.github/workflows/pr-checks.yml +103 -0
- package/README.md +30 -0
- package/build-component.js +141 -0
- package/build-zip-bundle-lib.js +44 -0
- package/config/playwright.config.ts +47 -0
- package/config/vite.config.ts +44 -0
- package/config/vitest.config.ts +39 -0
- package/dist/assets/automerge_wasm_bg-Cik4BF9l.wasm +0 -0
- package/dist/assets/index-CbOXzGiA.js +216 -0
- package/dist/assets/index-CbOXzGiA.js.map +1 -0
- package/dist/assets/index-TvJ6RFpy.css +1 -0
- package/dist/assets/midden-DceCrT_L.js +2 -0
- package/dist/assets/midden-DceCrT_L.js.map +1 -0
- package/dist/assets/midden_bg-BLhfGIU-.wasm +0 -0
- package/dist/index.html +55 -0
- package/dist/sw.js +134 -0
- package/docs/AUTOMERGE_P2P_PLAN.md +233 -0
- package/docs/COLLABORATIVE_SHARING_PLAN.md +188 -0
- package/docs/E2E_TESTID_PLAN.md +234 -0
- package/docs/IROH_P2P_PLAN.md +302 -0
- package/docs/ROADMAP.md +695 -0
- package/docs/TODO.md +167 -0
- package/docs/bundle-embedding-plan.md +134 -0
- package/docs/standalone-refactor.md +184 -0
- package/e2e/all-playlists.spec.ts +220 -0
- package/e2e/audio-player.spec.ts +226 -0
- package/e2e/collaborative-features.spec.ts +229 -0
- package/e2e/contexts.ts +238 -0
- package/e2e/edit-panel.spec.ts +87 -0
- package/e2e/fixtures/bare-glitch-1s.m4a +0 -0
- package/e2e/fixtures/bare-glitch-1s.mp3 +0 -0
- package/e2e/fixtures/bare-glitch-1s.ogg +0 -0
- package/e2e/fixtures/chord-stack-3s.wav +0 -0
- package/e2e/fixtures/cover-anim.gif +0 -0
- package/e2e/fixtures/cover-blue.png +0 -0
- package/e2e/fixtures/cover-checkers.png +0 -0
- package/e2e/fixtures/cover-gradient.jpg +0 -0
- package/e2e/fixtures/cover-mono.gif +0 -0
- package/e2e/fixtures/cover-noise.png +0 -0
- package/e2e/fixtures/cover-plasma.webp +0 -0
- package/e2e/fixtures/cover-portrait.jpg +0 -0
- package/e2e/fixtures/cover-red.png +0 -0
- package/e2e/fixtures/cover-thumb.jpg +0 -0
- package/e2e/fixtures/cover-wide.webp +0 -0
- package/e2e/fixtures/generate.mjs +257 -0
- package/e2e/fixtures/long-drone-90s.mp3 +0 -0
- package/e2e/fixtures/noisy-binaural-8s.mp3 +0 -0
- package/e2e/fixtures/tagged-a3-4s.m4a +0 -0
- package/e2e/fixtures/tagged-a3-4s.mp3 +0 -0
- package/e2e/fixtures/tagged-a3-4s.ogg +0 -0
- package/e2e/fixtures/tagged-c5-3s.m4a +0 -0
- package/e2e/fixtures/tagged-c5-3s.mp3 +0 -0
- package/e2e/fixtures/tagged-c5-3s.ogg +0 -0
- package/e2e/fixtures/tagged-f4-6s.m4a +0 -0
- package/e2e/fixtures/tagged-f4-6s.mp3 +0 -0
- package/e2e/fixtures/tagged-f4-6s.ogg +0 -0
- package/e2e/fixtures/tone-220hz-10s.wav +0 -0
- package/e2e/fixtures/tone-440hz-2s.wav +0 -0
- package/e2e/fixtures/tone-880hz-5s.wav +0 -0
- package/e2e/fixtures/tone-stereo-3s.wav +0 -0
- package/e2e/fixtures/user-provided/README.md +1 -0
- package/e2e/helpers/app.ts +143 -0
- package/e2e/helpers/hooks.ts +133 -0
- package/e2e/helpers/index.ts +12 -0
- package/e2e/helpers/media.ts +125 -0
- package/e2e/helpers.ts +10 -0
- package/e2e/p2p-collaboration.spec.ts +356 -0
- package/e2e/p2p-multi-peer.spec.ts +723 -0
- package/e2e/p2p-states.spec.ts +302 -0
- package/e2e/playback.spec.ts +56 -0
- package/e2e/playlist-crud.spec.ts +126 -0
- package/e2e/share-link-autoplay.spec.ts +129 -0
- package/e2e/sharing-access.spec.ts +205 -0
- package/e2e/sharing.spec.ts +195 -0
- package/e2e/song-cache-state.spec.ts +202 -0
- package/e2e/zip-bundle.spec.ts +855 -0
- package/eslint.config.js +114 -0
- package/index.html +54 -0
- package/package.json +119 -0
- package/public/sw.js +134 -0
- package/scripts/use-local.mjs +37 -0
- package/scripts/use-published.mjs +37 -0
- package/src/App.tsx +9 -0
- package/src/cli/check.ts +164 -0
- package/src/cli/generate.ts +184 -0
- package/src/cli/http.ts +88 -0
- package/src/cli/index.ts +65 -0
- package/src/cli/init.ts +18 -0
- package/src/components/AllPlaylistsPanel.tsx +713 -0
- package/src/components/AudioPlayer.tsx +122 -0
- package/src/components/MarqueeText.tsx +101 -0
- package/src/components/PlaylistCoverModal.tsx +519 -0
- package/src/components/PlaylistEditPanel.tsx +803 -0
- package/src/components/PlaylistSharePanel.tsx +1020 -0
- package/src/components/ShareLinkKnockPanel.tsx +144 -0
- package/src/components/SharePanel.tsx +584 -0
- package/src/components/SongEditModal.tsx +453 -0
- package/src/components/SongEditPanel.tsx +578 -0
- package/src/components/SongRow.tsx +689 -0
- package/src/components/index.tsx +494 -0
- package/src/components/playlist/index.tsx +1203 -0
- package/src/context/PlaylistzContext.tsx +74 -0
- package/src/dev-hooks.ts +35 -0
- package/src/hooks/createDocIndexQuery.ts +53 -0
- package/src/hooks/createDocStore.test.ts +303 -0
- package/src/hooks/createDocStore.ts +90 -0
- package/src/hooks/useDragAndDrop.test.ts +474 -0
- package/src/hooks/useDragAndDrop.ts +400 -0
- package/src/hooks/useImageModal.test.ts +174 -0
- package/src/hooks/useImageModal.ts +201 -0
- package/src/hooks/usePlaylistManager.test.ts +453 -0
- package/src/hooks/usePlaylistManager.ts +685 -0
- package/src/hooks/usePlaylistsQuery.test.tsx +120 -0
- package/src/hooks/usePlaylistsQuery.ts +44 -0
- package/src/hooks/useSongState.test.ts +236 -0
- package/src/hooks/useSongState.ts +114 -0
- package/src/hooks/useUIState.ts +71 -0
- package/src/index.tsx +18 -0
- package/src/services/audioService.dev.ts +22 -0
- package/src/services/audioService.test.ts +1226 -0
- package/src/services/audioService.ts +1395 -0
- package/src/services/automergeRepo.test.ts +269 -0
- package/src/services/automergeRepo.ts +226 -0
- package/src/services/blobTransferService.dev.ts +119 -0
- package/src/services/blobTransferService.test.ts +441 -0
- package/src/services/blobTransferService.ts +702 -0
- package/src/services/docIndexService.test.ts +179 -0
- package/src/services/docIndexService.ts +118 -0
- package/src/services/fileProcessingService.test.ts +554 -0
- package/src/services/fileProcessingService.ts +239 -0
- package/src/services/imageService.test.ts +701 -0
- package/src/services/imageService.ts +365 -0
- package/src/services/indexedDBService.integration.test.ts +104 -0
- package/src/services/indexedDBService.test.ts +202 -0
- package/src/services/indexedDBService.ts +436 -0
- package/src/services/offlineService.test.ts +661 -0
- package/src/services/offlineService.ts +382 -0
- package/src/services/p2pService.test.ts +305 -0
- package/src/services/p2pService.ts +344 -0
- package/src/services/playlistDocService.test.ts +448 -0
- package/src/services/playlistDocService.ts +707 -0
- package/src/services/playlistDownloadService.test.ts +674 -0
- package/src/services/playlistDownloadService.ts +389 -0
- package/src/services/sharingService.test.ts +812 -0
- package/src/services/sharingService.ts +1073 -0
- package/src/services/sharingState.ts +161 -0
- package/src/services/songReactivity.test.ts +620 -0
- package/src/services/songReactivity.ts +145 -0
- package/src/services/standaloneService.test.ts +1025 -0
- package/src/services/standaloneService.ts +588 -0
- package/src/services/streamingAudioService.test.ts +275 -0
- package/src/services/streamingAudioService.ts +166 -0
- package/src/styles.css +428 -0
- package/src/test-setup.ts +547 -0
- package/src/types/global.d.ts +40 -0
- package/src/types/playlist.ts +99 -0
- package/src/utils/hashUtils.ts +41 -0
- package/src/utils/log.ts +97 -0
- package/src/utils/m3u.test.ts +172 -0
- package/src/utils/m3u.ts +136 -0
- package/src/utils/mockData.ts +166 -0
- package/src/utils/standaloneTemplates.test.ts +175 -0
- package/src/utils/standaloneTemplates.ts +83 -0
- package/src/utils/swTemplate.ts +84 -0
- package/src/utils/timeUtils.ts +166 -0
- package/src/utils/typeGuards.ts +171 -0
- package/src/web-component.tsx +98 -0
- package/src/zip-bundle/index.ts +7 -0
- package/src/zip-bundle/m3u.ts +45 -0
- package/src/zip-bundle/types.ts +50 -0
- package/src/zip-bundle/utils.ts +33 -0
- package/src/zip-bundle/zipBuilder.ts +309 -0
- package/tailwind.config.js +55 -0
- package/tsconfig.json +43 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// indexeddb service - phase 4+ schema.
|
|
2
|
+
// musicPlaylistDB v1 contains only non-doc state:
|
|
3
|
+
// playbackPositions, lastPlayed, settings, docIndex, knocks, accessGrants.
|
|
4
|
+
// playlist and song data live in automerge docs (freqhole-automerge idb via
|
|
5
|
+
// IndexedDBStorageAdapter). see playlistDocService for doc-backed crud.
|
|
6
|
+
|
|
7
|
+
import { openDB, type DBSchema, type IDBPDatabase } from "idb";
|
|
8
|
+
|
|
9
|
+
// simple signal implementation for live queries
|
|
10
|
+
interface Signal<T> {
|
|
11
|
+
get: () => T;
|
|
12
|
+
set: (value: T) => void;
|
|
13
|
+
subscribe: (fn: (value: T) => void) => () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createSignal<T>(initial: T): Signal<T> {
|
|
17
|
+
let value = initial;
|
|
18
|
+
const subs = new Set<(value: T) => void>();
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
get: () => value,
|
|
22
|
+
set: (newVal) => {
|
|
23
|
+
if (value !== newVal) {
|
|
24
|
+
value = newVal;
|
|
25
|
+
subs.forEach((fn) => fn(value));
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
subscribe: (fn) => {
|
|
29
|
+
subs.add(fn);
|
|
30
|
+
fn(value);
|
|
31
|
+
return () => subs.delete(fn);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// database configuration
|
|
37
|
+
export const DB_NAME = "musicPlaylistDB";
|
|
38
|
+
export const DB_VERSION = 1;
|
|
39
|
+
|
|
40
|
+
// legacy store name constants kept for compatibility with standalone/streaming services.
|
|
41
|
+
// these stores are no longer created in musicPlaylistDB - data lives in automerge docs.
|
|
42
|
+
export const PLAYLISTS_STORE = "playlists";
|
|
43
|
+
export const SONGS_STORE = "songs";
|
|
44
|
+
|
|
45
|
+
export const PLAYBACK_POSITIONS_STORE = "playbackPositions";
|
|
46
|
+
export const LAST_PLAYED_STORE = "lastPlayed";
|
|
47
|
+
export const SETTINGS_STORE = "settings";
|
|
48
|
+
export const DOC_INDEX_STORE = "docIndex";
|
|
49
|
+
export const KNOCKS_STORE = "knocks";
|
|
50
|
+
export const ACCESS_GRANTS_STORE = "accessGrants";
|
|
51
|
+
|
|
52
|
+
// record shape stored per-song in the playbackPositions store
|
|
53
|
+
export interface PlaybackPositionRecord {
|
|
54
|
+
songId: string;
|
|
55
|
+
position: number;
|
|
56
|
+
updatedAt: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface LastPlayedRecord {
|
|
60
|
+
playlistId: string;
|
|
61
|
+
songId: string;
|
|
62
|
+
updatedAt: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// generic key-value record for ui/app settings
|
|
66
|
+
export interface SettingRecord {
|
|
67
|
+
key: string;
|
|
68
|
+
value: unknown;
|
|
69
|
+
updatedAt: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// automerge doc index entry: maps an AutomergeUrl to display metadata.
|
|
73
|
+
// used to list known playlists in the sidebar without loading every doc.
|
|
74
|
+
export interface DocIndexEntry {
|
|
75
|
+
docId: string; // AutomergeUrl, e.g. "automerge:abc123..."
|
|
76
|
+
title: string;
|
|
77
|
+
addedAt: number; // unix ms timestamp
|
|
78
|
+
source: "local" | "shared" | "freqhole";
|
|
79
|
+
// set when this playlist was received from a remote peer.
|
|
80
|
+
// absent for locally-created playlists.
|
|
81
|
+
remoteNodeId?: string;
|
|
82
|
+
remoteName?: string;
|
|
83
|
+
remoteAvatarDataUrl?: string;
|
|
84
|
+
// true once the user has forked to a local editable copy
|
|
85
|
+
isForked?: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// inbound or outbound knock request record for the knock inbox/outbox ui.
|
|
89
|
+
export interface KnockRecord {
|
|
90
|
+
id: string; // uuid
|
|
91
|
+
nodeId: string; // requester (inbound) or responder (outbound) iroh node id
|
|
92
|
+
direction: "inbound" | "outbound";
|
|
93
|
+
name: string;
|
|
94
|
+
message: string;
|
|
95
|
+
status: "pending" | "accepted" | "rejected";
|
|
96
|
+
createdAt: number;
|
|
97
|
+
processedAt?: number;
|
|
98
|
+
knockType?: "browse" | "doc_access"; // browse = list playlists; doc_access = specific playlist
|
|
99
|
+
requestedDocId?: string; // set when knockType is "doc_access"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// access grant written when an inbound knock is accepted.
|
|
103
|
+
export interface AccessGrantRecord {
|
|
104
|
+
nodeId: string; // the granted peer's iroh node id (keyPath)
|
|
105
|
+
name: string;
|
|
106
|
+
grantedAt: number;
|
|
107
|
+
docIds?: string[]; // docs this grant covers; undefined = all listed playlists
|
|
108
|
+
avatarDataUrl?: string; // their avatar at time of last contact
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// database schema definition - v1 contains only non-doc state
|
|
112
|
+
interface PlaylistDB extends DBSchema {
|
|
113
|
+
playbackPositions: {
|
|
114
|
+
key: string;
|
|
115
|
+
value: PlaybackPositionRecord;
|
|
116
|
+
};
|
|
117
|
+
lastPlayed: {
|
|
118
|
+
key: string;
|
|
119
|
+
value: LastPlayedRecord;
|
|
120
|
+
};
|
|
121
|
+
settings: {
|
|
122
|
+
key: string;
|
|
123
|
+
value: SettingRecord;
|
|
124
|
+
};
|
|
125
|
+
docIndex: {
|
|
126
|
+
key: string; // docId (AutomergeUrl)
|
|
127
|
+
value: DocIndexEntry;
|
|
128
|
+
};
|
|
129
|
+
knocks: {
|
|
130
|
+
key: string; // id
|
|
131
|
+
value: KnockRecord;
|
|
132
|
+
};
|
|
133
|
+
accessGrants: {
|
|
134
|
+
key: string; // nodeId
|
|
135
|
+
value: AccessGrantRecord;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// database connection cache
|
|
140
|
+
let cachedDB: Promise<IDBPDatabase<PlaylistDB>> | null = null;
|
|
141
|
+
|
|
142
|
+
export async function setupDB(): Promise<IDBPDatabase<PlaylistDB>> {
|
|
143
|
+
if (cachedDB) {
|
|
144
|
+
return cachedDB;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
cachedDB = openDB<PlaylistDB>(DB_NAME, DB_VERSION, {
|
|
148
|
+
upgrade(db) {
|
|
149
|
+
if (!db.objectStoreNames.contains(PLAYBACK_POSITIONS_STORE)) {
|
|
150
|
+
db.createObjectStore(PLAYBACK_POSITIONS_STORE, { keyPath: "songId" });
|
|
151
|
+
}
|
|
152
|
+
if (!db.objectStoreNames.contains(LAST_PLAYED_STORE)) {
|
|
153
|
+
db.createObjectStore(LAST_PLAYED_STORE, { keyPath: "playlistId" });
|
|
154
|
+
}
|
|
155
|
+
if (!db.objectStoreNames.contains(SETTINGS_STORE)) {
|
|
156
|
+
db.createObjectStore(SETTINGS_STORE, { keyPath: "key" });
|
|
157
|
+
}
|
|
158
|
+
if (!db.objectStoreNames.contains(DOC_INDEX_STORE)) {
|
|
159
|
+
db.createObjectStore(DOC_INDEX_STORE, { keyPath: "docId" });
|
|
160
|
+
}
|
|
161
|
+
if (!db.objectStoreNames.contains(KNOCKS_STORE)) {
|
|
162
|
+
db.createObjectStore(KNOCKS_STORE, { keyPath: "id" });
|
|
163
|
+
}
|
|
164
|
+
if (!db.objectStoreNames.contains(ACCESS_GRANTS_STORE)) {
|
|
165
|
+
db.createObjectStore(ACCESS_GRANTS_STORE, { keyPath: "nodeId" });
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return cachedDB;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// reset the database cache - for testing only
|
|
174
|
+
export function resetDBCache(): void {
|
|
175
|
+
cachedDB = null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// live query configuration
|
|
179
|
+
interface LiveQueryConfig {
|
|
180
|
+
dbName: string;
|
|
181
|
+
storeName: string;
|
|
182
|
+
queryFn?: (item: unknown) => boolean;
|
|
183
|
+
fields?: string[];
|
|
184
|
+
limit?: number | null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function arraysDiffer<T>(a: T[], b: T[]): boolean {
|
|
188
|
+
if (a.length !== b.length) return true;
|
|
189
|
+
return a.some((item, index) => {
|
|
190
|
+
if (typeof item === "object" && item !== null && b[index] !== null) {
|
|
191
|
+
return JSON.stringify(item) !== JSON.stringify(b[index]);
|
|
192
|
+
}
|
|
193
|
+
return item !== b[index];
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// global registry to track all live queries for direct same-tab updates
|
|
198
|
+
const globalQueryRegistry = new Map<string, Set<() => void>>();
|
|
199
|
+
|
|
200
|
+
export function createLiveQuery<T>({
|
|
201
|
+
dbName,
|
|
202
|
+
storeName,
|
|
203
|
+
queryFn,
|
|
204
|
+
fields = [],
|
|
205
|
+
limit = null,
|
|
206
|
+
}: LiveQueryConfig): Signal<T[]> {
|
|
207
|
+
const signal = createSignal<T[]>([]);
|
|
208
|
+
const bc = new BroadcastChannel(`${dbName}-changes`);
|
|
209
|
+
let last: T[] = [];
|
|
210
|
+
|
|
211
|
+
const validStores = [
|
|
212
|
+
PLAYBACK_POSITIONS_STORE,
|
|
213
|
+
LAST_PLAYED_STORE,
|
|
214
|
+
SETTINGS_STORE,
|
|
215
|
+
DOC_INDEX_STORE,
|
|
216
|
+
KNOCKS_STORE,
|
|
217
|
+
ACCESS_GRANTS_STORE,
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
async function fetchAndUpdate() {
|
|
221
|
+
try {
|
|
222
|
+
if (!validStores.includes(storeName)) {
|
|
223
|
+
signal.set([]);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const db = await setupDB();
|
|
228
|
+
let items = await (db as IDBPDatabase).getAll(storeName);
|
|
229
|
+
|
|
230
|
+
if (queryFn) items = items.filter(queryFn);
|
|
231
|
+
if (limit) items = items.slice(0, limit);
|
|
232
|
+
|
|
233
|
+
const filtered = items.map((item): T => {
|
|
234
|
+
if (fields.length === 0) return item as T;
|
|
235
|
+
|
|
236
|
+
const rec = item as Record<string, unknown>;
|
|
237
|
+
const out: Record<string, unknown> = {};
|
|
238
|
+
// copy whichever key field the record uses
|
|
239
|
+
if (typeof rec.id === "string") out.id = rec.id;
|
|
240
|
+
if (typeof rec.docId === "string") out.docId = rec.docId;
|
|
241
|
+
if (typeof rec.songId === "string") out.songId = rec.songId;
|
|
242
|
+
if (typeof rec.playlistId === "string") out.playlistId = rec.playlistId;
|
|
243
|
+
if (typeof rec.nodeId === "string") out.nodeId = rec.nodeId;
|
|
244
|
+
for (const f of fields) {
|
|
245
|
+
out[f] = rec[f];
|
|
246
|
+
}
|
|
247
|
+
return out as T;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (arraysDiffer(last, filtered)) {
|
|
251
|
+
last = filtered;
|
|
252
|
+
signal.set(filtered);
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error("error in fetchandupdate:", error);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const registryKey = `${dbName}-${storeName}`;
|
|
260
|
+
if (!globalQueryRegistry.has(registryKey)) {
|
|
261
|
+
globalQueryRegistry.set(registryKey, new Set());
|
|
262
|
+
}
|
|
263
|
+
const querySet = globalQueryRegistry.get(registryKey)!;
|
|
264
|
+
querySet.add(fetchAndUpdate);
|
|
265
|
+
|
|
266
|
+
bc.onmessage = (e) => {
|
|
267
|
+
if (e.data?.type === "mutation" && e.data.store === storeName) {
|
|
268
|
+
fetchAndUpdate();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
fetchAndUpdate();
|
|
273
|
+
|
|
274
|
+
const originalSignal = signal;
|
|
275
|
+
return {
|
|
276
|
+
...originalSignal,
|
|
277
|
+
subscribe: (fn: (value: T[]) => void) => {
|
|
278
|
+
const unsubscribe = originalSignal.subscribe(fn);
|
|
279
|
+
return () => {
|
|
280
|
+
unsubscribe();
|
|
281
|
+
querySet.delete(fetchAndUpdate);
|
|
282
|
+
if (querySet.size === 0) {
|
|
283
|
+
globalQueryRegistry.delete(registryKey);
|
|
284
|
+
}
|
|
285
|
+
bc.close();
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// --- playback positions ---
|
|
292
|
+
|
|
293
|
+
export async function loadAllPlaybackPositions(): Promise<Map<string, number>> {
|
|
294
|
+
try {
|
|
295
|
+
const db = await setupDB();
|
|
296
|
+
const records = await db.getAll(PLAYBACK_POSITIONS_STORE);
|
|
297
|
+
const map = new Map<string, number>();
|
|
298
|
+
for (const r of records) {
|
|
299
|
+
map.set(r.songId, r.position);
|
|
300
|
+
}
|
|
301
|
+
return map;
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.warn("error loading playback positions:", error);
|
|
304
|
+
return new Map();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function savePlaybackPosition(
|
|
309
|
+
songId: string,
|
|
310
|
+
position: number
|
|
311
|
+
): Promise<void> {
|
|
312
|
+
try {
|
|
313
|
+
const db = await setupDB();
|
|
314
|
+
await db.put(PLAYBACK_POSITIONS_STORE, {
|
|
315
|
+
songId,
|
|
316
|
+
position,
|
|
317
|
+
updatedAt: Date.now(),
|
|
318
|
+
});
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.warn(`error saving playback position for ${songId}:`, error);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export async function deletePlaybackPosition(songId: string): Promise<void> {
|
|
325
|
+
try {
|
|
326
|
+
const db = await setupDB();
|
|
327
|
+
await db.delete(PLAYBACK_POSITIONS_STORE, songId);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.warn(`error deleting playback position for ${songId}:`, error);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// --- last played ---
|
|
334
|
+
|
|
335
|
+
export async function saveLastPlayed(
|
|
336
|
+
playlistId: string,
|
|
337
|
+
songId: string
|
|
338
|
+
): Promise<void> {
|
|
339
|
+
try {
|
|
340
|
+
const db = await setupDB();
|
|
341
|
+
await db.put(LAST_PLAYED_STORE, {
|
|
342
|
+
playlistId,
|
|
343
|
+
songId,
|
|
344
|
+
updatedAt: Date.now(),
|
|
345
|
+
});
|
|
346
|
+
} catch (error) {
|
|
347
|
+
console.warn(`error saving last played for playlist ${playlistId}:`, error);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function loadLastPlayed(
|
|
352
|
+
playlistId: string
|
|
353
|
+
): Promise<string | null> {
|
|
354
|
+
try {
|
|
355
|
+
const db = await setupDB();
|
|
356
|
+
const record = await db.get(LAST_PLAYED_STORE, playlistId);
|
|
357
|
+
return record?.songId ?? null;
|
|
358
|
+
} catch (error) {
|
|
359
|
+
console.warn(
|
|
360
|
+
`error loading last played for playlist ${playlistId}:`,
|
|
361
|
+
error
|
|
362
|
+
);
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// --- settings ---
|
|
368
|
+
|
|
369
|
+
export async function saveSetting(key: string, value: unknown): Promise<void> {
|
|
370
|
+
try {
|
|
371
|
+
const db = await setupDB();
|
|
372
|
+
await db.put(SETTINGS_STORE, { key, value, updatedAt: Date.now() });
|
|
373
|
+
} catch (error) {
|
|
374
|
+
console.warn(`error saving setting ${key}:`, error);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export async function loadSetting<T>(key: string): Promise<T | null> {
|
|
379
|
+
try {
|
|
380
|
+
const db = await setupDB();
|
|
381
|
+
const record = await db.get(SETTINGS_STORE, key);
|
|
382
|
+
return record === undefined ? null : (record.value as T);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
console.warn(`error loading setting ${key}:`, error);
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// --- compatibility stubs ---
|
|
390
|
+
// kept for services that get minimal-compile-fix treatment only:
|
|
391
|
+
// standaloneService, playlistDownloadService, streamingAudioService.
|
|
392
|
+
// they are no-ops at runtime; real implementations are in playlistDocService.
|
|
393
|
+
|
|
394
|
+
import type { Playlist, Song } from "../types/playlist.js";
|
|
395
|
+
|
|
396
|
+
export interface MutationConfig<T> {
|
|
397
|
+
dbName: string;
|
|
398
|
+
storeName: string;
|
|
399
|
+
key: string;
|
|
400
|
+
updateFn: (current: T | null) => T;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// no-op stub - real mutations go through playlistDocService
|
|
404
|
+
export async function mutateAndNotify<T extends Playlist | Song>(
|
|
405
|
+
_config: MutationConfig<T>
|
|
406
|
+
): Promise<void> {
|
|
407
|
+
console.warn(
|
|
408
|
+
"mutateandnotify: called on stub - data is doc-backed, use playlistDocService"
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// stub - returns empty array; real impl in playlistDocService
|
|
413
|
+
export async function getSongsWithAudioData(
|
|
414
|
+
_songIds: string[]
|
|
415
|
+
): Promise<Song[]> {
|
|
416
|
+
console.warn(
|
|
417
|
+
"getsongswithaudiodata: stub - use playlistDocService instead"
|
|
418
|
+
);
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// stub - no-op
|
|
423
|
+
export async function updatePlaylist(
|
|
424
|
+
_id: string,
|
|
425
|
+
_updates: Partial<Playlist>
|
|
426
|
+
): Promise<void> {
|
|
427
|
+
console.warn("updateplaylist: stub - use playlistDocService instead");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// stub - no-op
|
|
431
|
+
export async function updateSong(
|
|
432
|
+
_id: string,
|
|
433
|
+
_updates: Partial<Song>
|
|
434
|
+
): Promise<void> {
|
|
435
|
+
console.warn("updatesong: stub - use playlistDocService instead");
|
|
436
|
+
}
|