@tomorrowos/sdk 0.8.4 → 0.8.5

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.5",
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.5",
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.5",
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.5",
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,6 +808,12 @@ 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.textContent = "Download";
814
+ downloadBtn.title = "Download asset";
815
+ downloadBtn.addEventListener("click", () => void downloadMediaAsset(item));
816
+
811
817
  const removeBtn = document.createElement("button");
812
818
  removeBtn.type = "button";
813
819
  removeBtn.className = "danger";
@@ -818,6 +824,7 @@ function renderEditorAssets() {
818
824
  });
819
825
 
820
826
  actions.appendChild(durInput);
827
+ actions.appendChild(downloadBtn);
821
828
  actions.appendChild(removeBtn);
822
829
  li.appendChild(name);
823
830
  li.appendChild(meta);
@@ -1679,6 +1686,56 @@ function closeScreenshotModal() {
1679
1686
  modal?.classList.add("hidden");
1680
1687
  }
1681
1688
 
1689
+ function openDownloadFailedModal(detail) {
1690
+ const modal = document.getElementById("downloadFailedModal");
1691
+ const message = document.getElementById("downloadFailedModalMessage");
1692
+ if (!modal) return;
1693
+ if (message) {
1694
+ message.textContent = detail
1695
+ ? `Could not download this asset: ${detail}`
1696
+ : "Could not download this asset.";
1697
+ }
1698
+ modal.classList.remove("hidden");
1699
+ }
1700
+
1701
+ function closeDownloadFailedModal() {
1702
+ document.getElementById("downloadFailedModal")?.classList.add("hidden");
1703
+ }
1704
+
1705
+ function sanitizeDownloadFilename(name) {
1706
+ const base = String(name || "asset").trim() || "asset";
1707
+ return base.replace(/[<>:"/\\|?*\x00-\x1f]/g, "_");
1708
+ }
1709
+
1710
+ async function downloadMediaAsset(item) {
1711
+ let url;
1712
+ try {
1713
+ url = absoluteMediaUrl(item.url);
1714
+ } catch (err) {
1715
+ openDownloadFailedModal(err?.message || "Media URL is not available.");
1716
+ return;
1717
+ }
1718
+
1719
+ const filename = sanitizeDownloadFilename(item.name || item.url.split("/").pop() || "asset");
1720
+
1721
+ try {
1722
+ const res = await fetch(url);
1723
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1724
+ const blob = await res.blob();
1725
+ const objectUrl = URL.createObjectURL(blob);
1726
+ const anchor = document.createElement("a");
1727
+ anchor.href = objectUrl;
1728
+ anchor.download = filename;
1729
+ anchor.style.display = "none";
1730
+ document.body.appendChild(anchor);
1731
+ anchor.click();
1732
+ anchor.remove();
1733
+ URL.revokeObjectURL(objectUrl);
1734
+ } catch (err) {
1735
+ openDownloadFailedModal(err?.message || "Network or browser error.");
1736
+ }
1737
+ }
1738
+
1682
1739
  function startDevicePolling() {
1683
1740
  if (devicePollTimer) clearInterval(devicePollTimer);
1684
1741
  void fetchDevices();
@@ -1710,6 +1767,9 @@ document.addEventListener("DOMContentLoaded", () => {
1710
1767
  document.querySelectorAll("[data-close-screenshot-modal]").forEach((el) => {
1711
1768
  el.addEventListener("click", closeScreenshotModal);
1712
1769
  });
1770
+ document.querySelectorAll("[data-close-download-failed-modal]").forEach((el) => {
1771
+ el.addEventListener("click", closeDownloadFailedModal);
1772
+ });
1713
1773
 
1714
1774
  document.getElementById("addAssetBtn")?.addEventListener("click", () => {
1715
1775
  if (uploadInProgress) return;