@naxodev/opencode-music-player 0.5.1 → 0.5.3
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/dist/index.js +138 -67
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -32,6 +32,14 @@ var CONVERSION_TIMEOUT_MS = 3000;
|
|
|
32
32
|
function imageDimensionsAreSafe(width, height) {
|
|
33
33
|
return Number.isInteger(width) && Number.isInteger(height) && width > 0 && height > 0 && width <= MAX_IMAGE_DIMENSION && height <= MAX_IMAGE_DIMENSION && width * height <= MAX_IMAGE_PIXELS;
|
|
34
34
|
}
|
|
35
|
+
function pngDimensions(base64) {
|
|
36
|
+
const header = Buffer.from(base64.slice(0, 44), "base64");
|
|
37
|
+
if (header.length < 24 || header.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a" || header.readUInt32BE(8) !== 13 || header.toString("ascii", 12, 16) !== "IHDR")
|
|
38
|
+
return null;
|
|
39
|
+
const width = header.readUInt32BE(16);
|
|
40
|
+
const height = header.readUInt32BE(20);
|
|
41
|
+
return imageDimensionsAreSafe(width, height) ? { width, height } : null;
|
|
42
|
+
}
|
|
35
43
|
async function runCommandWithTimeout(command, timeoutMs) {
|
|
36
44
|
const proc = Bun.spawn(command, { stdout: "pipe", stderr: "ignore" });
|
|
37
45
|
let timedOut = false;
|
|
@@ -54,7 +62,7 @@ function dimensionsFromSips(output) {
|
|
|
54
62
|
const height = Number(output.match(/pixelHeight:\s*(\d+)/)?.[1]);
|
|
55
63
|
return imageDimensionsAreSafe(width, height) ? { width, height } : null;
|
|
56
64
|
}
|
|
57
|
-
async function
|
|
65
|
+
async function fitPng(bytes, size) {
|
|
58
66
|
if (bytes.byteLength === 0 || bytes.byteLength > MAX_ARTWORK_BYTES)
|
|
59
67
|
return null;
|
|
60
68
|
const dir = await mkdtemp(join(tmpdir(), "opencode-music-art-"));
|
|
@@ -63,14 +71,18 @@ async function squarePng(bytes, size) {
|
|
|
63
71
|
try {
|
|
64
72
|
await Bun.write(input, bytes);
|
|
65
73
|
const inspected = await runCommandWithTimeout(["sips", "-g", "pixelWidth", "-g", "pixelHeight", input], CONVERSION_TIMEOUT_MS);
|
|
66
|
-
|
|
74
|
+
const dimensions = dimensionsFromSips(inspected.out);
|
|
75
|
+
if (inspected.timed_out || inspected.code !== 0 || !dimensions) {
|
|
67
76
|
return null;
|
|
68
77
|
}
|
|
78
|
+
const scale = size / Math.max(dimensions.width, dimensions.height);
|
|
79
|
+
const width = Math.max(1, Math.round(dimensions.width * scale));
|
|
80
|
+
const height = Math.max(1, Math.round(dimensions.height * scale));
|
|
69
81
|
const converted = await runCommandWithTimeout([
|
|
70
82
|
"sips",
|
|
71
83
|
"-z",
|
|
72
|
-
String(
|
|
73
|
-
String(
|
|
84
|
+
String(height),
|
|
85
|
+
String(width),
|
|
74
86
|
"-s",
|
|
75
87
|
"format",
|
|
76
88
|
"png",
|
|
@@ -134,8 +146,8 @@ async function resolveArtworkDetails(id, target, nativeBase64, legacyId = id, fe
|
|
|
134
146
|
}
|
|
135
147
|
for (const bytes of candidates) {
|
|
136
148
|
try {
|
|
137
|
-
const nativePng = await
|
|
138
|
-
const thumbnail = nativePng ? await
|
|
149
|
+
const nativePng = await fitPng(bytes, 300);
|
|
150
|
+
const thumbnail = nativePng ? await fitPng(nativePng, 24) : null;
|
|
139
151
|
if (nativePng && thumbnail) {
|
|
140
152
|
return {
|
|
141
153
|
artwork: {
|
|
@@ -162,8 +174,8 @@ async function resolveArtworkDetails(id, target, nativeBase64, legacyId = id, fe
|
|
|
162
174
|
return { artwork: null, duration_ms: catalog.duration_ms };
|
|
163
175
|
}
|
|
164
176
|
try {
|
|
165
|
-
const nativePng = await
|
|
166
|
-
const thumbnail = nativePng ? await
|
|
177
|
+
const nativePng = await fitPng(catalog.bytes, 300);
|
|
178
|
+
const thumbnail = nativePng ? await fitPng(nativePng, 24) : null;
|
|
167
179
|
if (!nativePng || !thumbnail) {
|
|
168
180
|
return { artwork: null, duration_ms: catalog.duration_ms };
|
|
169
181
|
}
|
|
@@ -709,7 +721,7 @@ import { use as _$use3 } from "@opentui/solid";
|
|
|
709
721
|
import { setProp as _$setProp2 } from "@opentui/solid";
|
|
710
722
|
import { createElement as _$createElement3 } from "@opentui/solid";
|
|
711
723
|
import { createComponent as _$createComponent2 } from "@opentui/solid";
|
|
712
|
-
import { Show, createEffect, createMemo, createSignal, onCleanup as onCleanup3, onMount as onMount3 } from "solid-js";
|
|
724
|
+
import { Show as Show2, createEffect, createMemo as createMemo2, createSignal as createSignal2, onCleanup as onCleanup3, onMount as onMount3 } from "solid-js";
|
|
713
725
|
import { MouseButton } from "@opentui/core";
|
|
714
726
|
|
|
715
727
|
// artwork.tsx
|
|
@@ -721,9 +733,49 @@ import { createComponent as _$createComponent } from "@opentui/solid";
|
|
|
721
733
|
import { setProp as _$setProp } from "@opentui/solid";
|
|
722
734
|
import { use as _$use } from "@opentui/solid";
|
|
723
735
|
import { createElement as _$createElement } from "@opentui/solid";
|
|
724
|
-
import { For, onCleanup, onMount } from "solid-js";
|
|
736
|
+
import { For, Show, createMemo, createSignal, onCleanup, onMount } from "solid-js";
|
|
725
737
|
|
|
726
738
|
// artwork-placement.ts
|
|
739
|
+
function fitNativeArtworkGeometry(slot, image, cell) {
|
|
740
|
+
if ([
|
|
741
|
+
slot.width,
|
|
742
|
+
slot.height,
|
|
743
|
+
image.width,
|
|
744
|
+
image.height,
|
|
745
|
+
cell.width,
|
|
746
|
+
cell.height
|
|
747
|
+
].some((value) => !Number.isFinite(value) || value <= 0))
|
|
748
|
+
return null;
|
|
749
|
+
const width = Math.floor(slot.width);
|
|
750
|
+
const height = Math.floor(slot.height);
|
|
751
|
+
if (width < 1 || height < 1)
|
|
752
|
+
return null;
|
|
753
|
+
const pixelWidth = width * cell.width;
|
|
754
|
+
const pixelHeight = height * cell.height;
|
|
755
|
+
const aspect = image.width / image.height;
|
|
756
|
+
if (pixelWidth / aspect <= pixelHeight) {
|
|
757
|
+
const displayedHeight = Math.round(pixelWidth / aspect);
|
|
758
|
+
if (displayedHeight < 1)
|
|
759
|
+
return null;
|
|
760
|
+
const occupiedRows = Math.ceil(displayedHeight / cell.height);
|
|
761
|
+
return {
|
|
762
|
+
x: slot.x,
|
|
763
|
+
y: slot.y + Math.floor((height - occupiedRows) / 2),
|
|
764
|
+
width,
|
|
765
|
+
height: 0
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
const displayedWidth = Math.round(pixelHeight * aspect);
|
|
769
|
+
if (displayedWidth < 1)
|
|
770
|
+
return null;
|
|
771
|
+
const occupiedColumns = Math.ceil(displayedWidth / cell.width);
|
|
772
|
+
return {
|
|
773
|
+
x: slot.x + Math.floor((width - occupiedColumns) / 2),
|
|
774
|
+
y: slot.y,
|
|
775
|
+
width: 0,
|
|
776
|
+
height
|
|
777
|
+
};
|
|
778
|
+
}
|
|
727
779
|
var CLEARED_STATE = {
|
|
728
780
|
transmitted: 0,
|
|
729
781
|
placement: null
|
|
@@ -798,6 +850,9 @@ function planNativeArtworkPlacement(input) {
|
|
|
798
850
|
var APC = "\x1B_G";
|
|
799
851
|
var ST = "\x1B\\";
|
|
800
852
|
var CHUNK_SIZE = 4096;
|
|
853
|
+
function scalingDimensions(width, height) {
|
|
854
|
+
return [width > 0 ? `c=${width}` : "", height > 0 ? `r=${height}` : ""].filter(Boolean).join(",");
|
|
855
|
+
}
|
|
801
856
|
function tmuxPassthrough(data) {
|
|
802
857
|
return `\x1BPtmux;${data.replaceAll("\x1B", "\x1B\x1B")}\x1B\\`;
|
|
803
858
|
}
|
|
@@ -816,13 +871,13 @@ function kittyDisplayPng(pngBase64, imageId, x, y, width, height) {
|
|
|
816
871
|
return chunks.map((command, index) => {
|
|
817
872
|
let next = command;
|
|
818
873
|
if (index === 0) {
|
|
819
|
-
next = next.replace(`${APC}a=t,f=100,i=${imageId},q=2,`, `\x1B7\x1B[${y + 1};${x + 1}H${APC}a=T,f=100,i=${imageId},p=${imageId},q=2,C=1
|
|
874
|
+
next = next.replace(`${APC}a=t,f=100,i=${imageId},q=2,`, `\x1B7\x1B[${y + 1};${x + 1}H${APC}a=T,f=100,i=${imageId},p=${imageId},q=2,C=1,${scalingDimensions(width, height)},z=1,`);
|
|
820
875
|
}
|
|
821
876
|
return index === chunks.length - 1 ? `${next}\x1B8` : next;
|
|
822
877
|
});
|
|
823
878
|
}
|
|
824
879
|
function kittyPlace(imageId, placementId, x, y, width, height) {
|
|
825
|
-
return `\x1B7\x1B[${y + 1};${x + 1}H${APC}a=p,i=${imageId},p=${placementId},q=2
|
|
880
|
+
return `\x1B7\x1B[${y + 1};${x + 1}H${APC}a=p,i=${imageId},p=${placementId},q=2,${scalingDimensions(width, height)},z=1;${ST}\x1B8`;
|
|
826
881
|
}
|
|
827
882
|
function kittyDelete(imageId) {
|
|
828
883
|
return `${APC}a=d,d=I,i=${imageId},q=2;${ST}`;
|
|
@@ -840,25 +895,14 @@ function kittyImageId(key) {
|
|
|
840
895
|
}
|
|
841
896
|
function writeGraphics(renderer, data) {
|
|
842
897
|
const target = renderer;
|
|
843
|
-
if (
|
|
898
|
+
if (typeof target.writeOut !== "function")
|
|
844
899
|
return false;
|
|
900
|
+
const commands = typeof data === "string" ? [data] : data;
|
|
845
901
|
const output = (value) => process.env.TMUX && !process.env.HERDR_ENV ? tmuxPassthrough(value) : value;
|
|
846
|
-
let cursorSaved = false;
|
|
847
902
|
try {
|
|
848
|
-
|
|
849
|
-
if (command.includes("\x1B7"))
|
|
850
|
-
cursorSaved = true;
|
|
851
|
-
target.realStdoutWrite.call(target.stdout, output(command));
|
|
852
|
-
if (command.includes("\x1B8"))
|
|
853
|
-
cursorSaved = false;
|
|
854
|
-
}
|
|
903
|
+
target.writeOut.call(renderer, commands.map(output).join(""));
|
|
855
904
|
return true;
|
|
856
905
|
} catch {
|
|
857
|
-
if (cursorSaved) {
|
|
858
|
-
try {
|
|
859
|
-
target.realStdoutWrite.call(target.stdout, output("\x1B8"));
|
|
860
|
-
} catch {}
|
|
861
|
-
}
|
|
862
906
|
return false;
|
|
863
907
|
}
|
|
864
908
|
}
|
|
@@ -986,6 +1030,8 @@ function AlbumArtwork(props) {
|
|
|
986
1030
|
let container;
|
|
987
1031
|
let paintPending = false;
|
|
988
1032
|
let disposed = false;
|
|
1033
|
+
const [nativeVisible, setNativeVisible] = createSignal(false);
|
|
1034
|
+
const imageDimensions = createMemo(() => pngDimensions(props.artwork.png_base64));
|
|
989
1035
|
const applyAction = (action) => {
|
|
990
1036
|
const renderer2 = props.context.renderer;
|
|
991
1037
|
switch (action.type) {
|
|
@@ -1034,23 +1080,39 @@ function AlbumArtwork(props) {
|
|
|
1034
1080
|
const imageId = imageIdForArtwork(props.artwork.id);
|
|
1035
1081
|
const x = slot ? slot.screenX + offset.x : 0;
|
|
1036
1082
|
const y = slot ? slot.screenY + offset.y : 0;
|
|
1037
|
-
const
|
|
1038
|
-
const
|
|
1083
|
+
const image = imageDimensions();
|
|
1084
|
+
const resolution = renderer.resolution;
|
|
1085
|
+
const geometry = slot && image && resolution ? fitNativeArtworkGeometry({
|
|
1086
|
+
x,
|
|
1087
|
+
y,
|
|
1088
|
+
width: slot.width,
|
|
1089
|
+
height: slot.height
|
|
1090
|
+
}, image, {
|
|
1091
|
+
width: Math.floor(resolution.width / renderer.terminalWidth),
|
|
1092
|
+
height: Math.floor(resolution.height / renderer.terminalHeight)
|
|
1093
|
+
}) : null;
|
|
1039
1094
|
const plan = planNativeArtworkPlacement({
|
|
1040
1095
|
state: runtime.state,
|
|
1041
1096
|
imageId,
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1097
|
+
...geometry ?? {
|
|
1098
|
+
x,
|
|
1099
|
+
y,
|
|
1100
|
+
width: 0,
|
|
1101
|
+
height: 0
|
|
1102
|
+
},
|
|
1046
1103
|
kittySupported,
|
|
1047
|
-
slotValid,
|
|
1104
|
+
slotValid: slotValid && geometry !== null,
|
|
1048
1105
|
disposed
|
|
1049
1106
|
});
|
|
1050
|
-
if (plan.actions.length === 0)
|
|
1107
|
+
if (plan.actions.length === 0) {
|
|
1108
|
+
setNativeVisible(runtime.state.placement !== null);
|
|
1051
1109
|
return;
|
|
1052
|
-
|
|
1110
|
+
}
|
|
1111
|
+
if (applyPlan(plan)) {
|
|
1053
1112
|
commitPlan(plan);
|
|
1113
|
+
setNativeVisible(plan.nextState.placement !== null);
|
|
1114
|
+
} else
|
|
1115
|
+
setNativeVisible(false);
|
|
1054
1116
|
};
|
|
1055
1117
|
const scheduleNativeImage = () => {
|
|
1056
1118
|
if (paintPending || disposed)
|
|
@@ -1088,27 +1150,36 @@ function AlbumArtwork(props) {
|
|
|
1088
1150
|
_$setProp(_el$, "width", 24);
|
|
1089
1151
|
_$setProp(_el$, "height", 12);
|
|
1090
1152
|
_$setProp(_el$, "flexDirection", "column");
|
|
1153
|
+
_$setProp(_el$, "alignItems", "center");
|
|
1154
|
+
_$setProp(_el$, "justifyContent", "center");
|
|
1091
1155
|
_$setProp(_el$, "overflow", "hidden");
|
|
1092
|
-
_$insert(_el$, _$createComponent(
|
|
1093
|
-
get
|
|
1094
|
-
return
|
|
1156
|
+
_$insert(_el$, _$createComponent(Show, {
|
|
1157
|
+
get when() {
|
|
1158
|
+
return !nativeVisible();
|
|
1095
1159
|
},
|
|
1096
|
-
children
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
_$
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1160
|
+
get children() {
|
|
1161
|
+
return _$createComponent(For, {
|
|
1162
|
+
get each() {
|
|
1163
|
+
return props.artwork.cells;
|
|
1164
|
+
},
|
|
1165
|
+
children: (row) => (() => {
|
|
1166
|
+
var _el$2 = _$createElement("text");
|
|
1167
|
+
_$insert(_el$2, _$createComponent(For, {
|
|
1168
|
+
each: row,
|
|
1169
|
+
children: (cell) => (() => {
|
|
1170
|
+
var _el$3 = _$createElement("span");
|
|
1171
|
+
_$insertNode(_el$3, _$createTextNode(`\u2580`));
|
|
1172
|
+
_$effect((_$p) => _$setProp(_el$3, "style", {
|
|
1173
|
+
fg: cell.upper,
|
|
1174
|
+
bg: cell.lower
|
|
1175
|
+
}, _$p));
|
|
1176
|
+
return _el$3;
|
|
1177
|
+
})()
|
|
1178
|
+
}));
|
|
1179
|
+
return _el$2;
|
|
1108
1180
|
})()
|
|
1109
|
-
})
|
|
1110
|
-
|
|
1111
|
-
})()
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1112
1183
|
}));
|
|
1113
1184
|
return _el$;
|
|
1114
1185
|
})();
|
|
@@ -1376,7 +1447,7 @@ function CompactPlayer(props) {
|
|
|
1376
1447
|
let row;
|
|
1377
1448
|
let content;
|
|
1378
1449
|
let allocatedWidth = Number.MAX_SAFE_INTEGER;
|
|
1379
|
-
const track =
|
|
1450
|
+
const track = createMemo2(() => props.state.player?.track);
|
|
1380
1451
|
const renderLine = () => {
|
|
1381
1452
|
const current = track();
|
|
1382
1453
|
if (!current)
|
|
@@ -1399,7 +1470,7 @@ function CompactPlayer(props) {
|
|
|
1399
1470
|
if (row?.onSizeChange === updateWidth)
|
|
1400
1471
|
row.onSizeChange = undefined;
|
|
1401
1472
|
});
|
|
1402
|
-
return _$createComponent2(
|
|
1473
|
+
return _$createComponent2(Show2, {
|
|
1403
1474
|
get when() {
|
|
1404
1475
|
return track();
|
|
1405
1476
|
},
|
|
@@ -1580,8 +1651,8 @@ function LiveElapsed(props) {
|
|
|
1580
1651
|
})();
|
|
1581
1652
|
}
|
|
1582
1653
|
function IconBtn(props) {
|
|
1583
|
-
const [hover, setHover] =
|
|
1584
|
-
const [press, setPress] =
|
|
1654
|
+
const [hover, setHover] = createSignal2(false);
|
|
1655
|
+
const [press, setPress] = createSignal2(false);
|
|
1585
1656
|
const bg = () => {
|
|
1586
1657
|
if (press() || props.active) {
|
|
1587
1658
|
return props.theme.background.action.primary.base;
|
|
@@ -1659,10 +1730,10 @@ function SidebarPlayer(props) {
|
|
|
1659
1730
|
unsubscribeSession?.();
|
|
1660
1731
|
presentationListeners.clear();
|
|
1661
1732
|
});
|
|
1662
|
-
const player =
|
|
1663
|
-
const track =
|
|
1664
|
-
const playing =
|
|
1665
|
-
const duration =
|
|
1733
|
+
const player = createMemo2(() => props.state.player);
|
|
1734
|
+
const track = createMemo2(() => player()?.track);
|
|
1735
|
+
const playing = createMemo2(() => !!player()?.is_playing);
|
|
1736
|
+
const duration = createMemo2(() => presentationDuration(track()));
|
|
1666
1737
|
return (() => {
|
|
1667
1738
|
var _el$1 = _$createElement3("box"), _el$10 = _$createElement3("box"), _el$11 = _$createElement3("box"), _el$12 = _$createElement3("box"), _el$16 = _$createElement3("box");
|
|
1668
1739
|
_$insertNode2(_el$1, _el$10);
|
|
@@ -1676,7 +1747,7 @@ function SidebarPlayer(props) {
|
|
|
1676
1747
|
_$setProp2(_el$1, "paddingBottom", 1);
|
|
1677
1748
|
_$setProp2(_el$1, "gap", 1);
|
|
1678
1749
|
_$setProp2(_el$1, "flexShrink", 0);
|
|
1679
|
-
_$insert3(_el$1, _$createComponent2(
|
|
1750
|
+
_$insert3(_el$1, _$createComponent2(Show2, {
|
|
1680
1751
|
get when() {
|
|
1681
1752
|
return props.state.error;
|
|
1682
1753
|
},
|
|
@@ -1693,7 +1764,7 @@ function SidebarPlayer(props) {
|
|
|
1693
1764
|
_$setProp2(_el$11, "width", 24);
|
|
1694
1765
|
_$setProp2(_el$11, "height", 12);
|
|
1695
1766
|
_$setProp2(_el$11, "overflow", "hidden");
|
|
1696
|
-
_$insert3(_el$11, _$createComponent2(
|
|
1767
|
+
_$insert3(_el$11, _$createComponent2(Show2, {
|
|
1697
1768
|
get when() {
|
|
1698
1769
|
return track()?.artwork;
|
|
1699
1770
|
},
|
|
@@ -1722,7 +1793,7 @@ function SidebarPlayer(props) {
|
|
|
1722
1793
|
}
|
|
1723
1794
|
})
|
|
1724
1795
|
}));
|
|
1725
|
-
_$insert3(_el$1, _$createComponent2(
|
|
1796
|
+
_$insert3(_el$1, _$createComponent2(Show2, {
|
|
1726
1797
|
get when() {
|
|
1727
1798
|
return track();
|
|
1728
1799
|
},
|
|
@@ -1741,7 +1812,7 @@ function SidebarPlayer(props) {
|
|
|
1741
1812
|
_$setProp2(_el$21, "gap", 0);
|
|
1742
1813
|
_$insertNode2(_el$22, _el$23);
|
|
1743
1814
|
_$insert3(_el$23, () => sanitizeTerminalText(t2().name, "Unknown track"));
|
|
1744
|
-
_$insert3(_el$21, _$createComponent2(
|
|
1815
|
+
_$insert3(_el$21, _$createComponent2(Show2, {
|
|
1745
1816
|
get when() {
|
|
1746
1817
|
return t2().artists;
|
|
1747
1818
|
},
|
|
@@ -1752,7 +1823,7 @@ function SidebarPlayer(props) {
|
|
|
1752
1823
|
return _el$24;
|
|
1753
1824
|
}
|
|
1754
1825
|
}), null);
|
|
1755
|
-
_$insert3(_el$21, _$createComponent2(
|
|
1826
|
+
_$insert3(_el$21, _$createComponent2(Show2, {
|
|
1756
1827
|
get when() {
|
|
1757
1828
|
return t2().album;
|
|
1758
1829
|
},
|
|
@@ -1778,7 +1849,7 @@ function SidebarPlayer(props) {
|
|
|
1778
1849
|
bars: 24,
|
|
1779
1850
|
variant: "hero"
|
|
1780
1851
|
}));
|
|
1781
|
-
_$insert3(_el$1, _$createComponent2(
|
|
1852
|
+
_$insert3(_el$1, _$createComponent2(Show2, {
|
|
1782
1853
|
get when() {
|
|
1783
1854
|
return _$memo(() => !!track())() && duration() > 0;
|
|
1784
1855
|
},
|