@naxodev/opencode-music-player 0.1.3 → 0.1.4
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 +16 -6
- package/package.json +1 -1
- package/ui.tsx +26 -8
package/index.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import { Plugin } from "@opencode-ai/plugin/tui"
|
|
3
|
-
import { createStore, produce } from "solid-js/store"
|
|
4
3
|
import { createSessionSystemMedia, openNowPlayingApp } from "./system-media.ts"
|
|
5
4
|
import { isMac, mergeArtworkCompletion, type SessionMedia } from "./types.ts"
|
|
6
5
|
import { CompactPlayer, SidebarPlayer, type UiState } from "./ui.tsx"
|
|
@@ -14,6 +13,7 @@ type SeekIntent = {
|
|
|
14
13
|
|
|
15
14
|
export type Controller = {
|
|
16
15
|
session: SessionStore
|
|
16
|
+
subscribe: (listener: (session: SessionStore) => void) => () => void
|
|
17
17
|
openApp: () => Promise<void>
|
|
18
18
|
refreshAll: () => Promise<void>
|
|
19
19
|
playPause: () => Promise<void>
|
|
@@ -83,14 +83,16 @@ export function createController(
|
|
|
83
83
|
context: Context,
|
|
84
84
|
dependencies: ControllerDependencies = controllerDependencies,
|
|
85
85
|
): Controller {
|
|
86
|
-
|
|
87
|
-
const [session, setSessionStore] = createStore<SessionStore>({
|
|
86
|
+
const session: SessionStore = {
|
|
88
87
|
loading: false,
|
|
89
88
|
error: null,
|
|
90
89
|
player: null,
|
|
91
|
-
}
|
|
92
|
-
const
|
|
93
|
-
|
|
90
|
+
}
|
|
91
|
+
const sessionListeners = new Set<(session: SessionStore) => void>()
|
|
92
|
+
const setSession = (update: (draft: SessionStore) => void) => {
|
|
93
|
+
update(session)
|
|
94
|
+
for (const listener of sessionListeners) listener(session)
|
|
95
|
+
}
|
|
94
96
|
const media = dependencies.createSessionMedia()
|
|
95
97
|
let disposed = false
|
|
96
98
|
let lifecycleGeneration = 0
|
|
@@ -287,6 +289,11 @@ export function createController(
|
|
|
287
289
|
|
|
288
290
|
return {
|
|
289
291
|
session,
|
|
292
|
+
subscribe(listener) {
|
|
293
|
+
sessionListeners.add(listener)
|
|
294
|
+
listener(session)
|
|
295
|
+
return () => sessionListeners.delete(listener)
|
|
296
|
+
},
|
|
290
297
|
refreshAll,
|
|
291
298
|
async openApp() {
|
|
292
299
|
if (!isActive()) return
|
|
@@ -324,6 +331,7 @@ export function createController(
|
|
|
324
331
|
draft.loading = false
|
|
325
332
|
})
|
|
326
333
|
}
|
|
334
|
+
sessionListeners.clear()
|
|
327
335
|
},
|
|
328
336
|
}
|
|
329
337
|
}
|
|
@@ -372,6 +380,7 @@ function AppHost(props: { context: Context; ctrl: Controller }) {
|
|
|
372
380
|
<CompactPlayer
|
|
373
381
|
context={context}
|
|
374
382
|
state={ctrl.session}
|
|
383
|
+
subscribe={ctrl.subscribe}
|
|
375
384
|
onPlayPause={() => void ctrl.playPause()}
|
|
376
385
|
onSeek={(positionMs) => void ctrl.seek(positionMs)}
|
|
377
386
|
/>
|
|
@@ -414,6 +423,7 @@ export function createMusicPlayerPlugin(options?: {
|
|
|
414
423
|
<SidebarPlayer
|
|
415
424
|
context={context}
|
|
416
425
|
state={ctrl.session}
|
|
426
|
+
subscribe={ctrl.subscribe}
|
|
417
427
|
onPlayPause={() => void ctrl.playPause()}
|
|
418
428
|
onNext={() => void ctrl.next()}
|
|
419
429
|
onPrev={() => void ctrl.prev()}
|
package/package.json
CHANGED
package/ui.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
onCleanup,
|
|
8
8
|
onMount,
|
|
9
9
|
} from "solid-js"
|
|
10
|
+
import { createStore, reconcile } from "solid-js/store"
|
|
10
11
|
import type { Plugin } from "@opencode-ai/plugin/tui"
|
|
11
12
|
import {
|
|
12
13
|
MouseButton,
|
|
@@ -24,6 +25,19 @@ export type UiState = {
|
|
|
24
25
|
player: import("./types.ts").PlayerState | null
|
|
25
26
|
}
|
|
26
27
|
|
|
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
|
+
|
|
27
41
|
type Theme = Context["theme"]
|
|
28
42
|
type Context = Plugin.Context
|
|
29
43
|
|
|
@@ -210,14 +224,16 @@ export function compactPresentation(
|
|
|
210
224
|
export function CompactPlayer(props: {
|
|
211
225
|
context: Context
|
|
212
226
|
state: UiState
|
|
227
|
+
subscribe?: UiStateSubscribe
|
|
213
228
|
onPlayPause: () => void
|
|
214
229
|
onSeek: (positionMs: number) => void
|
|
215
230
|
}) {
|
|
216
231
|
const theme = () => props.context.theme
|
|
232
|
+
const state = reactiveUiState(props.state, props.subscribe)
|
|
217
233
|
let row: BoxRenderable | undefined
|
|
218
234
|
let content: TextRenderable | undefined
|
|
219
235
|
let allocatedWidth = Number.MAX_SAFE_INTEGER
|
|
220
|
-
const track = createMemo(() =>
|
|
236
|
+
const track = createMemo(() => state.player?.track)
|
|
221
237
|
const renderLine = () => {
|
|
222
238
|
const current = track()
|
|
223
239
|
if (!current) return ""
|
|
@@ -225,7 +241,7 @@ export function CompactPlayer(props: {
|
|
|
225
241
|
allocatedWidth,
|
|
226
242
|
current.name,
|
|
227
243
|
current.artists,
|
|
228
|
-
!!
|
|
244
|
+
!!state.player?.is_playing,
|
|
229
245
|
)
|
|
230
246
|
const padding = " ".repeat(display.padding)
|
|
231
247
|
return `${padding}${display.marker}${
|
|
@@ -263,7 +279,7 @@ export function CompactPlayer(props: {
|
|
|
263
279
|
overflow="hidden"
|
|
264
280
|
onMouseDown={(event) => {
|
|
265
281
|
if (event.button !== MouseButton.LEFT) return
|
|
266
|
-
const current =
|
|
282
|
+
const current = state.player?.track
|
|
267
283
|
if (!current || !row) return
|
|
268
284
|
|
|
269
285
|
const seekWidth = compactSeekRegionWidth(row.width)
|
|
@@ -272,7 +288,7 @@ export function CompactPlayer(props: {
|
|
|
272
288
|
row.width,
|
|
273
289
|
current.name,
|
|
274
290
|
current.artists,
|
|
275
|
-
!!
|
|
291
|
+
!!state.player?.is_playing,
|
|
276
292
|
).padding
|
|
277
293
|
if (cell === markerCell) {
|
|
278
294
|
event.preventDefault()
|
|
@@ -457,13 +473,15 @@ function IconBtn(props: {
|
|
|
457
473
|
export function SidebarPlayer(props: {
|
|
458
474
|
context: Context
|
|
459
475
|
state: UiState
|
|
476
|
+
subscribe?: UiStateSubscribe
|
|
460
477
|
onPlayPause: () => void
|
|
461
478
|
onNext: () => void
|
|
462
479
|
onPrev: () => void
|
|
463
480
|
onSeek: (positionMs: number) => void
|
|
464
481
|
}) {
|
|
465
482
|
const theme = () => props.context.theme
|
|
466
|
-
const
|
|
483
|
+
const state = reactiveUiState(props.state, props.subscribe)
|
|
484
|
+
const player = createMemo(() => state.player)
|
|
467
485
|
const track = createMemo(() => player()?.track)
|
|
468
486
|
const playing = createMemo(() => !!player()?.is_playing)
|
|
469
487
|
|
|
@@ -471,7 +489,7 @@ export function SidebarPlayer(props: {
|
|
|
471
489
|
onMount(() => {
|
|
472
490
|
let id: ReturnType<typeof setInterval> | null = null
|
|
473
491
|
const sync = () => {
|
|
474
|
-
const on = !!
|
|
492
|
+
const on = !!state.player?.is_playing
|
|
475
493
|
if (on && !id) id = setInterval(() => setTick((t) => t + 1), 1000)
|
|
476
494
|
else if (!on && id) {
|
|
477
495
|
clearInterval(id)
|
|
@@ -508,7 +526,7 @@ export function SidebarPlayer(props: {
|
|
|
508
526
|
gap={1}
|
|
509
527
|
flexShrink={0}
|
|
510
528
|
>
|
|
511
|
-
<Show when={
|
|
529
|
+
<Show when={state.error}>
|
|
512
530
|
{(err) => <text fg={theme().text.feedback.error.default}>{err()}</text>}
|
|
513
531
|
</Show>
|
|
514
532
|
|
|
@@ -544,7 +562,7 @@ export function SidebarPlayer(props: {
|
|
|
544
562
|
when={track()}
|
|
545
563
|
fallback={
|
|
546
564
|
<text fg={theme().text.subdued}>
|
|
547
|
-
{
|
|
565
|
+
{state.loading ? "Syncing…" : "Nothing playing"}
|
|
548
566
|
</text>
|
|
549
567
|
}
|
|
550
568
|
>
|