@naxodev/opencode-music-player 0.2.5 → 0.3.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/waveform.tsx DELETED
@@ -1,205 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
- import {
3
- createEngine,
4
- displayLevel,
5
- isFlat,
6
- sameTrackIdentity,
7
- stepEngine,
8
- waveformSeedKey,
9
- type WaveEngine,
10
- } from "@naxodev/music-core"
11
- import { onCleanup, onMount } from "solid-js"
12
- import { StyledText, fg, type TextRenderable } from "@opentui/core"
13
- import type { Plugin } from "@opencode-ai/plugin/tui"
14
- import type { PlayerState } from "./types.ts"
15
-
16
- type Context = Plugin.Context
17
- type Theme = Context["theme"]
18
-
19
- export type PlayerPresentationSource = {
20
- current: () => PlayerState | null
21
- subscribe: (listener: (player: PlayerState | null) => void) => () => void
22
- }
23
-
24
- export type WaveformScheduler = {
25
- setInterval: (callback: () => void, ms: number) => unknown
26
- clearInterval: (timer: unknown) => void
27
- }
28
-
29
- type WaveformCoordinatorOptions = {
30
- now: () => number
31
- scheduler: WaveformScheduler
32
- render: (player: PlayerState, engine: WaveEngine) => void
33
- clear: () => void
34
- intervalMs: number
35
- }
36
-
37
- /** OpenCode-owned engine and animation lifecycle, with injectable time and timers. */
38
- export function createWaveformCoordinator(options: WaveformCoordinatorOptions) {
39
- let player: PlayerState | null = null
40
- let bars = 0
41
- let requestedKey = ""
42
- let engineKey = ""
43
- let engine: WaveEngine | null = null
44
- let timer: unknown = null
45
-
46
- const stop = () => {
47
- if (timer === null) return
48
- options.scheduler.clearInterval(timer)
49
- timer = null
50
- }
51
-
52
- const start = () => {
53
- if (timer !== null || !player?.track) return
54
- timer = options.scheduler.setInterval(() => frame(), options.intervalMs)
55
- }
56
-
57
- const frame = (seek = false) => {
58
- const track = player?.track
59
- if (!player || !track) {
60
- stop()
61
- return null
62
- }
63
-
64
- const key = engineKey || requestedKey
65
- if (!engine || engine.n !== bars || engineKey !== key) {
66
- engine = createEngine(bars, key)
67
- engineKey = key
68
- }
69
- stepEngine(engine, {
70
- track_key: key,
71
- bars,
72
- progress_ms: player.progress_ms,
73
- fetched_at: player.fetched_at,
74
- is_playing: player.is_playing,
75
- duration_ms: track.duration_ms,
76
- now_ms: options.now(),
77
- seek,
78
- })
79
- options.render(player, engine)
80
- if (player.is_playing || !isFlat(engine)) start()
81
- else stop()
82
- return engine
83
- }
84
-
85
- return {
86
- setInput: (
87
- next: PlayerState | null,
88
- trackIdentity: string,
89
- nextBars: number,
90
- ) => {
91
- if (!next?.track) {
92
- player = null
93
- engine = null
94
- engineKey = ""
95
- requestedKey = ""
96
- bars = nextBars
97
- stop()
98
- options.clear()
99
- return
100
- }
101
- if (
102
- bars !== nextBars ||
103
- (player?.track && !sameTrackIdentity(player.track, next.track))
104
- ) {
105
- engine = null
106
- engineKey = ""
107
- }
108
- player = next
109
- requestedKey = trackIdentity
110
- bars = nextBars
111
- },
112
- frame,
113
- start,
114
- stop,
115
- dispose: () => {
116
- stop()
117
- player = null
118
- engine = null
119
- engineKey = ""
120
- requestedKey = ""
121
- },
122
- }
123
- }
124
-
125
- const BLOCK = [" ", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"] as const
126
-
127
- /** Tokyonight blue ramp — low energy → peak (no rainbow). */
128
- const BLUE = [
129
- "#1a1b26", // void
130
- "#24283b", // surface
131
- "#3d59a1", // deep
132
- "#414868", // comment
133
- "#565f89", // dark blue-gray
134
- "#7aa2f7", // blue
135
- "#89b4fa", // soft blue
136
- "#b4c0f7", // pale
137
- "#c0caf5", // foreground flash
138
- ] as const
139
-
140
- function blueFor(level: number, playing: boolean, theme: Theme) {
141
- if (!playing && level < 0.05) return theme.text.subdued
142
- if (level <= 0.02) return BLUE[1]!
143
- const idx = Math.min(BLUE.length - 1, Math.floor(level * (BLUE.length - 1)))
144
- return BLUE[Math.max(2, idx)]!
145
- }
146
-
147
- function blockChar(level: number): string {
148
- return BLOCK[Math.max(0, Math.min(8, Math.round(level * 8)))]!
149
- }
150
-
151
- export function Waveform(props: {
152
- theme: Theme
153
- source: PlayerPresentationSource
154
- bars?: number
155
- /** Kept for API compatibility; both variants render one clean row. */
156
- variant?: "mini" | "hero"
157
- rows?: number
158
- }) {
159
- const barCount = () => props.bars ?? (props.variant === "hero" ? 48 : 16)
160
- let text: TextRenderable | undefined
161
- const paint = (player: PlayerState, engine: WaveEngine) => {
162
- if (!text) return
163
- const playing = player.is_playing
164
- text.content = new StyledText(
165
- Array.from({ length: barCount() }, (_, index) => {
166
- const level = displayLevel(engine.levels[index] ?? 0, index, playing)
167
- return fg(blueFor(level, playing || level > 0.05, props.theme))(
168
- blockChar(level),
169
- )
170
- }),
171
- )
172
- }
173
- const coordinator = createWaveformCoordinator({
174
- now: () => Date.now(),
175
- scheduler: {
176
- setInterval,
177
- clearInterval: (timer) =>
178
- clearInterval(timer as ReturnType<typeof setInterval>),
179
- },
180
- intervalMs: props.variant === "hero" ? 48 : 64,
181
- render: paint,
182
- clear: () => {
183
- if (text) text.content = " ".repeat(barCount())
184
- },
185
- })
186
-
187
- const update = (player: PlayerState | null) => {
188
- coordinator.setInput(
189
- player,
190
- player?.track ? waveformSeedKey(player.track.name, player.track.id) : "",
191
- barCount(),
192
- )
193
- coordinator.frame()
194
- }
195
- const unsubscribe = props.source.subscribe(update)
196
- onMount(() => coordinator.frame())
197
- onCleanup(() => {
198
- unsubscribe()
199
- coordinator.dispose()
200
- })
201
-
202
- return (
203
- <text ref={(element) => (text = element)}>{" ".repeat(barCount())}</text>
204
- )
205
- }