@naxodev/opencode-music-player 0.2.6 → 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/README.md +13 -14
- package/SUPPORT.md +1 -1
- package/dist/index.js +2272 -0
- package/dist/tui.js +1 -0
- package/package.json +28 -20
- package/artwork-placement.ts +0 -169
- package/artwork.ts +0 -240
- package/artwork.tsx +0 -246
- package/index.tsx +0 -493
- package/kitty-graphics.ts +0 -105
- package/system-media.ts +0 -593
- package/tmux-offset.ts +0 -120
- package/tui.tsx +0 -1
- package/types.ts +0 -144
- package/ui.tsx +0 -695
- package/waveform.tsx +0 -205
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
|
-
}
|