@tomorrowos/sdk 0.8.4 → 0.8.6
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/REPLIT_SETUP.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.6",
|
|
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.8.
|
|
3
|
+
"version": "0.8.6",
|
|
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",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build-player": "tomorrowos build --platform tizen"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@tomorrowos/sdk": "^0.8.
|
|
16
|
+
"@tomorrowos/sdk": "^0.8.6",
|
|
17
17
|
"dotenv": "^17.2.3",
|
|
18
18
|
"tsx": "^4.19.0"
|
|
19
19
|
},
|
|
@@ -155,6 +155,23 @@
|
|
|
155
155
|
</div>
|
|
156
156
|
</div>
|
|
157
157
|
|
|
158
|
+
<div id="downloadFailedModal" class="modal hidden" role="dialog" aria-modal="true">
|
|
159
|
+
<div class="modal-backdrop" data-close-download-failed-modal="1"></div>
|
|
160
|
+
<div class="modal-card">
|
|
161
|
+
<h2>Download failed</h2>
|
|
162
|
+
<p class="hint" id="downloadFailedModalMessage">Could not download this asset.</p>
|
|
163
|
+
<p class="hint">
|
|
164
|
+
For permanent, reliable storage of media assets, configure
|
|
165
|
+
<strong>Cloudinary</strong> on your CMS server (<code>CLOUDINARY_CLOUD_NAME</code>,
|
|
166
|
+
<code>CLOUDINARY_API_KEY</code>, <code>CLOUDINARY_API_SECRET</code>). Uploads stored on
|
|
167
|
+
Cloudinary use stable public URLs that are easier to download and share.
|
|
168
|
+
</p>
|
|
169
|
+
<div class="modal-actions">
|
|
170
|
+
<button type="button" data-close-download-failed-modal="1">Close</button>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
158
175
|
<script src="./methods.js" defer></script>
|
|
159
176
|
</body>
|
|
160
177
|
</html>
|
|
@@ -808,9 +808,16 @@ function renderEditorAssets() {
|
|
|
808
808
|
meta.textContent = `${item.type} · ${Math.round(item.durationMs / 1000)}s`;
|
|
809
809
|
});
|
|
810
810
|
|
|
811
|
+
const downloadBtn = document.createElement("button");
|
|
812
|
+
downloadBtn.type = "button";
|
|
813
|
+
downloadBtn.className = "playlist-item-btn";
|
|
814
|
+
downloadBtn.textContent = "Download";
|
|
815
|
+
downloadBtn.title = "Download asset";
|
|
816
|
+
downloadBtn.addEventListener("click", () => void downloadMediaAsset(item));
|
|
817
|
+
|
|
811
818
|
const removeBtn = document.createElement("button");
|
|
812
819
|
removeBtn.type = "button";
|
|
813
|
-
removeBtn.className = "danger";
|
|
820
|
+
removeBtn.className = "danger playlist-item-btn";
|
|
814
821
|
removeBtn.textContent = "Remove";
|
|
815
822
|
removeBtn.addEventListener("click", () => {
|
|
816
823
|
editorItems = editorItems.filter((x) => x.id !== item.id);
|
|
@@ -818,6 +825,7 @@ function renderEditorAssets() {
|
|
|
818
825
|
});
|
|
819
826
|
|
|
820
827
|
actions.appendChild(durInput);
|
|
828
|
+
actions.appendChild(downloadBtn);
|
|
821
829
|
actions.appendChild(removeBtn);
|
|
822
830
|
li.appendChild(name);
|
|
823
831
|
li.appendChild(meta);
|
|
@@ -1679,6 +1687,70 @@ function closeScreenshotModal() {
|
|
|
1679
1687
|
modal?.classList.add("hidden");
|
|
1680
1688
|
}
|
|
1681
1689
|
|
|
1690
|
+
function openDownloadFailedModal(detail) {
|
|
1691
|
+
const modal = document.getElementById("downloadFailedModal");
|
|
1692
|
+
const message = document.getElementById("downloadFailedModalMessage");
|
|
1693
|
+
if (!modal) return;
|
|
1694
|
+
if (message) {
|
|
1695
|
+
message.textContent = detail
|
|
1696
|
+
? `Could not download this asset: ${detail}`
|
|
1697
|
+
: "Could not download this asset.";
|
|
1698
|
+
}
|
|
1699
|
+
modal.classList.remove("hidden");
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function closeDownloadFailedModal() {
|
|
1703
|
+
document.getElementById("downloadFailedModal")?.classList.add("hidden");
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function sanitizeDownloadFilename(name) {
|
|
1707
|
+
const base = String(name || "asset").trim() || "asset";
|
|
1708
|
+
return base.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_");
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
async function downloadMediaAsset(item) {
|
|
1712
|
+
const candidates = [];
|
|
1713
|
+
const sameOriginPath = resolveVerificationMediaUrl(item.url);
|
|
1714
|
+
if (sameOriginPath) candidates.push(sameOriginPath);
|
|
1715
|
+
try {
|
|
1716
|
+
const absolute = absoluteMediaUrl(item.url);
|
|
1717
|
+
if (absolute && !candidates.includes(absolute)) candidates.push(absolute);
|
|
1718
|
+
} catch (_) {}
|
|
1719
|
+
|
|
1720
|
+
if (candidates.length === 0) {
|
|
1721
|
+
openDownloadFailedModal("Media URL is not available.");
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
const filename = sanitizeDownloadFilename(item.name || item.url.split("/").pop() || "asset");
|
|
1726
|
+
|
|
1727
|
+
let lastError = "Network or browser error.";
|
|
1728
|
+
for (const url of candidates) {
|
|
1729
|
+
try {
|
|
1730
|
+
const res = await fetch(url);
|
|
1731
|
+
if (!res.ok) {
|
|
1732
|
+
lastError = `HTTP ${res.status}`;
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
const blob = await res.blob();
|
|
1736
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
1737
|
+
const anchor = document.createElement("a");
|
|
1738
|
+
anchor.href = objectUrl;
|
|
1739
|
+
anchor.download = filename;
|
|
1740
|
+
anchor.style.display = "none";
|
|
1741
|
+
document.body.appendChild(anchor);
|
|
1742
|
+
anchor.click();
|
|
1743
|
+
anchor.remove();
|
|
1744
|
+
URL.revokeObjectURL(objectUrl);
|
|
1745
|
+
return;
|
|
1746
|
+
} catch (err) {
|
|
1747
|
+
lastError = err?.message || lastError;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
openDownloadFailedModal(lastError);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1682
1754
|
function startDevicePolling() {
|
|
1683
1755
|
if (devicePollTimer) clearInterval(devicePollTimer);
|
|
1684
1756
|
void fetchDevices();
|
|
@@ -1710,6 +1782,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
1710
1782
|
document.querySelectorAll("[data-close-screenshot-modal]").forEach((el) => {
|
|
1711
1783
|
el.addEventListener("click", closeScreenshotModal);
|
|
1712
1784
|
});
|
|
1785
|
+
document.querySelectorAll("[data-close-download-failed-modal]").forEach((el) => {
|
|
1786
|
+
el.addEventListener("click", closeDownloadFailedModal);
|
|
1787
|
+
});
|
|
1713
1788
|
|
|
1714
1789
|
document.getElementById("addAssetBtn")?.addEventListener("click", () => {
|
|
1715
1790
|
if (uploadInProgress) return;
|
|
@@ -430,8 +430,10 @@ button.danger {
|
|
|
430
430
|
}
|
|
431
431
|
|
|
432
432
|
.playlist-assets-hint {
|
|
433
|
-
margin: 0
|
|
433
|
+
margin: 0;
|
|
434
|
+
padding: 0.65rem 1rem 0.75rem;
|
|
434
435
|
font-size: 0.78rem;
|
|
436
|
+
line-height: 1.45;
|
|
435
437
|
}
|
|
436
438
|
|
|
437
439
|
.playlist-item {
|
|
@@ -441,6 +443,8 @@ button.danger {
|
|
|
441
443
|
margin-bottom: 0.5rem;
|
|
442
444
|
background: #fafaf9;
|
|
443
445
|
position: relative;
|
|
446
|
+
min-width: 0;
|
|
447
|
+
overflow: hidden;
|
|
444
448
|
}
|
|
445
449
|
|
|
446
450
|
.playlist-item--dragging {
|
|
@@ -501,19 +505,34 @@ button.danger {
|
|
|
501
505
|
}
|
|
502
506
|
|
|
503
507
|
.playlist-item-actions {
|
|
504
|
-
display:
|
|
508
|
+
display: grid;
|
|
509
|
+
grid-template-columns: minmax(0, 4.25rem) minmax(0, 1fr) minmax(0, 1fr);
|
|
505
510
|
gap: 0.35rem;
|
|
506
|
-
align-items:
|
|
511
|
+
align-items: stretch;
|
|
512
|
+
width: 100%;
|
|
513
|
+
min-width: 0;
|
|
507
514
|
}
|
|
508
515
|
|
|
509
516
|
.playlist-item-actions input[type="number"] {
|
|
510
|
-
width:
|
|
517
|
+
width: 100%;
|
|
518
|
+
min-width: 0;
|
|
511
519
|
font: inherit;
|
|
512
|
-
padding: 0.
|
|
520
|
+
padding: 0.28rem 0.35rem;
|
|
513
521
|
border: 1px solid #d6d2cb;
|
|
514
522
|
border-radius: 4px;
|
|
515
523
|
}
|
|
516
524
|
|
|
525
|
+
.playlist-item-actions button {
|
|
526
|
+
min-width: 0;
|
|
527
|
+
padding: 0.28rem 0.35rem;
|
|
528
|
+
font-size: 0.72rem;
|
|
529
|
+
line-height: 1.2;
|
|
530
|
+
border-radius: 4px;
|
|
531
|
+
white-space: nowrap;
|
|
532
|
+
overflow: hidden;
|
|
533
|
+
text-overflow: ellipsis;
|
|
534
|
+
}
|
|
535
|
+
|
|
517
536
|
.publish-bar {
|
|
518
537
|
padding: 1rem 0 0;
|
|
519
538
|
}
|