@gallop.software/studio 1.5.5 → 1.5.7

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.
@@ -1752,6 +1752,8 @@ function StudioToolbar() {
1752
1752
  const [syncImageCount, setSyncImageCount] = useState4(0);
1753
1753
  const [syncHasRemote, setSyncHasRemote] = useState4(false);
1754
1754
  const [syncHasLocal, setSyncHasLocal] = useState4(false);
1755
+ const [showDownloadConfirm, setShowDownloadConfirm] = useState4(false);
1756
+ const [downloadImageCount, setDownloadImageCount] = useState4(0);
1755
1757
  const [showProgress, setShowProgress] = useState4(false);
1756
1758
  const [progressTitle, setProgressTitle] = useState4("Processing Images");
1757
1759
  const [progressState, setProgressState] = useState4({
@@ -2181,32 +2183,43 @@ function StudioToolbar() {
2181
2183
  status: "processing",
2182
2184
  currentFile: imageKey.replace(/^\//, "")
2183
2185
  });
2184
- try {
2185
- const response = await fetch("/api/studio/sync", {
2186
- method: "POST",
2187
- headers: { "Content-Type": "application/json" },
2188
- body: JSON.stringify({ imageKeys: [imageKey] })
2189
- });
2190
- const data = await response.json();
2191
- if (!response.ok) {
2192
- if (data.error?.includes("R2 not configured") || data.error?.includes("CLOUDFLARE_R2")) {
2193
- setShowProgress(false);
2194
- setShowR2SetupModal(true);
2195
- return;
2186
+ let success = false;
2187
+ let lastError;
2188
+ for (let attempt = 0; attempt < 3 && !success; attempt++) {
2189
+ try {
2190
+ const response = await fetch("/api/studio/sync", {
2191
+ method: "POST",
2192
+ headers: { "Content-Type": "application/json" },
2193
+ body: JSON.stringify({ imageKeys: [imageKey] })
2194
+ });
2195
+ const data = await response.json();
2196
+ if (!response.ok) {
2197
+ if (data.error?.includes("R2 not configured") || data.error?.includes("CLOUDFLARE_R2")) {
2198
+ setShowProgress(false);
2199
+ setShowR2SetupModal(true);
2200
+ return;
2201
+ }
2202
+ lastError = data.error || `Failed: ${imageKey}`;
2203
+ } else if (data.pushed?.length > 0) {
2204
+ pushed++;
2205
+ success = true;
2206
+ } else if (data.errors?.length > 0) {
2207
+ for (const errMsg of data.errors) {
2208
+ lastError = errMsg;
2209
+ }
2210
+ } else {
2211
+ success = true;
2196
2212
  }
2197
- errors++;
2198
- errorMessages.push(data.error || `Failed: ${imageKey}`);
2199
- } else if (data.pushed?.length > 0) {
2200
- pushed++;
2201
- } else if (data.errors?.length > 0) {
2202
- errors++;
2203
- for (const errMsg of data.errors) {
2204
- errorMessages.push(errMsg);
2213
+ } catch (err) {
2214
+ lastError = `Network error: ${imageKey}`;
2215
+ if (attempt < 2) {
2216
+ await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
2205
2217
  }
2206
2218
  }
2207
- } catch (err) {
2219
+ }
2220
+ if (!success && lastError) {
2208
2221
  errors++;
2209
- errorMessages.push(`Network error: ${imageKey}`);
2222
+ errorMessages.push(lastError);
2210
2223
  }
2211
2224
  }
2212
2225
  setProgressState({
@@ -2231,6 +2244,106 @@ function StudioToolbar() {
2231
2244
  });
2232
2245
  }
2233
2246
  }, [selectedItems, clearSelection, triggerRefresh]);
2247
+ const handleDownloadClick = useCallback2(async () => {
2248
+ if (selectedItems.size === 0) return;
2249
+ const selectedPaths2 = Array.from(selectedItems);
2250
+ const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg", "ico", "bmp", "tiff", "tif"];
2251
+ const selectedImagePaths = selectedPaths2.filter((p) => {
2252
+ const ext = p.split(".").pop()?.toLowerCase() || "";
2253
+ return imageExtensions.includes(ext);
2254
+ });
2255
+ if (selectedImagePaths.length === 0) {
2256
+ setAlertMessage({
2257
+ title: "No Images Found",
2258
+ message: "No images found in the selected items."
2259
+ });
2260
+ return;
2261
+ }
2262
+ setDownloadImageCount(selectedImagePaths.length);
2263
+ setShowDownloadConfirm(true);
2264
+ }, [selectedItems]);
2265
+ const handleDownloadConfirm = useCallback2(async () => {
2266
+ setShowDownloadConfirm(false);
2267
+ const selectedPaths2 = Array.from(selectedItems);
2268
+ const imageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "svg", "ico", "bmp", "tiff", "tif"];
2269
+ const selectedImagePaths = selectedPaths2.filter((p) => {
2270
+ const ext = p.split(".").pop()?.toLowerCase() || "";
2271
+ return imageExtensions.includes(ext);
2272
+ });
2273
+ const imageKeys = selectedImagePaths.map((p) => "/" + p.replace(/^public\//, ""));
2274
+ setProgressTitle("Downloading from CDN");
2275
+ setShowProgress(true);
2276
+ setProgressState({
2277
+ current: 0,
2278
+ total: imageKeys.length,
2279
+ percent: 0,
2280
+ status: "processing"
2281
+ });
2282
+ try {
2283
+ const response = await fetch("/api/studio/download-stream", {
2284
+ method: "POST",
2285
+ headers: { "Content-Type": "application/json" },
2286
+ body: JSON.stringify({ imageKeys })
2287
+ });
2288
+ if (!response.ok || !response.body) {
2289
+ throw new Error("Download request failed");
2290
+ }
2291
+ const reader = response.body.getReader();
2292
+ const decoder = new TextDecoder();
2293
+ let buffer = "";
2294
+ while (true) {
2295
+ const { done, value } = await reader.read();
2296
+ if (done) break;
2297
+ buffer += decoder.decode(value, { stream: true });
2298
+ const lines = buffer.split("\n");
2299
+ buffer = lines.pop() || "";
2300
+ for (const line of lines) {
2301
+ if (line.startsWith("data: ")) {
2302
+ try {
2303
+ const data = JSON.parse(line.slice(6));
2304
+ if (data.type === "progress") {
2305
+ setProgressState({
2306
+ current: data.current,
2307
+ total: data.total,
2308
+ percent: Math.round(data.current / data.total * 100),
2309
+ status: "processing",
2310
+ message: data.message
2311
+ });
2312
+ } else if (data.type === "complete") {
2313
+ setProgressState({
2314
+ current: data.total || imageKeys.length,
2315
+ total: data.total || imageKeys.length,
2316
+ percent: 100,
2317
+ status: "complete",
2318
+ message: data.message
2319
+ });
2320
+ } else if (data.type === "error") {
2321
+ setProgressState({
2322
+ current: 0,
2323
+ total: 0,
2324
+ percent: 0,
2325
+ status: "error",
2326
+ message: data.message
2327
+ });
2328
+ }
2329
+ } catch {
2330
+ }
2331
+ }
2332
+ }
2333
+ }
2334
+ clearSelection();
2335
+ triggerRefresh();
2336
+ } catch (error) {
2337
+ console.error("Download error:", error);
2338
+ setProgressState({
2339
+ current: 0,
2340
+ total: 0,
2341
+ percent: 0,
2342
+ status: "error",
2343
+ message: "Failed to download from CDN."
2344
+ });
2345
+ }
2346
+ }, [selectedItems, clearSelection, triggerRefresh]);
2234
2347
  const handleCreateFolder = useCallback2(async (folderName) => {
2235
2348
  setShowNewFolderModal(false);
2236
2349
  try {
@@ -2349,6 +2462,10 @@ function StudioToolbar() {
2349
2462
  const item = fileItems.find((f) => f.path === path);
2350
2463
  return item && item.cdnPushed && !item.isRemote;
2351
2464
  });
2465
+ const allR2Selection = hasSelection && Array.from(selectedItems).every((path) => {
2466
+ const item = fileItems.find((f) => f.path === path);
2467
+ return item && item.type === "file" && item.cdnPushed && !item.isRemote;
2468
+ });
2352
2469
  const selectedPaths = Array.from(selectedItems);
2353
2470
  const singleFolderSelected = selectedPaths.length === 1 && !selectedPaths[0].includes(".");
2354
2471
  const selectedFolderPath = singleFolderSelected ? selectedPaths[0] : null;
@@ -2395,6 +2512,16 @@ function StudioToolbar() {
2395
2512
  onCancel: () => setShowSyncConfirm(false)
2396
2513
  }
2397
2514
  ),
2515
+ showDownloadConfirm && /* @__PURE__ */ jsx5(
2516
+ ConfirmModal,
2517
+ {
2518
+ title: "Download from CDN",
2519
+ message: `Download ${downloadImageCount} image${downloadImageCount !== 1 ? "s" : ""} from Cloudflare R2 to local storage? Images will be removed from the CDN.`,
2520
+ confirmLabel: "Download",
2521
+ onConfirm: handleDownloadConfirm,
2522
+ onCancel: () => setShowDownloadConfirm(false)
2523
+ }
2524
+ ),
2398
2525
  showProgress && /* @__PURE__ */ jsx5(
2399
2526
  ProgressModal,
2400
2527
  {
@@ -2550,7 +2677,18 @@ function StudioToolbar() {
2550
2677
  ]
2551
2678
  }
2552
2679
  ),
2553
- /* @__PURE__ */ jsxs5(
2680
+ allR2Selection ? /* @__PURE__ */ jsxs5(
2681
+ "button",
2682
+ {
2683
+ css: styles5.btn,
2684
+ onClick: handleDownloadClick,
2685
+ disabled: !hasSelection,
2686
+ children: [
2687
+ /* @__PURE__ */ jsx5(CloudDownloadIcon, {}),
2688
+ "Download"
2689
+ ]
2690
+ }
2691
+ ) : /* @__PURE__ */ jsxs5(
2554
2692
  "button",
2555
2693
  {
2556
2694
  css: styles5.btn,
@@ -2649,6 +2787,9 @@ function MoveIcon() {
2649
2787
  function CloudIcon() {
2650
2788
  return /* @__PURE__ */ jsx5("svg", { css: styles5.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx5("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" }) });
2651
2789
  }
2790
+ function CloudDownloadIcon() {
2791
+ return /* @__PURE__ */ jsx5("svg", { css: styles5.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx5("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" }) });
2792
+ }
2652
2793
  function GridIcon() {
2653
2794
  return /* @__PURE__ */ jsx5("svg", { css: styles5.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx5("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" }) });
2654
2795
  }
@@ -6352,4 +6493,4 @@ export {
6352
6493
  StudioUI,
6353
6494
  StudioUI_default as default
6354
6495
  };
6355
- //# sourceMappingURL=StudioUI-VIYONKHA.mjs.map
6496
+ //# sourceMappingURL=StudioUI-6Q7GX6IY.mjs.map