@gallop.software/studio 0.1.85 → 0.1.87

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.
@@ -1266,6 +1266,8 @@ function StudioToolbar() {
1266
1266
  const [processing, setProcessing] = _react.useState.call(void 0, false);
1267
1267
  const [showDeleteConfirm, setShowDeleteConfirm] = _react.useState.call(void 0, false);
1268
1268
  const [showProcessConfirm, setShowProcessConfirm] = _react.useState.call(void 0, false);
1269
+ const [showSyncConfirm, setShowSyncConfirm] = _react.useState.call(void 0, false);
1270
+ const [syncImageCount, setSyncImageCount] = _react.useState.call(void 0, 0);
1269
1271
  const [showProgress, setShowProgress] = _react.useState.call(void 0, false);
1270
1272
  const [progressState, setProgressState] = _react.useState.call(void 0, {
1271
1273
  current: 0,
@@ -1294,39 +1296,92 @@ function StudioToolbar() {
1294
1296
  const handleFileChange = _react.useCallback.call(void 0, async (e) => {
1295
1297
  const files = e.target.files;
1296
1298
  if (!files || files.length === 0) return;
1297
- setUploading(true);
1299
+ const fileList = Array.from(files);
1300
+ if (fileList.length > 1) {
1301
+ setProgressState({
1302
+ current: 0,
1303
+ total: fileList.length,
1304
+ percent: 0,
1305
+ status: "processing",
1306
+ message: "Uploading files..."
1307
+ });
1308
+ setShowProgress(true);
1309
+ } else {
1310
+ setUploading(true);
1311
+ }
1312
+ let uploaded = 0;
1313
+ let errors = 0;
1298
1314
  try {
1299
- for (const file of Array.from(files)) {
1315
+ for (let i = 0; i < fileList.length; i++) {
1316
+ const file = fileList[i];
1317
+ if (fileList.length > 1) {
1318
+ setProgressState({
1319
+ current: i + 1,
1320
+ total: fileList.length,
1321
+ percent: Math.round((i + 1) / fileList.length * 100),
1322
+ status: "processing",
1323
+ currentFile: file.name
1324
+ });
1325
+ }
1300
1326
  const formData = new FormData();
1301
1327
  formData.append("file", file);
1302
1328
  formData.append("path", currentPath);
1303
- const response = await fetch("/api/studio/upload", {
1304
- method: "POST",
1305
- body: formData
1306
- });
1307
- if (!response.ok) {
1308
- const error = await response.json();
1309
- if (response.status >= 500) {
1310
- console.error("Upload error:", error);
1311
- setAlertMessage({
1312
- title: "Upload Failed",
1313
- message: `Failed to upload ${file.name}: ${error.error || "Unknown error"}`
1314
- });
1329
+ try {
1330
+ const response = await fetch("/api/studio/upload", {
1331
+ method: "POST",
1332
+ body: formData
1333
+ });
1334
+ if (!response.ok) {
1335
+ const error = await response.json();
1336
+ errors++;
1337
+ if (fileList.length === 1) {
1338
+ if (response.status >= 500) {
1339
+ console.error("Upload error:", error);
1340
+ setAlertMessage({
1341
+ title: "Upload Failed",
1342
+ message: `Failed to upload ${file.name}: ${error.error || "Unknown error"}`
1343
+ });
1344
+ } else {
1345
+ setAlertMessage({
1346
+ title: "Cannot Upload Here",
1347
+ message: error.error || "Upload not allowed in this location."
1348
+ });
1349
+ }
1350
+ }
1315
1351
  } else {
1316
- setAlertMessage({
1317
- title: "Cannot Upload Here",
1318
- message: error.error || "Upload not allowed in this location."
1319
- });
1352
+ uploaded++;
1320
1353
  }
1354
+ } catch (e2) {
1355
+ errors++;
1321
1356
  }
1322
1357
  }
1358
+ if (fileList.length > 1) {
1359
+ setProgressState({
1360
+ current: fileList.length,
1361
+ total: fileList.length,
1362
+ percent: 100,
1363
+ status: "complete",
1364
+ processed: uploaded,
1365
+ errors
1366
+ });
1367
+ }
1323
1368
  triggerRefresh();
1324
1369
  } catch (error) {
1325
1370
  console.error("Upload error:", error);
1326
- setAlertMessage({
1327
- title: "Upload Failed",
1328
- message: "Upload failed. Check console for details."
1329
- });
1371
+ if (fileList.length > 1) {
1372
+ setProgressState({
1373
+ current: 0,
1374
+ total: 0,
1375
+ percent: 0,
1376
+ status: "error",
1377
+ message: "Upload failed."
1378
+ });
1379
+ } else {
1380
+ setAlertMessage({
1381
+ title: "Upload Failed",
1382
+ message: "Upload failed. Check console for details."
1383
+ });
1384
+ }
1330
1385
  } finally {
1331
1386
  setUploading(false);
1332
1387
  if (fileInputRef.current) {
@@ -1467,7 +1522,7 @@ function StudioToolbar() {
1467
1522
  message: data.message
1468
1523
  }));
1469
1524
  }
1470
- } catch (e2) {
1525
+ } catch (e3) {
1471
1526
  }
1472
1527
  }
1473
1528
  }
@@ -1578,58 +1633,129 @@ function StudioToolbar() {
1578
1633
  });
1579
1634
  }
1580
1635
  }, [selectedItems, clearSelection, triggerRefresh]);
1581
- const handleSyncCdn = _react.useCallback.call(void 0, async () => {
1636
+ const handleSyncClick = _react.useCallback.call(void 0, async () => {
1582
1637
  if (selectedItems.size === 0) return;
1583
- const imageKeys = Array.from(selectedItems).filter((p) => !p.endsWith("/")).map((p) => "/" + p.replace(/^public\//, ""));
1584
- if (imageKeys.length === 0) {
1638
+ const selectedPaths2 = Array.from(selectedItems);
1639
+ const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg", "ico", "bmp", "tiff", "tif"];
1640
+ const selectedImagePaths = selectedPaths2.filter((p) => {
1641
+ const ext = _optionalChain([p, 'access', _19 => _19.split, 'call', _20 => _20("."), 'access', _21 => _21.pop, 'call', _22 => _22(), 'optionalAccess', _23 => _23.toLowerCase, 'call', _24 => _24()]) || "";
1642
+ return imageExtensions.includes(ext);
1643
+ });
1644
+ const selectedFolders = selectedPaths2.filter((p) => !p.includes(".") || p.endsWith("/"));
1645
+ if (selectedFolders.length > 0) {
1646
+ try {
1647
+ const response = await fetch(`/api/studio/folder-images?folders=${encodeURIComponent(selectedFolders.join(","))}`);
1648
+ const data = await response.json();
1649
+ if (data.images) {
1650
+ for (const img of data.images) {
1651
+ const fullPath = `public/${img}`;
1652
+ if (!selectedImagePaths.includes(fullPath)) {
1653
+ selectedImagePaths.push(fullPath);
1654
+ }
1655
+ }
1656
+ }
1657
+ } catch (error) {
1658
+ console.error("Failed to get folder images:", error);
1659
+ }
1660
+ }
1661
+ if (selectedImagePaths.length === 0) {
1585
1662
  setAlertMessage({
1586
- title: "No Images Selected",
1587
- message: "Please select image files to sync to CDN."
1663
+ title: "No Images Found",
1664
+ message: "No images found in the selected items."
1588
1665
  });
1589
1666
  return;
1590
1667
  }
1591
- setSyncing(true);
1592
- try {
1593
- const response = await fetch("/api/studio/sync", {
1594
- method: "POST",
1595
- headers: { "Content-Type": "application/json" },
1596
- body: JSON.stringify({ imageKeys })
1597
- });
1598
- const data = await response.json();
1599
- if (response.ok) {
1600
- const syncedCount = _optionalChain([data, 'access', _19 => _19.synced, 'optionalAccess', _20 => _20.length]) || 0;
1601
- const errorCount = _optionalChain([data, 'access', _21 => _21.errors, 'optionalAccess', _22 => _22.length]) || 0;
1602
- if (errorCount > 0) {
1603
- setAlertMessage({
1604
- title: "Sync Partially Complete",
1605
- message: `Synced ${syncedCount} images. ${errorCount} failed.`
1606
- });
1607
- } else {
1608
- setAlertMessage({
1609
- title: "Sync Complete",
1610
- message: `Successfully synced ${syncedCount} images to CDN.`
1611
- });
1668
+ setSyncImageCount(selectedImagePaths.length);
1669
+ setShowSyncConfirm(true);
1670
+ }, [selectedItems]);
1671
+ const handleSyncConfirm = _react.useCallback.call(void 0, async () => {
1672
+ setShowSyncConfirm(false);
1673
+ const selectedPaths2 = Array.from(selectedItems);
1674
+ const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg", "ico", "bmp", "tiff", "tif"];
1675
+ const selectedImagePaths = selectedPaths2.filter((p) => {
1676
+ const ext = _optionalChain([p, 'access', _25 => _25.split, 'call', _26 => _26("."), 'access', _27 => _27.pop, 'call', _28 => _28(), 'optionalAccess', _29 => _29.toLowerCase, 'call', _30 => _30()]) || "";
1677
+ return imageExtensions.includes(ext);
1678
+ });
1679
+ const selectedFolders = selectedPaths2.filter((p) => !p.includes(".") || p.endsWith("/"));
1680
+ if (selectedFolders.length > 0) {
1681
+ try {
1682
+ const response = await fetch(`/api/studio/folder-images?folders=${encodeURIComponent(selectedFolders.join(","))}`);
1683
+ const data = await response.json();
1684
+ if (data.images) {
1685
+ for (const img of data.images) {
1686
+ const fullPath = `public/${img}`;
1687
+ if (!selectedImagePaths.includes(fullPath)) {
1688
+ selectedImagePaths.push(fullPath);
1689
+ }
1690
+ }
1612
1691
  }
1613
- clearSelection();
1614
- triggerRefresh();
1615
- } else {
1616
- if (_optionalChain([data, 'access', _23 => _23.error, 'optionalAccess', _24 => _24.includes, 'call', _25 => _25("R2 not configured")]) || _optionalChain([data, 'access', _26 => _26.error, 'optionalAccess', _27 => _27.includes, 'call', _28 => _28("CLOUDFLARE_R2")])) {
1617
- setShowR2SetupModal(true);
1618
- } else {
1619
- setAlertMessage({
1620
- title: "Sync Failed",
1621
- message: data.error || "Failed to sync to CDN."
1692
+ } catch (error) {
1693
+ console.error("Failed to get folder images:", error);
1694
+ }
1695
+ }
1696
+ const imageKeys = selectedImagePaths.map((p) => "/" + p.replace(/^public\//, ""));
1697
+ setProgressState({
1698
+ current: 0,
1699
+ total: imageKeys.length,
1700
+ percent: 0,
1701
+ status: "processing",
1702
+ message: "Syncing to CDN..."
1703
+ });
1704
+ setShowProgress(true);
1705
+ let synced = 0;
1706
+ let errors = 0;
1707
+ try {
1708
+ for (let i = 0; i < imageKeys.length; i++) {
1709
+ const imageKey = imageKeys[i];
1710
+ setProgressState({
1711
+ current: i + 1,
1712
+ total: imageKeys.length,
1713
+ percent: Math.round((i + 1) / imageKeys.length * 100),
1714
+ status: "processing",
1715
+ currentFile: imageKey.replace(/^\//, "")
1716
+ });
1717
+ try {
1718
+ const response = await fetch("/api/studio/sync", {
1719
+ method: "POST",
1720
+ headers: { "Content-Type": "application/json" },
1721
+ body: JSON.stringify({ imageKeys: [imageKey] })
1622
1722
  });
1723
+ const data = await response.json();
1724
+ if (!response.ok) {
1725
+ if (_optionalChain([data, 'access', _31 => _31.error, 'optionalAccess', _32 => _32.includes, 'call', _33 => _33("R2 not configured")]) || _optionalChain([data, 'access', _34 => _34.error, 'optionalAccess', _35 => _35.includes, 'call', _36 => _36("CLOUDFLARE_R2")])) {
1726
+ setShowProgress(false);
1727
+ setShowR2SetupModal(true);
1728
+ return;
1729
+ }
1730
+ errors++;
1731
+ } else if (_optionalChain([data, 'access', _37 => _37.synced, 'optionalAccess', _38 => _38.length]) > 0) {
1732
+ synced++;
1733
+ } else if (_optionalChain([data, 'access', _39 => _39.errors, 'optionalAccess', _40 => _40.length]) > 0) {
1734
+ errors++;
1735
+ }
1736
+ } catch (e4) {
1737
+ errors++;
1623
1738
  }
1624
1739
  }
1740
+ setProgressState({
1741
+ current: imageKeys.length,
1742
+ total: imageKeys.length,
1743
+ percent: 100,
1744
+ status: "complete",
1745
+ processed: synced,
1746
+ errors
1747
+ });
1748
+ clearSelection();
1749
+ triggerRefresh();
1625
1750
  } catch (error) {
1626
1751
  console.error("Sync error:", error);
1627
- setAlertMessage({
1628
- title: "Sync Failed",
1629
- message: "Failed to sync to CDN. Check console for details."
1752
+ setProgressState({
1753
+ current: 0,
1754
+ total: 0,
1755
+ percent: 0,
1756
+ status: "error",
1757
+ message: "Failed to sync to CDN."
1630
1758
  });
1631
- } finally {
1632
- setSyncing(false);
1633
1759
  }
1634
1760
  }, [selectedItems, clearSelection, triggerRefresh]);
1635
1761
  const handleCreateFolder = _react.useCallback.call(void 0, async (folderName) => {
@@ -1740,6 +1866,16 @@ function StudioToolbar() {
1740
1866
  onCancel: () => setShowDeleteConfirm(false)
1741
1867
  }
1742
1868
  ),
1869
+ showSyncConfirm && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
1870
+ ConfirmModal,
1871
+ {
1872
+ title: "Sync to CDN",
1873
+ message: `Sync ${syncImageCount} image${syncImageCount !== 1 ? "s" : ""} to Cloudflare R2? Images must be processed first. After syncing, local thumbnails will be deleted.`,
1874
+ confirmLabel: "Sync",
1875
+ onConfirm: handleSyncConfirm,
1876
+ onCancel: () => setShowSyncConfirm(false)
1877
+ }
1878
+ ),
1743
1879
  showProcessConfirm && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
1744
1880
  ConfirmModal,
1745
1881
  {
@@ -1897,11 +2033,11 @@ function StudioToolbar() {
1897
2033
  "button",
1898
2034
  {
1899
2035
  css: styles4.btn,
1900
- onClick: handleSyncCdn,
1901
- disabled: !hasSelection || syncing,
2036
+ onClick: handleSyncClick,
2037
+ disabled: !hasSelection,
1902
2038
  children: [
1903
2039
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, CloudIcon, {}),
1904
- syncing ? "Syncing..." : "Sync CDN"
2040
+ "Sync CDN"
1905
2041
  ]
1906
2042
  }
1907
2043
  ),
@@ -3539,7 +3675,7 @@ function StudioDetailView() {
3539
3675
  });
3540
3676
  triggerRefresh();
3541
3677
  } else {
3542
- if (_optionalChain([data, 'access', _29 => _29.error, 'optionalAccess', _30 => _30.includes, 'call', _31 => _31("R2 not configured")]) || _optionalChain([data, 'access', _32 => _32.error, 'optionalAccess', _33 => _33.includes, 'call', _34 => _34("CLOUDFLARE_R2")])) {
3678
+ if (_optionalChain([data, 'access', _41 => _41.error, 'optionalAccess', _42 => _42.includes, 'call', _43 => _43("R2 not configured")]) || _optionalChain([data, 'access', _44 => _44.error, 'optionalAccess', _45 => _45.includes, 'call', _46 => _46("CLOUDFLARE_R2")])) {
3543
3679
  setShowR2SetupModal(true);
3544
3680
  } else {
3545
3681
  setAlertMessage({
@@ -3578,7 +3714,7 @@ function StudioDetailView() {
3578
3714
  if (!response.ok) {
3579
3715
  throw new Error("Processing failed");
3580
3716
  }
3581
- const reader = _optionalChain([response, 'access', _35 => _35.body, 'optionalAccess', _36 => _36.getReader, 'call', _37 => _37()]);
3717
+ const reader = _optionalChain([response, 'access', _47 => _47.body, 'optionalAccess', _48 => _48.getReader, 'call', _49 => _49()]);
3582
3718
  if (!reader) {
3583
3719
  throw new Error("No response body");
3584
3720
  }
@@ -3595,7 +3731,7 @@ function StudioDetailView() {
3595
3731
  try {
3596
3732
  const data = JSON.parse(line.slice(6));
3597
3733
  setProcessProgress(data);
3598
- } catch (e3) {
3734
+ } catch (e5) {
3599
3735
  }
3600
3736
  }
3601
3737
  }
@@ -4525,4 +4661,4 @@ var StudioUI_default = StudioUI;
4525
4661
 
4526
4662
 
4527
4663
  exports.StudioUI = StudioUI; exports.default = StudioUI_default;
4528
- //# sourceMappingURL=StudioUI-P4TLZNF7.js.map
4664
+ //# sourceMappingURL=StudioUI-ZBSTYTUV.js.map