@mulmoclaude/spotify-plugin 0.1.0

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/dist/vue.js ADDED
@@ -0,0 +1,867 @@
1
+ import { t as TOOL_DEFINITION } from "./definition-CfBmxEFr.js";
2
+ import { Fragment, computed, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, defineComponent, normalizeClass, onMounted, onUnmounted, openBlock, ref, renderList, toDisplayString, unref, vModelText, withDirectives, withModifiers } from "vue";
3
+ import { useRuntime } from "gui-chat-protocol/vue";
4
+ //#endregion
5
+ //#region src/lang/index.ts
6
+ var MESSAGES = {
7
+ en: {
8
+ title: "Spotify",
9
+ notConnected: "Not connected to Spotify",
10
+ notConfigured: "Client ID not configured",
11
+ configureHelp: "Paste your Spotify Developer Dashboard Client ID and click Save.",
12
+ configurePlaceholder: "Spotify Client ID",
13
+ save: "Save",
14
+ saving: "Saving…",
15
+ saved: "Saved.",
16
+ saveFailed: "Save failed.",
17
+ connect: "Connect Spotify",
18
+ connecting: "Opening Spotify consent…",
19
+ connected: "Connected.",
20
+ reconnect: "Reconnect",
21
+ disconnect: "Disconnect",
22
+ refresh: "Refresh",
23
+ setupGuideLink: "How do I get a Client ID?",
24
+ scopes: "Scopes",
25
+ expiresAt: "Expires",
26
+ tabLiked: "Liked",
27
+ tabPlaylists: "Playlists",
28
+ tabRecent: "Recent",
29
+ tabNowPlaying: "Now playing",
30
+ tabSearch: "Search",
31
+ searchPlaceholder: "Search tracks, artists, albums, playlists",
32
+ searchSubmit: "Search",
33
+ searchHint: "Type a query and press Search.",
34
+ searchEmpty: "No results.",
35
+ searchTracks: "Tracks",
36
+ searchArtists: "Artists",
37
+ searchAlbums: "Albums",
38
+ searchPlaylists: "Playlists",
39
+ empty: "Nothing to show.",
40
+ emptyLiked: "You haven't liked any songs yet.",
41
+ emptyPlaylists: "No playlists found.",
42
+ emptyRecent: "No recently played tracks.",
43
+ emptyNowPlaying: "Nothing is playing right now.",
44
+ loading: "Loading…",
45
+ loadFailed: "Failed to load.",
46
+ retry: "Retry",
47
+ trackBy: "by",
48
+ tracksCount: "tracks",
49
+ previewSummary: "Spotify",
50
+ playerControls: "Playback",
51
+ premiumRequired: "Spotify Premium is required to control playback. Free / open accounts cannot use these controls; the rest of the plugin still works.",
52
+ volume: "Volume",
53
+ devices: "Devices",
54
+ deviceActive: "active",
55
+ transferToDevice: "Transfer here",
56
+ btnPrevious: "Previous track",
57
+ btnPause: "Pause",
58
+ btnPlay: "Play",
59
+ btnNext: "Next track"
60
+ },
61
+ ja: {
62
+ title: "Spotify",
63
+ notConnected: "Spotify に未接続です",
64
+ notConfigured: "Client ID が未設定です",
65
+ configureHelp: "Spotify Developer Dashboard で発行した Client ID を貼り付けて Save を押してください。",
66
+ configurePlaceholder: "Spotify Client ID",
67
+ save: "保存",
68
+ saving: "保存中…",
69
+ saved: "保存しました。",
70
+ saveFailed: "保存に失敗しました。",
71
+ connect: "Spotify に接続",
72
+ connecting: "Spotify の同意画面を開きます…",
73
+ connected: "接続済み",
74
+ reconnect: "再接続",
75
+ disconnect: "切断",
76
+ refresh: "更新",
77
+ setupGuideLink: "Client ID の取得方法",
78
+ scopes: "Scope",
79
+ expiresAt: "有効期限",
80
+ tabLiked: "Liked",
81
+ tabPlaylists: "Playlists",
82
+ tabRecent: "Recent",
83
+ tabNowPlaying: "Now playing",
84
+ tabSearch: "検索",
85
+ searchPlaceholder: "曲・アーティスト・アルバム・プレイリストを検索",
86
+ searchSubmit: "検索",
87
+ searchHint: "クエリを入力して検索を押してください。",
88
+ searchEmpty: "ヒットなし。",
89
+ searchTracks: "曲",
90
+ searchArtists: "アーティスト",
91
+ searchAlbums: "アルバム",
92
+ searchPlaylists: "プレイリスト",
93
+ empty: "表示する項目がありません。",
94
+ emptyLiked: "Liked Songs がありません。",
95
+ emptyPlaylists: "Playlist が見つかりませんでした。",
96
+ emptyRecent: "最近聞いた曲はありません。",
97
+ emptyNowPlaying: "現在再生中の曲はありません。",
98
+ loading: "読み込み中…",
99
+ loadFailed: "読み込みに失敗しました。",
100
+ retry: "再試行",
101
+ trackBy: "—",
102
+ tracksCount: "曲",
103
+ previewSummary: "Spotify",
104
+ playerControls: "再生制御",
105
+ premiumRequired: "再生制御には Spotify Premium が必要です。Free / Open アカウントでは利用できません。それ以外の機能はそのまま使えます。",
106
+ volume: "音量",
107
+ devices: "デバイス",
108
+ deviceActive: "アクティブ",
109
+ transferToDevice: "ここに移す",
110
+ btnPrevious: "前の曲",
111
+ btnPause: "一時停止",
112
+ btnPlay: "再生",
113
+ btnNext: "次の曲"
114
+ }
115
+ };
116
+ function isSupportedLocale(value) {
117
+ return Object.prototype.hasOwnProperty.call(MESSAGES, value);
118
+ }
119
+ function useT() {
120
+ const { locale } = useRuntime();
121
+ return computed(() => isSupportedLocale(locale.value) ? MESSAGES[locale.value] : MESSAGES.en);
122
+ }
123
+ //#endregion
124
+ //#region src/View.vue?vue&type=script&setup=true&lang.ts
125
+ var _hoisted_1$1 = { class: "spotify-view" };
126
+ var _hoisted_2$1 = { class: "spotify-header" };
127
+ var _hoisted_3$1 = { class: "spotify-status" };
128
+ var _hoisted_4 = { class: "spotify-connected-pill" };
129
+ var _hoisted_5 = { class: "spotify-expiry" };
130
+ var _hoisted_6 = ["disabled"];
131
+ var _hoisted_7 = {
132
+ key: 0,
133
+ class: "spotify-configure"
134
+ };
135
+ var _hoisted_8 = { class: "spotify-configure-help" };
136
+ var _hoisted_9 = ["placeholder"];
137
+ var _hoisted_10 = ["disabled"];
138
+ var _hoisted_11 = {
139
+ key: 0,
140
+ class: "spotify-error"
141
+ };
142
+ var _hoisted_12 = {
143
+ key: 1,
144
+ class: "spotify-connect-section"
145
+ };
146
+ var _hoisted_13 = ["disabled"];
147
+ var _hoisted_14 = {
148
+ key: 2,
149
+ class: "spotify-connected"
150
+ };
151
+ var _hoisted_15 = { class: "spotify-tab-row" };
152
+ var _hoisted_16 = {
153
+ class: "spotify-tabs",
154
+ role: "tablist"
155
+ };
156
+ var _hoisted_17 = ["aria-selected", "onClick"];
157
+ var _hoisted_18 = { class: "spotify-content" };
158
+ var _hoisted_19 = {
159
+ key: 0,
160
+ class: "spotify-loading"
161
+ };
162
+ var _hoisted_20 = {
163
+ key: 1,
164
+ class: "spotify-error"
165
+ };
166
+ var _hoisted_21 = {
167
+ key: 2,
168
+ class: "spotify-list"
169
+ };
170
+ var _hoisted_22 = ["onClick"];
171
+ var _hoisted_23 = ["src"];
172
+ var _hoisted_24 = { class: "spotify-track-meta" };
173
+ var _hoisted_25 = { class: "spotify-track-name" };
174
+ var _hoisted_26 = { class: "spotify-track-artists" };
175
+ var _hoisted_27 = { class: "spotify-track-duration" };
176
+ var _hoisted_28 = {
177
+ key: 3,
178
+ class: "spotify-empty"
179
+ };
180
+ var _hoisted_29 = {
181
+ key: 4,
182
+ class: "spotify-list"
183
+ };
184
+ var _hoisted_30 = ["onClick"];
185
+ var _hoisted_31 = ["src"];
186
+ var _hoisted_32 = { class: "spotify-track-meta" };
187
+ var _hoisted_33 = { class: "spotify-track-name" };
188
+ var _hoisted_34 = { class: "spotify-track-artists" };
189
+ var _hoisted_35 = {
190
+ key: 5,
191
+ class: "spotify-empty"
192
+ };
193
+ var _hoisted_36 = {
194
+ key: 6,
195
+ class: "spotify-list"
196
+ };
197
+ var _hoisted_37 = ["onClick"];
198
+ var _hoisted_38 = ["src"];
199
+ var _hoisted_39 = { class: "spotify-track-meta" };
200
+ var _hoisted_40 = { class: "spotify-track-name" };
201
+ var _hoisted_41 = { class: "spotify-track-artists" };
202
+ var _hoisted_42 = {
203
+ key: 7,
204
+ class: "spotify-empty"
205
+ };
206
+ var _hoisted_43 = {
207
+ key: 0,
208
+ class: "spotify-now-playing"
209
+ };
210
+ var _hoisted_44 = ["src"];
211
+ var _hoisted_45 = { class: "spotify-track-meta" };
212
+ var _hoisted_46 = { class: "spotify-track-name" };
213
+ var _hoisted_47 = { class: "spotify-track-artists" };
214
+ var _hoisted_48 = { class: "spotify-track-album" };
215
+ var _hoisted_49 = {
216
+ key: 1,
217
+ class: "spotify-empty"
218
+ };
219
+ var _hoisted_50 = {
220
+ key: 2,
221
+ class: "spotify-player-locked"
222
+ };
223
+ var _hoisted_51 = {
224
+ key: 3,
225
+ class: "spotify-player"
226
+ };
227
+ var _hoisted_52 = { class: "spotify-player-buttons" };
228
+ var _hoisted_53 = ["aria-label", "disabled"];
229
+ var _hoisted_54 = ["aria-label", "disabled"];
230
+ var _hoisted_55 = ["aria-label", "disabled"];
231
+ var _hoisted_56 = ["aria-label", "disabled"];
232
+ var _hoisted_57 = { class: "spotify-player-volume" };
233
+ var _hoisted_58 = { for: "spotify-volume" };
234
+ var _hoisted_59 = ["disabled"];
235
+ var _hoisted_60 = {
236
+ key: 0,
237
+ class: "spotify-error"
238
+ };
239
+ var _hoisted_61 = {
240
+ key: 4,
241
+ class: "spotify-devices"
242
+ };
243
+ var _hoisted_62 = { class: "spotify-list" };
244
+ var _hoisted_63 = { class: "spotify-device-name" };
245
+ var _hoisted_64 = { class: "spotify-device-type" };
246
+ var _hoisted_65 = {
247
+ key: 0,
248
+ class: "spotify-device-active"
249
+ };
250
+ var _hoisted_66 = [
251
+ "disabled",
252
+ "aria-disabled",
253
+ "onClick"
254
+ ];
255
+ var _hoisted_67 = [
256
+ "placeholder",
257
+ "aria-label",
258
+ "disabled"
259
+ ];
260
+ var _hoisted_68 = ["disabled"];
261
+ var _hoisted_69 = {
262
+ key: 0,
263
+ class: "spotify-empty"
264
+ };
265
+ var _hoisted_70 = {
266
+ key: 1,
267
+ class: "spotify-search-results"
268
+ };
269
+ var _hoisted_71 = {
270
+ key: 0,
271
+ class: "spotify-search-section"
272
+ };
273
+ var _hoisted_72 = { class: "spotify-list" };
274
+ var _hoisted_73 = ["onClick"];
275
+ var _hoisted_74 = ["src"];
276
+ var _hoisted_75 = { class: "spotify-track-meta" };
277
+ var _hoisted_76 = { class: "spotify-track-name" };
278
+ var _hoisted_77 = { class: "spotify-track-artists" };
279
+ var _hoisted_78 = {
280
+ key: 1,
281
+ class: "spotify-search-section"
282
+ };
283
+ var _hoisted_79 = { class: "spotify-list" };
284
+ var _hoisted_80 = ["onClick"];
285
+ var _hoisted_81 = ["src"];
286
+ var _hoisted_82 = { class: "spotify-track-meta" };
287
+ var _hoisted_83 = { class: "spotify-track-name" };
288
+ var _hoisted_84 = {
289
+ key: 0,
290
+ class: "spotify-track-artists"
291
+ };
292
+ var _hoisted_85 = {
293
+ key: 2,
294
+ class: "spotify-search-section"
295
+ };
296
+ var _hoisted_86 = { class: "spotify-list" };
297
+ var _hoisted_87 = ["onClick"];
298
+ var _hoisted_88 = ["src"];
299
+ var _hoisted_89 = { class: "spotify-track-meta" };
300
+ var _hoisted_90 = { class: "spotify-track-name" };
301
+ var _hoisted_91 = { class: "spotify-track-artists" };
302
+ var _hoisted_92 = {
303
+ key: 3,
304
+ class: "spotify-search-section"
305
+ };
306
+ var _hoisted_93 = { class: "spotify-list" };
307
+ var _hoisted_94 = ["onClick"];
308
+ var _hoisted_95 = ["src"];
309
+ var _hoisted_96 = { class: "spotify-track-meta" };
310
+ var _hoisted_97 = { class: "spotify-track-name" };
311
+ var _hoisted_98 = { class: "spotify-track-artists" };
312
+ var _hoisted_99 = {
313
+ key: 2,
314
+ class: "spotify-empty"
315
+ };
316
+ var View_vue_vue_type_script_setup_true_lang_default = /* @__PURE__ */ defineComponent({
317
+ __name: "View",
318
+ setup(__props) {
319
+ const { dispatch, openUrl, pubsub, log } = useRuntime();
320
+ const t = useT();
321
+ const status = ref(null);
322
+ const activeTab = ref("liked");
323
+ const liked = ref(null);
324
+ const playlists = ref(null);
325
+ const recent = ref(null);
326
+ const nowPlaying = ref(void 0);
327
+ const searchQuery = ref("");
328
+ const searchResult = ref(null);
329
+ const isSearching = ref(false);
330
+ const tabError = ref(null);
331
+ const isLoadingTab = ref(false);
332
+ const clientIdInput = ref("");
333
+ const isSavingClientId = ref(false);
334
+ const saveError = ref(null);
335
+ const isConnecting = ref(false);
336
+ const devices = ref([]);
337
+ const playerError = ref(null);
338
+ const isPlayerBusy = ref(false);
339
+ const volumeInput = ref(50);
340
+ async function refreshStatus() {
341
+ try {
342
+ const response = await dispatch({ kind: "status" });
343
+ if (response.ok) status.value = response.data;
344
+ } catch (err) {
345
+ log.warn("status fetch failed", { error: err instanceof Error ? err.message : String(err) });
346
+ }
347
+ }
348
+ async function saveClientId() {
349
+ if (clientIdInput.value.trim().length === 0) return;
350
+ isSavingClientId.value = true;
351
+ saveError.value = null;
352
+ try {
353
+ await dispatch({
354
+ kind: "configure",
355
+ clientId: clientIdInput.value.trim()
356
+ });
357
+ clientIdInput.value = "";
358
+ await refreshStatus();
359
+ } catch (err) {
360
+ saveError.value = err instanceof Error ? err.message : String(err);
361
+ } finally {
362
+ isSavingClientId.value = false;
363
+ }
364
+ }
365
+ function computeRedirectUri() {
366
+ return `${window.location.origin.replace("//localhost:", "//127.0.0.1:").replace("//localhost/", "//127.0.0.1/")}/api/plugins/runtime/oauth-callback/spotify`;
367
+ }
368
+ async function startConnect() {
369
+ isConnecting.value = true;
370
+ try {
371
+ const response = await dispatch({
372
+ kind: "connect",
373
+ redirectUri: computeRedirectUri()
374
+ });
375
+ if (response.ok && response.data?.authorizeUrl) window.open(response.data.authorizeUrl, "_blank", "noopener,noreferrer");
376
+ else log.warn("connect failed", { response });
377
+ } catch (err) {
378
+ log.warn("connect dispatch threw", { error: err instanceof Error ? err.message : String(err) });
379
+ } finally {
380
+ isConnecting.value = false;
381
+ }
382
+ }
383
+ async function loadLiked() {
384
+ const response = await dispatch({ kind: "liked" });
385
+ if (response.ok && response.data) liked.value = response.data;
386
+ else tabError.value = response.message ?? t.value.loadFailed;
387
+ }
388
+ async function loadPlaylists() {
389
+ const response = await dispatch({ kind: "playlists" });
390
+ if (response.ok && response.data) playlists.value = response.data;
391
+ else tabError.value = response.message ?? t.value.loadFailed;
392
+ }
393
+ async function loadRecent() {
394
+ const response = await dispatch({ kind: "recent" });
395
+ if (response.ok && response.data) recent.value = response.data;
396
+ else tabError.value = response.message ?? t.value.loadFailed;
397
+ }
398
+ async function loadNowPlaying() {
399
+ const response = await dispatch({ kind: "nowPlaying" });
400
+ if (response.ok) nowPlaying.value = response.data ?? null;
401
+ else tabError.value = response.message ?? t.value.loadFailed;
402
+ loadDevices();
403
+ }
404
+ async function loadSearch() {
405
+ if (searchQuery.value.trim().length > 0) await runSearch();
406
+ }
407
+ async function runSearch() {
408
+ const query = searchQuery.value.trim();
409
+ if (query.length === 0) return;
410
+ isSearching.value = true;
411
+ tabError.value = null;
412
+ try {
413
+ const response = await dispatch({
414
+ kind: "search",
415
+ query
416
+ });
417
+ if (response.ok && response.data) searchResult.value = response.data;
418
+ else tabError.value = response.message ?? t.value.loadFailed;
419
+ } catch (err) {
420
+ tabError.value = err instanceof Error ? err.message : String(err);
421
+ } finally {
422
+ isSearching.value = false;
423
+ }
424
+ }
425
+ const TAB_LOADERS = {
426
+ liked: loadLiked,
427
+ playlists: loadPlaylists,
428
+ recent: loadRecent,
429
+ nowPlaying: loadNowPlaying,
430
+ search: loadSearch
431
+ };
432
+ function tabIsCached(tab) {
433
+ if (tab === "liked") return liked.value !== null;
434
+ if (tab === "playlists") return playlists.value !== null;
435
+ if (tab === "recent") return recent.value !== null;
436
+ if (tab === "nowPlaying") return nowPlaying.value !== void 0;
437
+ return true;
438
+ }
439
+ async function loadActiveTab(force = false) {
440
+ if (!status.value?.connected) return;
441
+ tabError.value = null;
442
+ if (!force && tabIsCached(activeTab.value)) return;
443
+ isLoadingTab.value = true;
444
+ try {
445
+ await TAB_LOADERS[activeTab.value]();
446
+ } catch (err) {
447
+ tabError.value = err instanceof Error ? err.message : String(err);
448
+ } finally {
449
+ isLoadingTab.value = false;
450
+ }
451
+ }
452
+ function selectTab(next) {
453
+ activeTab.value = next;
454
+ loadActiveTab();
455
+ }
456
+ function refreshActiveTab() {
457
+ loadActiveTab(true);
458
+ }
459
+ async function loadDevices() {
460
+ try {
461
+ const response = await dispatch({ kind: "getDevices" });
462
+ if (response.ok && response.data) devices.value = response.data;
463
+ } catch (err) {
464
+ log.warn("getDevices failed", { error: err instanceof Error ? err.message : String(err) });
465
+ }
466
+ }
467
+ async function dispatchPlayer(args, busyMessage) {
468
+ isPlayerBusy.value = true;
469
+ playerError.value = null;
470
+ try {
471
+ const response = await dispatch(args);
472
+ if (!response.ok) playerError.value = response.message ?? busyMessage;
473
+ else await loadNowPlaying();
474
+ } catch (err) {
475
+ playerError.value = err instanceof Error ? err.message : String(err);
476
+ } finally {
477
+ isPlayerBusy.value = false;
478
+ }
479
+ }
480
+ function playerPlay() {
481
+ dispatchPlayer({ kind: "play" }, t.value.loadFailed);
482
+ }
483
+ function playerPause() {
484
+ dispatchPlayer({ kind: "pause" }, t.value.loadFailed);
485
+ }
486
+ function playerNext() {
487
+ dispatchPlayer({ kind: "next" }, t.value.loadFailed);
488
+ }
489
+ function playerPrevious() {
490
+ dispatchPlayer({ kind: "previous" }, t.value.loadFailed);
491
+ }
492
+ function playerVolume() {
493
+ dispatchPlayer({
494
+ kind: "setVolume",
495
+ volumePercent: volumeInput.value
496
+ }, t.value.loadFailed);
497
+ }
498
+ function playerTransfer(deviceId) {
499
+ dispatchPlayer({
500
+ kind: "transferPlayback",
501
+ deviceId,
502
+ play: false
503
+ }, t.value.loadFailed).then(() => loadDevices());
504
+ }
505
+ function safeOpenUrl(url) {
506
+ if (typeof url === "string" && url.length > 0) openUrl(url);
507
+ }
508
+ function formatDuration(ms) {
509
+ const totalSeconds = Math.floor(ms / 1e3);
510
+ return `${Math.floor(totalSeconds / 60)}:${(totalSeconds % 60).toString().padStart(2, "0")}`;
511
+ }
512
+ const searchResultIsEmpty = computed(() => {
513
+ const result = searchResult.value;
514
+ if (!result) return false;
515
+ return !(result.tracks?.length || result.artists?.length || result.albums?.length || result.playlists?.length);
516
+ });
517
+ const expiryDisplay = computed(() => {
518
+ if (!status.value?.expiresAt) return "";
519
+ try {
520
+ return new Date(status.value.expiresAt).toLocaleString();
521
+ } catch {
522
+ return status.value.expiresAt;
523
+ }
524
+ });
525
+ const unsubs = [];
526
+ onMounted(() => {
527
+ unsubs.push(pubsub.subscribe("connected", () => {
528
+ refreshStatus().then(() => loadActiveTab(true));
529
+ }));
530
+ refreshStatus().then(() => loadActiveTab());
531
+ });
532
+ onUnmounted(() => {
533
+ for (const unsub of unsubs) unsub();
534
+ });
535
+ return (_ctx, _cache) => {
536
+ return openBlock(), createElementBlock("div", _hoisted_1$1, [createElementVNode("header", _hoisted_2$1, [createElementVNode("h2", null, toDisplayString(unref(t).title), 1), createElementVNode("div", _hoisted_3$1, [status.value === null ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(toDisplayString(unref(t).loading), 1)], 64)) : !status.value.clientIdConfigured ? (openBlock(), createElementBlock(Fragment, { key: 1 }, [createTextVNode(toDisplayString(unref(t).notConfigured), 1)], 64)) : !status.value.connected ? (openBlock(), createElementBlock(Fragment, { key: 2 }, [createTextVNode(toDisplayString(unref(t).notConnected), 1)], 64)) : (openBlock(), createElementBlock(Fragment, { key: 3 }, [
537
+ createElementVNode("span", _hoisted_4, toDisplayString(unref(t).connected), 1),
538
+ createElementVNode("span", _hoisted_5, toDisplayString(unref(t).expiresAt) + ": " + toDisplayString(expiryDisplay.value), 1),
539
+ createElementVNode("button", {
540
+ type: "button",
541
+ class: "spotify-reconnect",
542
+ disabled: isConnecting.value,
543
+ onClick: startConnect
544
+ }, toDisplayString(isConnecting.value ? unref(t).connecting : unref(t).reconnect), 9, _hoisted_6)
545
+ ], 64))])]), status.value && !status.value.clientIdConfigured ? (openBlock(), createElementBlock("section", _hoisted_7, [
546
+ createElementVNode("p", _hoisted_8, toDisplayString(unref(t).configureHelp), 1),
547
+ createElementVNode("form", {
548
+ class: "spotify-configure-form",
549
+ onSubmit: withModifiers(saveClientId, ["prevent"])
550
+ }, [withDirectives(createElementVNode("input", {
551
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => clientIdInput.value = $event),
552
+ placeholder: unref(t).configurePlaceholder,
553
+ class: "spotify-input",
554
+ type: "text",
555
+ autocomplete: "off"
556
+ }, null, 8, _hoisted_9), [[vModelText, clientIdInput.value]]), createElementVNode("button", {
557
+ type: "submit",
558
+ disabled: isSavingClientId.value || clientIdInput.value.trim().length === 0,
559
+ class: "spotify-btn-primary"
560
+ }, toDisplayString(isSavingClientId.value ? unref(t).saving : unref(t).save), 9, _hoisted_10)], 32),
561
+ saveError.value ? (openBlock(), createElementBlock("p", _hoisted_11, toDisplayString(saveError.value), 1)) : createCommentVNode("", true)
562
+ ])) : status.value && status.value.clientIdConfigured && !status.value.connected ? (openBlock(), createElementBlock("section", _hoisted_12, [createElementVNode("button", {
563
+ type: "button",
564
+ disabled: isConnecting.value,
565
+ class: "spotify-btn-primary",
566
+ onClick: startConnect
567
+ }, toDisplayString(isConnecting.value ? unref(t).connecting : unref(t).connect), 9, _hoisted_13)])) : status.value?.connected ? (openBlock(), createElementBlock("div", _hoisted_14, [createElementVNode("div", _hoisted_15, [createElementVNode("nav", _hoisted_16, [(openBlock(), createElementBlock(Fragment, null, renderList([
568
+ "liked",
569
+ "playlists",
570
+ "recent",
571
+ "nowPlaying",
572
+ "search"
573
+ ], (tab) => {
574
+ return createElementVNode("button", {
575
+ key: tab,
576
+ type: "button",
577
+ role: "tab",
578
+ "aria-selected": activeTab.value === tab,
579
+ class: normalizeClass(["spotify-tab", { "spotify-tab-active": activeTab.value === tab }]),
580
+ onClick: ($event) => selectTab(tab)
581
+ }, toDisplayString(tab === "liked" ? unref(t).tabLiked : tab === "playlists" ? unref(t).tabPlaylists : tab === "recent" ? unref(t).tabRecent : tab === "nowPlaying" ? unref(t).tabNowPlaying : unref(t).tabSearch), 11, _hoisted_17);
582
+ }), 64))]), activeTab.value !== "search" ? (openBlock(), createElementBlock("button", {
583
+ key: 0,
584
+ type: "button",
585
+ class: "spotify-refresh",
586
+ onClick: refreshActiveTab
587
+ }, toDisplayString(unref(t).refresh), 1)) : createCommentVNode("", true)]), createElementVNode("div", _hoisted_18, [isLoadingTab.value ? (openBlock(), createElementBlock("p", _hoisted_19, toDisplayString(unref(t).loading), 1)) : tabError.value ? (openBlock(), createElementBlock("p", _hoisted_20, [createTextVNode(toDisplayString(tabError.value) + " ", 1), createElementVNode("button", {
588
+ class: "spotify-retry",
589
+ onClick: refreshActiveTab
590
+ }, toDisplayString(unref(t).retry), 1)])) : activeTab.value === "liked" && liked.value && liked.value.length > 0 ? (openBlock(), createElementBlock("ul", _hoisted_21, [(openBlock(true), createElementBlock(Fragment, null, renderList(liked.value, (track) => {
591
+ return openBlock(), createElementBlock("li", {
592
+ key: track.id,
593
+ class: "spotify-track-row"
594
+ }, [createElementVNode("button", {
595
+ type: "button",
596
+ class: "spotify-track-link",
597
+ onClick: ($event) => safeOpenUrl(track.url)
598
+ }, [
599
+ track.imageUrl ? (openBlock(), createElementBlock("img", {
600
+ key: 0,
601
+ src: track.imageUrl,
602
+ alt: "",
603
+ class: "spotify-cover"
604
+ }, null, 8, _hoisted_23)) : createCommentVNode("", true),
605
+ createElementVNode("span", _hoisted_24, [createElementVNode("span", _hoisted_25, toDisplayString(track.name), 1), createElementVNode("span", _hoisted_26, toDisplayString(unref(t).trackBy) + " " + toDisplayString(track.artists.join(", ")), 1)]),
606
+ createElementVNode("span", _hoisted_27, toDisplayString(formatDuration(track.durationMs)), 1)
607
+ ], 8, _hoisted_22)]);
608
+ }), 128))])) : activeTab.value === "liked" ? (openBlock(), createElementBlock("p", _hoisted_28, toDisplayString(unref(t).emptyLiked), 1)) : activeTab.value === "playlists" && playlists.value && playlists.value.length > 0 ? (openBlock(), createElementBlock("ul", _hoisted_29, [(openBlock(true), createElementBlock(Fragment, null, renderList(playlists.value, (playlist) => {
609
+ return openBlock(), createElementBlock("li", {
610
+ key: playlist.id,
611
+ class: "spotify-playlist-row"
612
+ }, [createElementVNode("button", {
613
+ type: "button",
614
+ class: "spotify-track-link",
615
+ onClick: ($event) => safeOpenUrl(playlist.url)
616
+ }, [playlist.imageUrl ? (openBlock(), createElementBlock("img", {
617
+ key: 0,
618
+ src: playlist.imageUrl,
619
+ alt: "",
620
+ class: "spotify-cover"
621
+ }, null, 8, _hoisted_31)) : createCommentVNode("", true), createElementVNode("span", _hoisted_32, [createElementVNode("span", _hoisted_33, toDisplayString(playlist.name), 1), createElementVNode("span", _hoisted_34, toDisplayString(playlist.trackCount) + " " + toDisplayString(unref(t).tracksCount), 1)])], 8, _hoisted_30)]);
622
+ }), 128))])) : activeTab.value === "playlists" ? (openBlock(), createElementBlock("p", _hoisted_35, toDisplayString(unref(t).emptyPlaylists), 1)) : activeTab.value === "recent" && recent.value && recent.value.length > 0 ? (openBlock(), createElementBlock("ul", _hoisted_36, [(openBlock(true), createElementBlock(Fragment, null, renderList(recent.value, (item) => {
623
+ return openBlock(), createElementBlock("li", {
624
+ key: `${item.track.id}-${item.playedAt}`,
625
+ class: "spotify-track-row"
626
+ }, [createElementVNode("button", {
627
+ type: "button",
628
+ class: "spotify-track-link",
629
+ onClick: ($event) => safeOpenUrl(item.track.url)
630
+ }, [item.track.imageUrl ? (openBlock(), createElementBlock("img", {
631
+ key: 0,
632
+ src: item.track.imageUrl,
633
+ alt: "",
634
+ class: "spotify-cover"
635
+ }, null, 8, _hoisted_38)) : createCommentVNode("", true), createElementVNode("span", _hoisted_39, [createElementVNode("span", _hoisted_40, toDisplayString(item.track.name), 1), createElementVNode("span", _hoisted_41, toDisplayString(unref(t).trackBy) + " " + toDisplayString(item.track.artists.join(", ")), 1)])], 8, _hoisted_37)]);
636
+ }), 128))])) : activeTab.value === "recent" ? (openBlock(), createElementBlock("p", _hoisted_42, toDisplayString(unref(t).emptyRecent), 1)) : activeTab.value === "nowPlaying" ? (openBlock(), createElementBlock(Fragment, { key: 8 }, [
637
+ nowPlaying.value ? (openBlock(), createElementBlock("div", _hoisted_43, [createElementVNode("button", {
638
+ type: "button",
639
+ class: "spotify-track-link",
640
+ onClick: _cache[1] || (_cache[1] = ($event) => safeOpenUrl(nowPlaying.value.url))
641
+ }, [nowPlaying.value.imageUrl ? (openBlock(), createElementBlock("img", {
642
+ key: 0,
643
+ src: nowPlaying.value.imageUrl,
644
+ alt: "",
645
+ class: "spotify-now-cover"
646
+ }, null, 8, _hoisted_44)) : createCommentVNode("", true), createElementVNode("span", _hoisted_45, [
647
+ createElementVNode("span", _hoisted_46, toDisplayString(nowPlaying.value.name), 1),
648
+ createElementVNode("span", _hoisted_47, toDisplayString(unref(t).trackBy) + " " + toDisplayString(nowPlaying.value.artists.join(", ")), 1),
649
+ createElementVNode("span", _hoisted_48, toDisplayString(nowPlaying.value.album), 1)
650
+ ])])])) : (openBlock(), createElementBlock("p", _hoisted_49, toDisplayString(unref(t).emptyNowPlaying), 1)),
651
+ status.value?.isPremium === false ? (openBlock(), createElementBlock("section", _hoisted_50, [createElementVNode("h3", null, toDisplayString(unref(t).playerControls), 1), createElementVNode("p", null, toDisplayString(unref(t).premiumRequired), 1)])) : status.value?.isPremium === true ? (openBlock(), createElementBlock("section", _hoisted_51, [
652
+ createElementVNode("h3", null, toDisplayString(unref(t).playerControls), 1),
653
+ createElementVNode("div", _hoisted_52, [
654
+ createElementVNode("button", {
655
+ type: "button",
656
+ class: "spotify-player-btn",
657
+ "aria-label": unref(t).btnPrevious,
658
+ disabled: isPlayerBusy.value,
659
+ onClick: playerPrevious
660
+ }, "⏮", 8, _hoisted_53),
661
+ createElementVNode("button", {
662
+ type: "button",
663
+ class: "spotify-player-btn",
664
+ "aria-label": unref(t).btnPause,
665
+ disabled: isPlayerBusy.value,
666
+ onClick: playerPause
667
+ }, "⏸", 8, _hoisted_54),
668
+ createElementVNode("button", {
669
+ type: "button",
670
+ class: "spotify-player-btn",
671
+ "aria-label": unref(t).btnPlay,
672
+ disabled: isPlayerBusy.value,
673
+ onClick: playerPlay
674
+ }, "▶", 8, _hoisted_55),
675
+ createElementVNode("button", {
676
+ type: "button",
677
+ class: "spotify-player-btn",
678
+ "aria-label": unref(t).btnNext,
679
+ disabled: isPlayerBusy.value,
680
+ onClick: playerNext
681
+ }, "⏭", 8, _hoisted_56)
682
+ ]),
683
+ createElementVNode("div", _hoisted_57, [createElementVNode("label", _hoisted_58, toDisplayString(unref(t).volume) + ": " + toDisplayString(volumeInput.value), 1), withDirectives(createElementVNode("input", {
684
+ id: "spotify-volume",
685
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => volumeInput.value = $event),
686
+ type: "range",
687
+ min: "0",
688
+ max: "100",
689
+ disabled: isPlayerBusy.value,
690
+ onChange: playerVolume
691
+ }, null, 40, _hoisted_59), [[
692
+ vModelText,
693
+ volumeInput.value,
694
+ void 0,
695
+ { number: true }
696
+ ]])]),
697
+ playerError.value ? (openBlock(), createElementBlock("p", _hoisted_60, toDisplayString(playerError.value), 1)) : createCommentVNode("", true)
698
+ ])) : createCommentVNode("", true),
699
+ devices.value.length > 0 ? (openBlock(), createElementBlock("section", _hoisted_61, [createElementVNode("h3", null, toDisplayString(unref(t).devices), 1), createElementVNode("ul", _hoisted_62, [(openBlock(true), createElementBlock(Fragment, null, renderList(devices.value, (device, idx) => {
700
+ return openBlock(), createElementBlock("li", {
701
+ key: device.id ?? `name:${device.name}:${idx}`,
702
+ class: "spotify-device-row"
703
+ }, [
704
+ createElementVNode("span", _hoisted_63, toDisplayString(device.name), 1),
705
+ createElementVNode("span", _hoisted_64, toDisplayString(device.type), 1),
706
+ device.isActive ? (openBlock(), createElementBlock("span", _hoisted_65, toDisplayString(unref(t).deviceActive), 1)) : status.value?.isPremium === true ? (openBlock(), createElementBlock("button", {
707
+ key: 1,
708
+ type: "button",
709
+ class: "spotify-device-transfer",
710
+ disabled: isPlayerBusy.value || device.id === null,
711
+ "aria-disabled": device.id === null,
712
+ onClick: ($event) => device.id !== null && playerTransfer(device.id)
713
+ }, toDisplayString(unref(t).transferToDevice), 9, _hoisted_66)) : createCommentVNode("", true)
714
+ ]);
715
+ }), 128))])])) : createCommentVNode("", true)
716
+ ], 64)) : activeTab.value === "search" ? (openBlock(), createElementBlock(Fragment, { key: 9 }, [createElementVNode("form", {
717
+ class: "spotify-search-form",
718
+ onSubmit: withModifiers(runSearch, ["prevent"])
719
+ }, [withDirectives(createElementVNode("input", {
720
+ "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => searchQuery.value = $event),
721
+ placeholder: unref(t).searchPlaceholder,
722
+ "aria-label": unref(t).searchPlaceholder,
723
+ class: "spotify-input",
724
+ type: "search",
725
+ autocomplete: "off",
726
+ disabled: isSearching.value
727
+ }, null, 8, _hoisted_67), [[vModelText, searchQuery.value]]), createElementVNode("button", {
728
+ type: "submit",
729
+ class: "spotify-btn-primary",
730
+ disabled: isSearching.value || searchQuery.value.trim().length === 0
731
+ }, toDisplayString(isSearching.value ? unref(t).loading : unref(t).searchSubmit), 9, _hoisted_68)], 32), searchResult.value && searchResultIsEmpty.value ? (openBlock(), createElementBlock("div", _hoisted_69, toDisplayString(unref(t).searchEmpty), 1)) : searchResult.value ? (openBlock(), createElementBlock("div", _hoisted_70, [
732
+ searchResult.value.tracks && searchResult.value.tracks.length > 0 ? (openBlock(), createElementBlock("section", _hoisted_71, [createElementVNode("h3", null, toDisplayString(unref(t).searchTracks), 1), createElementVNode("ul", _hoisted_72, [(openBlock(true), createElementBlock(Fragment, null, renderList(searchResult.value.tracks, (track) => {
733
+ return openBlock(), createElementBlock("li", {
734
+ key: `t-${track.id}`,
735
+ class: "spotify-track-row"
736
+ }, [createElementVNode("button", {
737
+ type: "button",
738
+ class: "spotify-track-link",
739
+ onClick: ($event) => safeOpenUrl(track.url)
740
+ }, [track.imageUrl ? (openBlock(), createElementBlock("img", {
741
+ key: 0,
742
+ src: track.imageUrl,
743
+ alt: "",
744
+ class: "spotify-cover"
745
+ }, null, 8, _hoisted_74)) : createCommentVNode("", true), createElementVNode("span", _hoisted_75, [createElementVNode("span", _hoisted_76, toDisplayString(track.name), 1), createElementVNode("span", _hoisted_77, toDisplayString(unref(t).trackBy) + " " + toDisplayString(track.artists.join(", ")), 1)])], 8, _hoisted_73)]);
746
+ }), 128))])])) : createCommentVNode("", true),
747
+ searchResult.value.artists && searchResult.value.artists.length > 0 ? (openBlock(), createElementBlock("section", _hoisted_78, [createElementVNode("h3", null, toDisplayString(unref(t).searchArtists), 1), createElementVNode("ul", _hoisted_79, [(openBlock(true), createElementBlock(Fragment, null, renderList(searchResult.value.artists, (artist) => {
748
+ return openBlock(), createElementBlock("li", {
749
+ key: `a-${artist.id}`,
750
+ class: "spotify-track-row"
751
+ }, [createElementVNode("button", {
752
+ type: "button",
753
+ class: "spotify-track-link",
754
+ onClick: ($event) => safeOpenUrl(artist.url)
755
+ }, [artist.imageUrl ? (openBlock(), createElementBlock("img", {
756
+ key: 0,
757
+ src: artist.imageUrl,
758
+ alt: "",
759
+ class: "spotify-cover"
760
+ }, null, 8, _hoisted_81)) : createCommentVNode("", true), createElementVNode("span", _hoisted_82, [createElementVNode("span", _hoisted_83, toDisplayString(artist.name), 1), artist.genres.length > 0 ? (openBlock(), createElementBlock("span", _hoisted_84, toDisplayString(artist.genres.slice(0, 3).join(", ")), 1)) : createCommentVNode("", true)])], 8, _hoisted_80)]);
761
+ }), 128))])])) : createCommentVNode("", true),
762
+ searchResult.value.albums && searchResult.value.albums.length > 0 ? (openBlock(), createElementBlock("section", _hoisted_85, [createElementVNode("h3", null, toDisplayString(unref(t).searchAlbums), 1), createElementVNode("ul", _hoisted_86, [(openBlock(true), createElementBlock(Fragment, null, renderList(searchResult.value.albums, (album) => {
763
+ return openBlock(), createElementBlock("li", {
764
+ key: `al-${album.id}`,
765
+ class: "spotify-track-row"
766
+ }, [createElementVNode("button", {
767
+ type: "button",
768
+ class: "spotify-track-link",
769
+ onClick: ($event) => safeOpenUrl(album.url)
770
+ }, [album.imageUrl ? (openBlock(), createElementBlock("img", {
771
+ key: 0,
772
+ src: album.imageUrl,
773
+ alt: "",
774
+ class: "spotify-cover"
775
+ }, null, 8, _hoisted_88)) : createCommentVNode("", true), createElementVNode("span", _hoisted_89, [createElementVNode("span", _hoisted_90, toDisplayString(album.name), 1), createElementVNode("span", _hoisted_91, [createTextVNode(toDisplayString(album.artists.join(", ")), 1), album.releaseDate ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [createTextVNode(" · " + toDisplayString(album.releaseDate.slice(0, 4)), 1)], 64)) : createCommentVNode("", true)])])], 8, _hoisted_87)]);
776
+ }), 128))])])) : createCommentVNode("", true),
777
+ searchResult.value.playlists && searchResult.value.playlists.length > 0 ? (openBlock(), createElementBlock("section", _hoisted_92, [createElementVNode("h3", null, toDisplayString(unref(t).searchPlaylists), 1), createElementVNode("ul", _hoisted_93, [(openBlock(true), createElementBlock(Fragment, null, renderList(searchResult.value.playlists, (playlist) => {
778
+ return openBlock(), createElementBlock("li", {
779
+ key: `p-${playlist.id}`,
780
+ class: "spotify-playlist-row"
781
+ }, [createElementVNode("button", {
782
+ type: "button",
783
+ class: "spotify-track-link",
784
+ onClick: ($event) => safeOpenUrl(playlist.url)
785
+ }, [playlist.imageUrl ? (openBlock(), createElementBlock("img", {
786
+ key: 0,
787
+ src: playlist.imageUrl,
788
+ alt: "",
789
+ class: "spotify-cover"
790
+ }, null, 8, _hoisted_95)) : createCommentVNode("", true), createElementVNode("span", _hoisted_96, [createElementVNode("span", _hoisted_97, toDisplayString(playlist.name), 1), createElementVNode("span", _hoisted_98, toDisplayString(playlist.trackCount) + " " + toDisplayString(unref(t).tracksCount), 1)])], 8, _hoisted_94)]);
791
+ }), 128))])])) : createCommentVNode("", true)
792
+ ])) : (openBlock(), createElementBlock("p", _hoisted_99, toDisplayString(unref(t).searchHint), 1))], 64)) : createCommentVNode("", true)])])) : createCommentVNode("", true)]);
793
+ };
794
+ }
795
+ });
796
+ //#endregion
797
+ //#region \0plugin-vue:export-helper
798
+ var _plugin_vue_export_helper_default = (sfc, props) => {
799
+ const target = sfc.__vccOpts || sfc;
800
+ for (const [key, val] of props) target[key] = val;
801
+ return target;
802
+ };
803
+ //#endregion
804
+ //#region src/View.vue
805
+ var View_default = /* @__PURE__ */ _plugin_vue_export_helper_default(View_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-57a122f2"]]);
806
+ //#endregion
807
+ //#region src/Preview.vue?vue&type=script&setup=true&lang.ts
808
+ var _hoisted_1 = { class: "spotify-preview" };
809
+ var _hoisted_2 = { class: "spotify-preview-label" };
810
+ var _hoisted_3 = { class: "spotify-preview-summary" };
811
+ //#endregion
812
+ //#region src/vue.ts
813
+ var plugin = {
814
+ toolDefinition: TOOL_DEFINITION,
815
+ viewComponent: View_default,
816
+ previewComponent: /* @__PURE__ */ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
817
+ __name: "Preview",
818
+ props: { selectedResult: {} },
819
+ setup(__props) {
820
+ const props = __props;
821
+ const t = useT();
822
+ const summary = computed(() => {
823
+ const result = props.selectedResult;
824
+ if (result.ok === false) return result.message ?? t.value.notConnected;
825
+ const data = result.data;
826
+ if (Array.isArray(data)) return summariseArray(data);
827
+ if (data === null) return t.value.emptyNowPlaying;
828
+ if (data && typeof data === "object" && "connected" in data) return data.connected ? t.value.connected : data.clientIdConfigured ? t.value.notConnected : t.value.notConfigured;
829
+ if (data && typeof data === "object" && isSearchResult(data)) return summariseSearchResult(data);
830
+ if (data && typeof data === "object" && "name" in data) return data.name;
831
+ return t.value.previewSummary;
832
+ });
833
+ function isSearchResult(value) {
834
+ return "tracks" in value || "artists" in value || "albums" in value || "playlists" in value;
835
+ }
836
+ function summariseSearchResult(result) {
837
+ const parts = [];
838
+ if (result.tracks?.length) parts.push(`${result.tracks.length} ${t.value.searchTracks}`);
839
+ if (result.artists?.length) parts.push(`${result.artists.length} ${t.value.searchArtists}`);
840
+ if (result.albums?.length) parts.push(`${result.albums.length} ${t.value.searchAlbums}`);
841
+ if (result.playlists?.length) parts.push(`${result.playlists.length} ${t.value.searchPlaylists}`);
842
+ return parts.length > 0 ? parts.join(" · ") : t.value.searchEmpty;
843
+ }
844
+ function summariseArray(data) {
845
+ if (data.length === 0) return t.value.empty;
846
+ const head = data[0];
847
+ if ("trackCount" in head) return `${data.length} ${t.value.tabPlaylists}`;
848
+ if ("playedAt" in head) return `${data.length} ${t.value.tabRecent}`;
849
+ return `${data.length} ${t.value.tracksCount}`;
850
+ }
851
+ return (_ctx, _cache) => {
852
+ return openBlock(), createElementBlock("div", _hoisted_1, [
853
+ _cache[0] || (_cache[0] = createElementVNode("span", {
854
+ class: "spotify-preview-icon",
855
+ "aria-hidden": "true"
856
+ }, "♪", -1)),
857
+ createElementVNode("span", _hoisted_2, toDisplayString(unref(t).previewSummary), 1),
858
+ createElementVNode("span", _hoisted_3, toDisplayString(summary.value), 1)
859
+ ]);
860
+ };
861
+ }
862
+ }), [["__scopeId", "data-v-53e3c895"]])
863
+ };
864
+ //#endregion
865
+ export { plugin };
866
+
867
+ //# sourceMappingURL=vue.js.map