@naxodev/opencode-music-player 0.1.4 → 0.1.5
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/index.tsx +46 -39
- package/package.json +1 -1
- package/ui.tsx +8 -26
package/index.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { isMac, mergeArtworkCompletion, type SessionMedia } from "./types.ts"
|
|
|
5
5
|
import { CompactPlayer, SidebarPlayer, type UiState } from "./ui.tsx"
|
|
6
6
|
|
|
7
7
|
type Context = Plugin.Context
|
|
8
|
-
type SessionStore = UiState
|
|
8
|
+
type SessionStore = UiState & { loadingOwners?: string[] }
|
|
9
9
|
type SeekIntent = {
|
|
10
10
|
positionMs: number
|
|
11
11
|
resolves: Array<() => void>
|
|
@@ -13,7 +13,6 @@ type SeekIntent = {
|
|
|
13
13
|
|
|
14
14
|
export type Controller = {
|
|
15
15
|
session: SessionStore
|
|
16
|
-
subscribe: (listener: (session: SessionStore) => void) => () => void
|
|
17
16
|
openApp: () => Promise<void>
|
|
18
17
|
refreshAll: () => Promise<void>
|
|
19
18
|
playPause: () => Promise<void>
|
|
@@ -83,16 +82,18 @@ export function createController(
|
|
|
83
82
|
context: Context,
|
|
84
83
|
dependencies: ControllerDependencies = controllerDependencies,
|
|
85
84
|
): Controller {
|
|
86
|
-
const session
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
const [session, setSession] = context.storage.memory<SessionStore>(
|
|
86
|
+
"music-player.session.v6",
|
|
87
|
+
{
|
|
88
|
+
initial: {
|
|
89
|
+
loading: false,
|
|
90
|
+
loadingOwners: [],
|
|
91
|
+
error: null,
|
|
92
|
+
player: null,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
const loadingOwner = crypto.randomUUID()
|
|
96
97
|
const media = dependencies.createSessionMedia()
|
|
97
98
|
let disposed = false
|
|
98
99
|
let lifecycleGeneration = 0
|
|
@@ -134,12 +135,18 @@ export function createController(
|
|
|
134
135
|
transportError = message
|
|
135
136
|
publishError()
|
|
136
137
|
}
|
|
137
|
-
const
|
|
138
|
-
if (!isActive()) return
|
|
138
|
+
const setLoadingOwner = (active: boolean) => {
|
|
139
139
|
setSession((draft) => {
|
|
140
|
-
|
|
140
|
+
const owners = new Set(draft.loadingOwners ?? [])
|
|
141
|
+
if (active) owners.add(loadingOwner)
|
|
142
|
+
else owners.delete(loadingOwner)
|
|
143
|
+
draft.loadingOwners = [...owners]
|
|
144
|
+
draft.loading = owners.size > 0
|
|
141
145
|
})
|
|
142
146
|
}
|
|
147
|
+
const updateLoading = () => {
|
|
148
|
+
if (isActive()) setLoadingOwner(pending.size > 0)
|
|
149
|
+
}
|
|
143
150
|
const settle = (resolve: () => void) => {
|
|
144
151
|
if (!pending.delete(resolve)) return
|
|
145
152
|
resolve()
|
|
@@ -289,11 +296,6 @@ export function createController(
|
|
|
289
296
|
|
|
290
297
|
return {
|
|
291
298
|
session,
|
|
292
|
-
subscribe(listener) {
|
|
293
|
-
sessionListeners.add(listener)
|
|
294
|
-
listener(session)
|
|
295
|
-
return () => sessionListeners.delete(listener)
|
|
296
|
-
},
|
|
297
299
|
refreshAll,
|
|
298
300
|
async openApp() {
|
|
299
301
|
if (!isActive()) return
|
|
@@ -326,12 +328,7 @@ export function createController(
|
|
|
326
328
|
activeSeek = null
|
|
327
329
|
for (const resolve of [...pending]) settle(resolve)
|
|
328
330
|
void media.dispose().catch(() => {})
|
|
329
|
-
|
|
330
|
-
setSession((draft) => {
|
|
331
|
-
draft.loading = false
|
|
332
|
-
})
|
|
333
|
-
}
|
|
334
|
-
sessionListeners.clear()
|
|
331
|
+
setLoadingOwner(false)
|
|
335
332
|
},
|
|
336
333
|
}
|
|
337
334
|
}
|
|
@@ -380,7 +377,6 @@ function AppHost(props: { context: Context; ctrl: Controller }) {
|
|
|
380
377
|
<CompactPlayer
|
|
381
378
|
context={context}
|
|
382
379
|
state={ctrl.session}
|
|
383
|
-
subscribe={ctrl.subscribe}
|
|
384
380
|
onPlayPause={() => void ctrl.playPause()}
|
|
385
381
|
onSeek={(positionMs) => void ctrl.seek(positionMs)}
|
|
386
382
|
/>
|
|
@@ -413,23 +409,34 @@ export function createMusicPlayerPlugin(options?: {
|
|
|
413
409
|
}
|
|
414
410
|
: controllerDependencies,
|
|
415
411
|
)
|
|
412
|
+
// Slot renderers must observe host-owned state before package components run.
|
|
413
|
+
const observeSession = () => {
|
|
414
|
+
void ctrl.session.loading
|
|
415
|
+
void ctrl.session.error
|
|
416
|
+
void ctrl.session.player
|
|
417
|
+
}
|
|
416
418
|
const unsubApp = context.ui.slot({
|
|
417
419
|
append: "session.composer.top",
|
|
418
|
-
render: () =>
|
|
420
|
+
render: () => {
|
|
421
|
+
observeSession()
|
|
422
|
+
return <AppHost context={context} ctrl={ctrl} />
|
|
423
|
+
},
|
|
419
424
|
})
|
|
420
425
|
const unsubSidebar = context.ui.slot({
|
|
421
426
|
append: "sidebar.content",
|
|
422
|
-
render: () =>
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
427
|
+
render: () => {
|
|
428
|
+
observeSession()
|
|
429
|
+
return (
|
|
430
|
+
<SidebarPlayer
|
|
431
|
+
context={context}
|
|
432
|
+
state={ctrl.session}
|
|
433
|
+
onPlayPause={() => void ctrl.playPause()}
|
|
434
|
+
onNext={() => void ctrl.next()}
|
|
435
|
+
onPrev={() => void ctrl.prev()}
|
|
436
|
+
onSeek={(positionMs) => void ctrl.seek(positionMs)}
|
|
437
|
+
/>
|
|
438
|
+
)
|
|
439
|
+
},
|
|
433
440
|
})
|
|
434
441
|
return () => {
|
|
435
442
|
unsubApp()
|
package/package.json
CHANGED
package/ui.tsx
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
onCleanup,
|
|
8
8
|
onMount,
|
|
9
9
|
} from "solid-js"
|
|
10
|
-
import { createStore, reconcile } from "solid-js/store"
|
|
11
10
|
import type { Plugin } from "@opencode-ai/plugin/tui"
|
|
12
11
|
import {
|
|
13
12
|
MouseButton,
|
|
@@ -25,19 +24,6 @@ export type UiState = {
|
|
|
25
24
|
player: import("./types.ts").PlayerState | null
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
type UiStateSubscribe = (listener: (state: UiState) => void) => () => void
|
|
29
|
-
|
|
30
|
-
function reactiveUiState(
|
|
31
|
-
state: UiState,
|
|
32
|
-
subscribe: UiStateSubscribe | undefined,
|
|
33
|
-
): UiState {
|
|
34
|
-
if (!subscribe) return state
|
|
35
|
-
const [local, setLocal] = createStore<UiState>({ ...state })
|
|
36
|
-
const unsubscribe = subscribe((next) => setLocal(reconcile({ ...next })))
|
|
37
|
-
onCleanup(unsubscribe)
|
|
38
|
-
return local
|
|
39
|
-
}
|
|
40
|
-
|
|
41
27
|
type Theme = Context["theme"]
|
|
42
28
|
type Context = Plugin.Context
|
|
43
29
|
|
|
@@ -224,16 +210,14 @@ export function compactPresentation(
|
|
|
224
210
|
export function CompactPlayer(props: {
|
|
225
211
|
context: Context
|
|
226
212
|
state: UiState
|
|
227
|
-
subscribe?: UiStateSubscribe
|
|
228
213
|
onPlayPause: () => void
|
|
229
214
|
onSeek: (positionMs: number) => void
|
|
230
215
|
}) {
|
|
231
216
|
const theme = () => props.context.theme
|
|
232
|
-
const state = reactiveUiState(props.state, props.subscribe)
|
|
233
217
|
let row: BoxRenderable | undefined
|
|
234
218
|
let content: TextRenderable | undefined
|
|
235
219
|
let allocatedWidth = Number.MAX_SAFE_INTEGER
|
|
236
|
-
const track = createMemo(() => state.player?.track)
|
|
220
|
+
const track = createMemo(() => props.state.player?.track)
|
|
237
221
|
const renderLine = () => {
|
|
238
222
|
const current = track()
|
|
239
223
|
if (!current) return ""
|
|
@@ -241,7 +225,7 @@ export function CompactPlayer(props: {
|
|
|
241
225
|
allocatedWidth,
|
|
242
226
|
current.name,
|
|
243
227
|
current.artists,
|
|
244
|
-
!!state.player?.is_playing,
|
|
228
|
+
!!props.state.player?.is_playing,
|
|
245
229
|
)
|
|
246
230
|
const padding = " ".repeat(display.padding)
|
|
247
231
|
return `${padding}${display.marker}${
|
|
@@ -279,7 +263,7 @@ export function CompactPlayer(props: {
|
|
|
279
263
|
overflow="hidden"
|
|
280
264
|
onMouseDown={(event) => {
|
|
281
265
|
if (event.button !== MouseButton.LEFT) return
|
|
282
|
-
const current = state.player?.track
|
|
266
|
+
const current = props.state.player?.track
|
|
283
267
|
if (!current || !row) return
|
|
284
268
|
|
|
285
269
|
const seekWidth = compactSeekRegionWidth(row.width)
|
|
@@ -288,7 +272,7 @@ export function CompactPlayer(props: {
|
|
|
288
272
|
row.width,
|
|
289
273
|
current.name,
|
|
290
274
|
current.artists,
|
|
291
|
-
!!state.player?.is_playing,
|
|
275
|
+
!!props.state.player?.is_playing,
|
|
292
276
|
).padding
|
|
293
277
|
if (cell === markerCell) {
|
|
294
278
|
event.preventDefault()
|
|
@@ -473,15 +457,13 @@ function IconBtn(props: {
|
|
|
473
457
|
export function SidebarPlayer(props: {
|
|
474
458
|
context: Context
|
|
475
459
|
state: UiState
|
|
476
|
-
subscribe?: UiStateSubscribe
|
|
477
460
|
onPlayPause: () => void
|
|
478
461
|
onNext: () => void
|
|
479
462
|
onPrev: () => void
|
|
480
463
|
onSeek: (positionMs: number) => void
|
|
481
464
|
}) {
|
|
482
465
|
const theme = () => props.context.theme
|
|
483
|
-
const
|
|
484
|
-
const player = createMemo(() => state.player)
|
|
466
|
+
const player = createMemo(() => props.state.player)
|
|
485
467
|
const track = createMemo(() => player()?.track)
|
|
486
468
|
const playing = createMemo(() => !!player()?.is_playing)
|
|
487
469
|
|
|
@@ -489,7 +471,7 @@ export function SidebarPlayer(props: {
|
|
|
489
471
|
onMount(() => {
|
|
490
472
|
let id: ReturnType<typeof setInterval> | null = null
|
|
491
473
|
const sync = () => {
|
|
492
|
-
const on = !!state.player?.is_playing
|
|
474
|
+
const on = !!props.state.player?.is_playing
|
|
493
475
|
if (on && !id) id = setInterval(() => setTick((t) => t + 1), 1000)
|
|
494
476
|
else if (!on && id) {
|
|
495
477
|
clearInterval(id)
|
|
@@ -526,7 +508,7 @@ export function SidebarPlayer(props: {
|
|
|
526
508
|
gap={1}
|
|
527
509
|
flexShrink={0}
|
|
528
510
|
>
|
|
529
|
-
<Show when={state.error}>
|
|
511
|
+
<Show when={props.state.error}>
|
|
530
512
|
{(err) => <text fg={theme().text.feedback.error.default}>{err()}</text>}
|
|
531
513
|
</Show>
|
|
532
514
|
|
|
@@ -562,7 +544,7 @@ export function SidebarPlayer(props: {
|
|
|
562
544
|
when={track()}
|
|
563
545
|
fallback={
|
|
564
546
|
<text fg={theme().text.subdued}>
|
|
565
|
-
{state.loading ? "Syncing…" : "Nothing playing"}
|
|
547
|
+
{props.state.loading ? "Syncing…" : "Nothing playing"}
|
|
566
548
|
</text>
|
|
567
549
|
}
|
|
568
550
|
>
|