@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] = useState3(false);
1267
1267
  const [showDeleteConfirm, setShowDeleteConfirm] = useState3(false);
1268
1268
  const [showProcessConfirm, setShowProcessConfirm] = useState3(false);
1269
+ const [showSyncConfirm, setShowSyncConfirm] = useState3(false);
1270
+ const [syncImageCount, setSyncImageCount] = useState3(0);
1269
1271
  const [showProgress, setShowProgress] = useState3(false);
1270
1272
  const [progressState, setProgressState] = useState3({
1271
1273
  current: 0,
@@ -1294,39 +1296,92 @@ function StudioToolbar() {
1294
1296
  const handleFileChange = useCallback(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 {
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) {
@@ -1578,58 +1633,129 @@ function StudioToolbar() {
1578
1633
  });
1579
1634
  }
1580
1635
  }, [selectedItems, clearSelection, triggerRefresh]);
1581
- const handleSyncCdn = useCallback(async () => {
1636
+ const handleSyncClick = useCallback(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 = p.split(".").pop()?.toLowerCase() || "";
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 = data.synced?.length || 0;
1601
- const errorCount = data.errors?.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 = useCallback(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 = p.split(".").pop()?.toLowerCase() || "";
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 (data.error?.includes("R2 not configured") || data.error?.includes("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 (data.error?.includes("R2 not configured") || data.error?.includes("CLOUDFLARE_R2")) {
1726
+ setShowProgress(false);
1727
+ setShowR2SetupModal(true);
1728
+ return;
1729
+ }
1730
+ errors++;
1731
+ } else if (data.synced?.length > 0) {
1732
+ synced++;
1733
+ } else if (data.errors?.length > 0) {
1734
+ errors++;
1735
+ }
1736
+ } catch {
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 = useCallback(async (folderName) => {
@@ -1740,6 +1866,16 @@ function StudioToolbar() {
1740
1866
  onCancel: () => setShowDeleteConfirm(false)
1741
1867
  }
1742
1868
  ),
1869
+ showSyncConfirm && /* @__PURE__ */ jsx4(
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__ */ jsx4(
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__ */ jsx4(CloudIcon, {}),
1904
- syncing ? "Syncing..." : "Sync CDN"
2040
+ "Sync CDN"
1905
2041
  ]
1906
2042
  }
1907
2043
  ),
@@ -4525,4 +4661,4 @@ export {
4525
4661
  StudioUI,
4526
4662
  StudioUI_default as default
4527
4663
  };
4528
- //# sourceMappingURL=StudioUI-PPX6VKNU.mjs.map
4664
+ //# sourceMappingURL=StudioUI-6HTM3QHM.mjs.map