@tomorrowos/sdk 0.1.8 → 0.1.10
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/store/memory-store.d.ts +1 -0
- package/dist/store/memory-store.d.ts.map +1 -1
- package/dist/store/memory-store.js +3 -0
- package/dist/store/types.d.ts +1 -0
- package/dist/store/types.d.ts.map +1 -1
- package/dist/tomorrowos.d.ts +9 -0
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +37 -1
- package/package.json +1 -1
- package/templates/cms-starter/package.json +2 -2
- package/templates/cms-starter/public/index.html +18 -0
- package/templates/cms-starter/public/methods.js +139 -5
|
@@ -11,5 +11,6 @@ export declare class MemoryStore implements TomorrowOSStore {
|
|
|
11
11
|
deletePendingCode(code: string): Promise<void>;
|
|
12
12
|
setPairedDevice(deviceId: string, record: PairedDeviceRecord): Promise<void>;
|
|
13
13
|
getPairedDevice(deviceId: string): Promise<PairedDeviceRecord | undefined>;
|
|
14
|
+
deletePairedDevice(deviceId: string): Promise<void>;
|
|
14
15
|
}
|
|
15
16
|
//# sourceMappingURL=memory-store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/store/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,WAAY,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwC;IACrE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyC;IAEjE,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAIpE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAIV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/store/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,WAAY,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwC;IACrE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyC;IAEjE,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAIpE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAIV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAIpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAG1D"}
|
package/dist/store/types.d.ts
CHANGED
|
@@ -20,5 +20,6 @@ export interface TomorrowOSStore {
|
|
|
20
20
|
deletePendingCode(code: string): Promise<void>;
|
|
21
21
|
setPairedDevice(deviceId: string, record: PairedDeviceRecord): Promise<void>;
|
|
22
22
|
getPairedDevice(deviceId: string): Promise<PairedDeviceRecord | undefined>;
|
|
23
|
+
deletePairedDevice(deviceId: string): Promise<void>;
|
|
23
24
|
}
|
|
24
25
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;IACrE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;IACrE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC;IAC3E,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrD"}
|
package/dist/tomorrowos.d.ts
CHANGED
|
@@ -40,10 +40,19 @@ export declare class TomorrowOS extends EventEmitter {
|
|
|
40
40
|
pairingVerify(code: string): Promise<{
|
|
41
41
|
deviceId: string;
|
|
42
42
|
}>;
|
|
43
|
+
/** Remove pairing for a device and notify it over WebSocket if connected. */
|
|
44
|
+
pairingUnpair(deviceId: string): Promise<{
|
|
45
|
+
deviceId: string;
|
|
46
|
+
notified: boolean;
|
|
47
|
+
}>;
|
|
43
48
|
pairing: {
|
|
44
49
|
verify: (code: string) => Promise<{
|
|
45
50
|
deviceId: string;
|
|
46
51
|
}>;
|
|
52
|
+
unpair: (deviceId: string) => Promise<{
|
|
53
|
+
deviceId: string;
|
|
54
|
+
notified: boolean;
|
|
55
|
+
}>;
|
|
47
56
|
};
|
|
48
57
|
device(deviceId: string): {
|
|
49
58
|
sendCommand<T = unknown>(method: string, params?: Record<string, unknown>): Promise<{
|
package/dist/tomorrowos.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,IAAI,MAAM,MAAM,CAAC;AAKxB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,eAAe,CAAC;IACvB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;
|
|
1
|
+
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,IAAI,MAAM,MAAM,CAAC;AAKxB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,eAAe,CAAC;IACvB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAyED,qBAAa,UAAW,SAAQ,YAAY;IAC1C,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,GAAG,CAAgC;IAC3C,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,eAAe,CAAgB;gBAE3B,OAAO,EAAE,iBAAiB;IAMtC,oEAAoE;IAC9D,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAiChE,6EAA6E;IACvE,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;IA2BvF,OAAO;uBACU,MAAM;sBA9DgC,MAAM;;2BA+DxC,MAAM;sBA7BgC,MAAM;sBAAY,OAAO;;MA8BlF;IAEF,MAAM,CAAC,QAAQ,EAAE,MAAM;oBAGD,CAAC,oBACT,MAAM,WACN,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;;IAU5E,OAAO,CAAC,mBAAmB;IA6D3B,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC,MAAM;YAkD7B,iBAAiB;YAqCjB,cAAc;YAmCd,UAAU;YAkEV,gBAAgB;IAgE9B,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,gBAAgB;CAgFzB"}
|
package/dist/tomorrowos.js
CHANGED
|
@@ -90,8 +90,31 @@ export class TomorrowOS extends EventEmitter {
|
|
|
90
90
|
this.emit("device.paired", { deviceId: record.deviceId });
|
|
91
91
|
return { deviceId: record.deviceId };
|
|
92
92
|
}
|
|
93
|
+
/** Remove pairing for a device and notify it over WebSocket if connected. */
|
|
94
|
+
async pairingUnpair(deviceId) {
|
|
95
|
+
const id = String(deviceId || "").trim();
|
|
96
|
+
if (!id) {
|
|
97
|
+
const err = new Error("deviceId is required");
|
|
98
|
+
err.code = "PAIRING_INVALID";
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
await this.store.deletePairedDevice(id);
|
|
102
|
+
const ws = this.devices.get(id);
|
|
103
|
+
let notified = false;
|
|
104
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
105
|
+
ws.send(JSON.stringify({
|
|
106
|
+
type: "pairing.unpaired",
|
|
107
|
+
method: "tomorrowos.pairing.unpair",
|
|
108
|
+
deviceId: id
|
|
109
|
+
}));
|
|
110
|
+
notified = true;
|
|
111
|
+
}
|
|
112
|
+
this.emit("device.unpaired", { deviceId: id });
|
|
113
|
+
return { deviceId: id, notified };
|
|
114
|
+
}
|
|
93
115
|
pairing = {
|
|
94
|
-
verify: (code) => this.pairingVerify(code)
|
|
116
|
+
verify: (code) => this.pairingVerify(code),
|
|
117
|
+
unpair: (deviceId) => this.pairingUnpair(deviceId)
|
|
95
118
|
};
|
|
96
119
|
device(deviceId) {
|
|
97
120
|
const self = this;
|
|
@@ -291,6 +314,19 @@ export class TomorrowOS extends EventEmitter {
|
|
|
291
314
|
}
|
|
292
315
|
return;
|
|
293
316
|
}
|
|
317
|
+
if (req.method === "POST" && pathname === "/pairing/unpair") {
|
|
318
|
+
const body = (await readJsonBody(req));
|
|
319
|
+
const deviceId = typeof body.deviceId === "string" ? body.deviceId : "";
|
|
320
|
+
try {
|
|
321
|
+
const result = await this.pairingUnpair(deviceId);
|
|
322
|
+
sendJson(res, 200, { status: "success", ...result });
|
|
323
|
+
}
|
|
324
|
+
catch (e) {
|
|
325
|
+
const msg = e instanceof Error ? e.message : "Unpair failed";
|
|
326
|
+
sendJson(res, 400, { status: "failed", error: msg });
|
|
327
|
+
}
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
294
330
|
if (req.method === "POST") {
|
|
295
331
|
const parsed = parseDevicePath(pathname);
|
|
296
332
|
if (parsed) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "TomorrowOS CMS server SDK — WebSocket transport, pairing, device commands, optional static CMS UI. Includes CLI (tomorrowos init / build) and starter templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-cms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "CMS server on @tomorrowos/sdk. Add your UI (React, static files, etc.) alongside this server.",
|
|
5
5
|
"private": true,
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"build-player": "tomorrowos build --platform tizen"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@tomorrowos/sdk": "^0.1.
|
|
13
|
+
"@tomorrowos/sdk": "^0.1.10"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@types/node": "^20.0.0",
|
|
@@ -19,10 +19,28 @@
|
|
|
19
19
|
<div class="row">
|
|
20
20
|
<input id="code" maxlength="6" placeholder="6-digit code" />
|
|
21
21
|
<button type="button" onclick="verify()">Verify</button>
|
|
22
|
+
<button type="button" class="danger" onclick="unpair()">Unpair</button>
|
|
22
23
|
</div>
|
|
23
24
|
<input type="hidden" id="deviceId" />
|
|
24
25
|
</section>
|
|
25
26
|
|
|
27
|
+
<section class="card">
|
|
28
|
+
<h2>CMS URL for screens</h2>
|
|
29
|
+
<p style="margin: 0 0 0.5rem; font-size: 0.8rem; color: #666">
|
|
30
|
+
HTTP(S) base URL your TV uses to download uploads (same host as <code>ws://</code> on the
|
|
31
|
+
device, not <code>localhost</code>).
|
|
32
|
+
</p>
|
|
33
|
+
<div class="row">
|
|
34
|
+
<input
|
|
35
|
+
id="cmsDeviceBaseUrl"
|
|
36
|
+
type="text"
|
|
37
|
+
style="flex: 1; min-width: 16rem"
|
|
38
|
+
placeholder="http://192.168.1.105:3000"
|
|
39
|
+
/>
|
|
40
|
+
<button type="button" onclick="saveCmsDeviceBaseUrl()">Save</button>
|
|
41
|
+
</div>
|
|
42
|
+
</section>
|
|
43
|
+
|
|
26
44
|
<section class="card">
|
|
27
45
|
<h2>When this playlist plays</h2>
|
|
28
46
|
<p style="margin: 0 0 0.75rem; font-size: 0.8rem; color: #666">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const PANEL_DEVICE_ID_KEY = "tomorrowos.panel.deviceId";
|
|
2
2
|
const PANEL_PLAYLIST_KEY = "tomorrowos.panel.playlistDraft";
|
|
3
3
|
const PANEL_SCHEDULE_KEY = "tomorrowos.panel.scheduleDraft";
|
|
4
|
+
const PANEL_MEDIA_BASE_KEY = "tomorrowos.panel.mediaBaseUrl";
|
|
4
5
|
|
|
5
6
|
/** @type {{ id: string, url: string, name: string, type: string, durationMs: number }[]} */
|
|
6
7
|
let playlistItems = [];
|
|
@@ -25,11 +26,65 @@ function showResult(data) {
|
|
|
25
26
|
document.getElementById("result").textContent = JSON.stringify(data, null, 2);
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
function isLocalPanelHost(hostname) {
|
|
30
|
+
const h = String(hostname || "").toLowerCase();
|
|
31
|
+
return h === "localhost" || h === "127.0.0.1" || h === "[::1]";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeMediaBaseUrl(raw) {
|
|
35
|
+
let s = String(raw || "").trim();
|
|
36
|
+
if (!s) return "";
|
|
37
|
+
if (!/^https?:\/\//i.test(s)) {
|
|
38
|
+
s = `http://${s}`;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const u = new URL(s);
|
|
42
|
+
return u.origin;
|
|
43
|
+
} catch {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getMediaBaseOrigin() {
|
|
49
|
+
const fromInput = normalizeMediaBaseUrl(
|
|
50
|
+
document.getElementById("cmsDeviceBaseUrl")?.value ||
|
|
51
|
+
localStorage.getItem(PANEL_MEDIA_BASE_KEY) ||
|
|
52
|
+
""
|
|
53
|
+
);
|
|
54
|
+
if (fromInput) return fromInput;
|
|
55
|
+
|
|
56
|
+
if (!isLocalPanelHost(window.location.hostname)) {
|
|
57
|
+
return window.location.origin;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function saveCmsDeviceBaseUrl() {
|
|
64
|
+
const normalized = normalizeMediaBaseUrl(
|
|
65
|
+
document.getElementById("cmsDeviceBaseUrl")?.value
|
|
66
|
+
);
|
|
67
|
+
if (!normalized) {
|
|
68
|
+
alert("Enter a valid URL, e.g. http://192.168.1.105:3000");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
document.getElementById("cmsDeviceBaseUrl").value = normalized;
|
|
72
|
+
localStorage.setItem(PANEL_MEDIA_BASE_KEY, normalized);
|
|
73
|
+
showResult({ status: "saved", mediaBaseUrl: normalized });
|
|
74
|
+
}
|
|
75
|
+
|
|
28
76
|
function absoluteMediaUrl(path) {
|
|
29
77
|
const p = String(path || "").trim();
|
|
30
78
|
if (!p) return "";
|
|
31
79
|
if (/^https?:\/\//i.test(p)) return p;
|
|
32
|
-
|
|
80
|
+
|
|
81
|
+
const base = getMediaBaseOrigin();
|
|
82
|
+
if (!base) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
"Set CMS URL for screens (use your PC LAN IP or Replit https URL, not localhost)."
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return `${base}${p.startsWith("/") ? p : `/${p}`}`;
|
|
33
88
|
}
|
|
34
89
|
|
|
35
90
|
function inferMediaType(filename, mime) {
|
|
@@ -49,6 +104,19 @@ function defaultDurationMs(type) {
|
|
|
49
104
|
return 10000;
|
|
50
105
|
}
|
|
51
106
|
|
|
107
|
+
function normalizeDurationMs(item) {
|
|
108
|
+
const maxMs = 3600 * 1000;
|
|
109
|
+
const minMs = 1000;
|
|
110
|
+
let ms = Number(item?.durationMs);
|
|
111
|
+
if (!Number.isFinite(ms) || ms < minMs) {
|
|
112
|
+
return defaultDurationMs(item?.type);
|
|
113
|
+
}
|
|
114
|
+
if (ms === 1000000) {
|
|
115
|
+
return defaultDurationMs(item?.type);
|
|
116
|
+
}
|
|
117
|
+
return Math.min(maxMs, ms);
|
|
118
|
+
}
|
|
119
|
+
|
|
52
120
|
function savePlaylistDraft() {
|
|
53
121
|
localStorage.setItem(PANEL_PLAYLIST_KEY, JSON.stringify(playlistItems));
|
|
54
122
|
}
|
|
@@ -87,7 +155,11 @@ function loadPlaylistDraft() {
|
|
|
87
155
|
if (!raw) return;
|
|
88
156
|
const parsed = JSON.parse(raw);
|
|
89
157
|
if (Array.isArray(parsed)) {
|
|
90
|
-
playlistItems = parsed
|
|
158
|
+
playlistItems = parsed.map((item) => ({
|
|
159
|
+
...item,
|
|
160
|
+
durationMs: normalizeDurationMs(item)
|
|
161
|
+
}));
|
|
162
|
+
savePlaylistDraft();
|
|
91
163
|
renderPlaylist();
|
|
92
164
|
}
|
|
93
165
|
} catch {
|
|
@@ -145,9 +217,10 @@ function renderPlaylist() {
|
|
|
145
217
|
durInput.max = "3600";
|
|
146
218
|
durInput.value = String(Math.round(item.durationMs / 1000));
|
|
147
219
|
durInput.addEventListener("change", () => {
|
|
148
|
-
|
|
220
|
+
const seconds = Math.min(3600, Math.max(1, Number(durInput.value) || 10));
|
|
221
|
+
item.durationMs = seconds * 1000;
|
|
222
|
+
meta.textContent = `${item.type} · ${seconds}s`;
|
|
149
223
|
savePlaylistDraft();
|
|
150
|
-
renderPlaylist();
|
|
151
224
|
});
|
|
152
225
|
|
|
153
226
|
const removeBtn = document.createElement("button");
|
|
@@ -258,6 +331,39 @@ async function verify() {
|
|
|
258
331
|
}
|
|
259
332
|
}
|
|
260
333
|
|
|
334
|
+
async function unpair() {
|
|
335
|
+
const deviceId = getPanelDeviceId();
|
|
336
|
+
if (!deviceId) {
|
|
337
|
+
alert("No paired device in this panel.");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (
|
|
342
|
+
!confirm(
|
|
343
|
+
"Unpair this device? The screen will show a new 6-digit code and this panel will forget the device."
|
|
344
|
+
)
|
|
345
|
+
) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const res = await fetch("/pairing/unpair", {
|
|
350
|
+
method: "POST",
|
|
351
|
+
headers: { "Content-Type": "application/json" },
|
|
352
|
+
body: JSON.stringify({ deviceId })
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const data = await res.json();
|
|
356
|
+
showResult(data);
|
|
357
|
+
|
|
358
|
+
if (res.ok) {
|
|
359
|
+
setPanelDeviceId("");
|
|
360
|
+
const codeInput = document.getElementById("code");
|
|
361
|
+
if (codeInput) codeInput.value = "";
|
|
362
|
+
} else {
|
|
363
|
+
alert(data.error || "Unpair failed");
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
261
367
|
async function publish() {
|
|
262
368
|
const deviceId = getPanelDeviceId();
|
|
263
369
|
|
|
@@ -271,8 +377,28 @@ async function publish() {
|
|
|
271
377
|
return;
|
|
272
378
|
}
|
|
273
379
|
|
|
380
|
+
const mediaBase = getMediaBaseOrigin();
|
|
381
|
+
if (!mediaBase) {
|
|
382
|
+
alert(
|
|
383
|
+
"Set CMS URL for screens first (e.g. http://192.168.1.105:3000 — same machine as the TV ws:// address)."
|
|
384
|
+
);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (isLocalPanelHost(new URL(mediaBase).hostname)) {
|
|
388
|
+
const ok = confirm(
|
|
389
|
+
"Media URLs use localhost. TVs cannot download from localhost unless the player runs on this PC. Continue anyway?"
|
|
390
|
+
);
|
|
391
|
+
if (!ok) return;
|
|
392
|
+
}
|
|
393
|
+
|
|
274
394
|
saveScheduleDraft();
|
|
275
|
-
|
|
395
|
+
let payload;
|
|
396
|
+
try {
|
|
397
|
+
payload = buildPolicyPayload();
|
|
398
|
+
} catch (err) {
|
|
399
|
+
alert(err.message);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
276
402
|
|
|
277
403
|
const res = await fetch(`/device/${deviceId}/content/set-policy`, {
|
|
278
404
|
method: "POST",
|
|
@@ -330,6 +456,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
330
456
|
const saved = localStorage.getItem(PANEL_DEVICE_ID_KEY);
|
|
331
457
|
if (saved) setPanelDeviceId(saved);
|
|
332
458
|
|
|
459
|
+
const savedMediaBase = localStorage.getItem(PANEL_MEDIA_BASE_KEY);
|
|
460
|
+
const cmsBaseInput = document.getElementById("cmsDeviceBaseUrl");
|
|
461
|
+
if (savedMediaBase && cmsBaseInput) {
|
|
462
|
+
cmsBaseInput.value = savedMediaBase;
|
|
463
|
+
} else if (cmsBaseInput && !isLocalPanelHost(window.location.hostname)) {
|
|
464
|
+
cmsBaseInput.value = window.location.origin;
|
|
465
|
+
}
|
|
466
|
+
|
|
333
467
|
loadPlaylistDraft();
|
|
334
468
|
loadScheduleDraft();
|
|
335
469
|
|