@naxodev/opencode-music-player 0.1.5 → 0.2.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/index.tsx +28 -3
- package/package.json +1 -1
- package/types.ts +32 -0
- package/ui.tsx +6 -26
package/index.tsx
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import { Plugin } from "@opencode-ai/plugin/tui"
|
|
3
3
|
import { createSessionSystemMedia, openNowPlayingApp } from "./system-media.ts"
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
isMac,
|
|
6
|
+
mergeArtworkCompletion,
|
|
7
|
+
mergePlayerSnapshot,
|
|
8
|
+
type SessionMedia,
|
|
9
|
+
} from "./types.ts"
|
|
5
10
|
import { CompactPlayer, SidebarPlayer, type UiState } from "./ui.tsx"
|
|
6
11
|
|
|
7
12
|
type Context = Plugin.Context
|
|
@@ -90,6 +95,7 @@ export function createController(
|
|
|
90
95
|
loadingOwners: [],
|
|
91
96
|
error: null,
|
|
92
97
|
player: null,
|
|
98
|
+
clock_ms: Date.now(),
|
|
93
99
|
},
|
|
94
100
|
},
|
|
95
101
|
)
|
|
@@ -112,8 +118,20 @@ export function createController(
|
|
|
112
118
|
let activeSeek: SeekIntent | null = null
|
|
113
119
|
let latestSeek: SeekIntent | null = null
|
|
114
120
|
const pending = new Set<() => void>()
|
|
115
|
-
|
|
116
121
|
const isActive = () => !disposed
|
|
122
|
+
let clockTimer: ReturnType<typeof setInterval> | null = null
|
|
123
|
+
const syncClock = () => {
|
|
124
|
+
if (clockTimer) clearInterval(clockTimer)
|
|
125
|
+
clockTimer = null
|
|
126
|
+
if (!isActive() || !session.player?.is_playing) return
|
|
127
|
+
clockTimer = setInterval(() => {
|
|
128
|
+
if (!isActive()) return
|
|
129
|
+
setSession((draft) => {
|
|
130
|
+
draft.clock_ms = Date.now()
|
|
131
|
+
})
|
|
132
|
+
}, 1_000)
|
|
133
|
+
}
|
|
134
|
+
|
|
117
135
|
const publishError = () => {
|
|
118
136
|
if (!isActive()) return
|
|
119
137
|
const message =
|
|
@@ -222,7 +240,9 @@ export function createController(
|
|
|
222
240
|
return
|
|
223
241
|
setSession((draft) => {
|
|
224
242
|
draft.player = player
|
|
243
|
+
draft.clock_ms = Date.now()
|
|
225
244
|
})
|
|
245
|
+
syncClock()
|
|
226
246
|
} catch (error) {
|
|
227
247
|
if (isActive() && generation === lifecycleGeneration)
|
|
228
248
|
setTransportError(errMsg(error))
|
|
@@ -239,6 +259,7 @@ export function createController(
|
|
|
239
259
|
setSession((draft) => {
|
|
240
260
|
draft.player = optimisticPlayerState(draft.player, nextPlaying)
|
|
241
261
|
})
|
|
262
|
+
syncClock()
|
|
242
263
|
},
|
|
243
264
|
).finally(() => {
|
|
244
265
|
playbackOperations--
|
|
@@ -270,8 +291,10 @@ export function createController(
|
|
|
270
291
|
receivedSnapshot = true
|
|
271
292
|
snapshotEpoch++
|
|
272
293
|
setSession((draft) => {
|
|
273
|
-
draft.player = event.state
|
|
294
|
+
draft.player = mergePlayerSnapshot(draft.player, event.state)
|
|
295
|
+
draft.clock_ms = Date.now()
|
|
274
296
|
})
|
|
297
|
+
syncClock()
|
|
275
298
|
return
|
|
276
299
|
}
|
|
277
300
|
if (event?.type === "lifecycle") {
|
|
@@ -327,6 +350,7 @@ export function createController(
|
|
|
327
350
|
activeSeek?.resolves.splice(0).forEach((resolve) => resolve())
|
|
328
351
|
activeSeek = null
|
|
329
352
|
for (const resolve of [...pending]) settle(resolve)
|
|
353
|
+
if (clockTimer) clearInterval(clockTimer)
|
|
330
354
|
void media.dispose().catch(() => {})
|
|
331
355
|
setLoadingOwner(false)
|
|
332
356
|
},
|
|
@@ -414,6 +438,7 @@ export function createMusicPlayerPlugin(options?: {
|
|
|
414
438
|
void ctrl.session.loading
|
|
415
439
|
void ctrl.session.error
|
|
416
440
|
void ctrl.session.player
|
|
441
|
+
void ctrl.session.clock_ms
|
|
417
442
|
}
|
|
418
443
|
const unsubApp = context.ui.slot({
|
|
419
444
|
append: "session.composer.top",
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -113,3 +113,35 @@ export function mergeArtworkCompletion(
|
|
|
113
113
|
},
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
+
|
|
117
|
+
/** Keep host-local artwork presentation across playback-only snapshots. */
|
|
118
|
+
export function mergePlayerSnapshot(
|
|
119
|
+
player: PlayerState | null,
|
|
120
|
+
snapshot: PlayerState,
|
|
121
|
+
): PlayerState {
|
|
122
|
+
const current = player?.track
|
|
123
|
+
const next = snapshot.track
|
|
124
|
+
if (
|
|
125
|
+
!current ||
|
|
126
|
+
!next ||
|
|
127
|
+
current.name !== next.name ||
|
|
128
|
+
current.artists !== next.artists ||
|
|
129
|
+
current.album !== next.album ||
|
|
130
|
+
(current.duration_ms > 0 &&
|
|
131
|
+
next.duration_ms > 0 &&
|
|
132
|
+
current.duration_ms !== next.duration_ms) ||
|
|
133
|
+
(current.artwork === null && current.artwork_loading !== false)
|
|
134
|
+
) {
|
|
135
|
+
return snapshot
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
...snapshot,
|
|
139
|
+
track: {
|
|
140
|
+
...next,
|
|
141
|
+
artwork: next.artwork ?? current.artwork,
|
|
142
|
+
artwork_loading: false,
|
|
143
|
+
duration_ms:
|
|
144
|
+
next.duration_ms > 0 ? next.duration_ms : current.duration_ms,
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
package/ui.tsx
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
createMemo,
|
|
6
6
|
createSignal,
|
|
7
7
|
onCleanup,
|
|
8
|
-
onMount,
|
|
9
8
|
} from "solid-js"
|
|
10
9
|
import type { Plugin } from "@opencode-ai/plugin/tui"
|
|
11
10
|
import {
|
|
@@ -22,6 +21,7 @@ export type UiState = {
|
|
|
22
21
|
loading: boolean
|
|
23
22
|
error: string | null
|
|
24
23
|
player: import("./types.ts").PlayerState | null
|
|
24
|
+
clock_ms?: number
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
type Theme = Context["theme"]
|
|
@@ -311,12 +311,12 @@ export function CompactPlayer(props: {
|
|
|
311
311
|
)
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
function liveProgress(player: UiState["player"]): number {
|
|
314
|
+
function liveProgress(player: UiState["player"], now = Date.now()): number {
|
|
315
315
|
if (!player?.track) return 0
|
|
316
316
|
if (!player.is_playing) return player.progress_ms
|
|
317
317
|
return Math.min(
|
|
318
318
|
player.track.duration_ms,
|
|
319
|
-
player.progress_ms + (
|
|
319
|
+
player.progress_ms + (Math.max(now, player.fetched_at) - player.fetched_at),
|
|
320
320
|
)
|
|
321
321
|
}
|
|
322
322
|
|
|
@@ -467,29 +467,9 @@ export function SidebarPlayer(props: {
|
|
|
467
467
|
const track = createMemo(() => player()?.track)
|
|
468
468
|
const playing = createMemo(() => !!player()?.is_playing)
|
|
469
469
|
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const sync = () => {
|
|
474
|
-
const on = !!props.state.player?.is_playing
|
|
475
|
-
if (on && !id) id = setInterval(() => setTick((t) => t + 1), 1000)
|
|
476
|
-
else if (!on && id) {
|
|
477
|
-
clearInterval(id)
|
|
478
|
-
id = null
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
sync()
|
|
482
|
-
const watch = setInterval(sync, 400)
|
|
483
|
-
onCleanup(() => {
|
|
484
|
-
if (id) clearInterval(id)
|
|
485
|
-
clearInterval(watch)
|
|
486
|
-
})
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
const progress = createMemo(() => {
|
|
490
|
-
tick()
|
|
491
|
-
return liveProgress(player())
|
|
492
|
-
})
|
|
470
|
+
const progress = createMemo(() =>
|
|
471
|
+
liveProgress(player(), props.state.clock_ms),
|
|
472
|
+
)
|
|
493
473
|
const duration = createMemo(() => track()?.duration_ms ?? 0)
|
|
494
474
|
|
|
495
475
|
return (
|