@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.
Files changed (3) hide show
  1. package/index.tsx +16 -6
  2. package/package.json +1 -1
  3. 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
- // Host storage may use another Solid runtime when this plugin comes from npm.
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 setSession = (update: (draft: SessionStore) => void) =>
93
- setSessionStore(produce(update))
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naxodev/opencode-music-player",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "macOS system Now Playing controls for the OpenCode 2 TUI",
5
5
  "license": "MIT",
6
6
  "author": {
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(() => props.state.player?.track)
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
- !!props.state.player?.is_playing,
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 = props.state.player?.track
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
- !!props.state.player?.is_playing,
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 player = createMemo(() => props.state.player)
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 = !!props.state.player?.is_playing
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={props.state.error}>
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
- {props.state.loading ? "Syncing…" : "Nothing playing"}
565
+ {state.loading ? "Syncing…" : "Nothing playing"}
548
566
  </text>
549
567
  }
550
568
  >