@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
@@ -232,7 +232,7 @@ Allowed extras (optional): `"build-player": "tomorrowos build --platform tizen"`
232
232
  "node": ">=20"
233
233
  },
234
234
  "dependencies": {
235
- "@tomorrowos/sdk": "^0.8.4",
235
+ "@tomorrowos/sdk": "^0.8.6",
236
236
  "dotenv": "^17.2.3",
237
237
  "tsx": "^4.19.0"
238
238
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.8.4",
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.4",
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.4",
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 0 0.75rem;
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: flex;
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: center;
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: 5rem;
517
+ width: 100%;
518
+ min-width: 0;
511
519
  font: inherit;
512
- padding: 0.25rem 0.4rem;
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
  }