@gallop.software/studio 0.1.24 → 0.1.25

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.
@@ -62,7 +62,7 @@ import { css as css2, keyframes as keyframes2 } from "@emotion/react";
62
62
 
63
63
  // src/components/StudioModal.tsx
64
64
  import { css, keyframes } from "@emotion/react";
65
- import { jsx, jsxs } from "@emotion/react/jsx-runtime";
65
+ import { Fragment, jsx, jsxs } from "@emotion/react/jsx-runtime";
66
66
  var fadeIn = keyframes`
67
67
  from { opacity: 0; }
68
68
  to { opacity: 1; }
@@ -204,9 +204,101 @@ function AlertModal({
204
204
  /* @__PURE__ */ jsx("div", { css: styles.footer, children: /* @__PURE__ */ jsx("button", { css: [styles.btn, styles.btnConfirm], onClick: onClose, children: buttonLabel }) })
205
205
  ] }) });
206
206
  }
207
+ var progressStyles = {
208
+ progressContainer: css`
209
+ margin-top: 16px;
210
+ `,
211
+ progressBar: css`
212
+ width: 100%;
213
+ height: 8px;
214
+ background-color: ${colors.background};
215
+ border-radius: 4px;
216
+ overflow: hidden;
217
+ margin-bottom: 12px;
218
+ `,
219
+ progressFill: css`
220
+ height: 100%;
221
+ background: linear-gradient(90deg, ${colors.primary}, ${colors.primaryHover});
222
+ border-radius: 4px;
223
+ transition: width 0.3s ease;
224
+ `,
225
+ progressText: css`
226
+ font-size: ${fontSize.sm};
227
+ color: ${colors.textSecondary};
228
+ margin: 0;
229
+ display: flex;
230
+ justify-content: space-between;
231
+ align-items: center;
232
+ `,
233
+ currentFile: css`
234
+ font-size: ${fontSize.xs};
235
+ color: ${colors.textMuted};
236
+ margin: 8px 0 0;
237
+ white-space: nowrap;
238
+ overflow: hidden;
239
+ text-overflow: ellipsis;
240
+ `
241
+ };
242
+ function ProgressModal({
243
+ title,
244
+ progress,
245
+ onClose
246
+ }) {
247
+ const isComplete = progress.status === "complete";
248
+ const isError = progress.status === "error";
249
+ const canClose = isComplete || isError;
250
+ return /* @__PURE__ */ jsx("div", { css: styles.overlay, onClick: canClose ? onClose : void 0, children: /* @__PURE__ */ jsxs("div", { css: styles.modal, onClick: (e) => e.stopPropagation(), children: [
251
+ /* @__PURE__ */ jsx("div", { css: styles.header, children: /* @__PURE__ */ jsx("h3", { css: styles.title, children: title }) }),
252
+ /* @__PURE__ */ jsx("div", { css: styles.body, children: isError ? /* @__PURE__ */ jsx("p", { css: styles.message, children: progress.message || "An error occurred" }) : isComplete ? /* @__PURE__ */ jsxs("p", { css: styles.message, children: [
253
+ "Processed ",
254
+ progress.processed,
255
+ " image",
256
+ progress.processed !== 1 ? "s" : "",
257
+ ".",
258
+ progress.orphansRemoved && progress.orphansRemoved > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
259
+ " Removed ",
260
+ progress.orphansRemoved,
261
+ " orphaned thumbnail",
262
+ progress.orphansRemoved !== 1 ? "s" : "",
263
+ "."
264
+ ] }),
265
+ progress.errors && progress.errors > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
266
+ " ",
267
+ progress.errors,
268
+ " error",
269
+ progress.errors !== 1 ? "s" : "",
270
+ " occurred."
271
+ ] })
272
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
273
+ /* @__PURE__ */ jsx("p", { css: styles.message, children: progress.status === "cleanup" ? "Cleaning up orphaned files..." : `Processing images...` }),
274
+ /* @__PURE__ */ jsxs("div", { css: progressStyles.progressContainer, children: [
275
+ /* @__PURE__ */ jsx("div", { css: progressStyles.progressBar, children: /* @__PURE__ */ jsx(
276
+ "div",
277
+ {
278
+ css: progressStyles.progressFill,
279
+ style: { width: `${progress.percent}%` }
280
+ }
281
+ ) }),
282
+ /* @__PURE__ */ jsxs("div", { css: progressStyles.progressText, children: [
283
+ /* @__PURE__ */ jsxs("span", { children: [
284
+ progress.current,
285
+ " of ",
286
+ progress.total
287
+ ] }),
288
+ /* @__PURE__ */ jsxs("span", { children: [
289
+ progress.percent,
290
+ "%"
291
+ ] })
292
+ ] }),
293
+ progress.currentFile && /* @__PURE__ */ jsx("p", { css: progressStyles.currentFile, title: progress.currentFile, children: progress.currentFile })
294
+ ] })
295
+ ] }) }),
296
+ canClose && /* @__PURE__ */ jsx("div", { css: styles.footer, children: /* @__PURE__ */ jsx("button", { css: [styles.btn, styles.btnConfirm], onClick: onClose, children: "Done" }) })
297
+ ] }) });
298
+ }
207
299
 
208
300
  // src/components/StudioToolbar.tsx
209
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "@emotion/react/jsx-runtime";
301
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "@emotion/react/jsx-runtime";
210
302
  var btnHeight = "36px";
211
303
  var spin = keyframes2`
212
304
  to { transform: rotate(360deg); }
@@ -351,6 +443,13 @@ function StudioToolbar() {
351
443
  const [processing, setProcessing] = useState(false);
352
444
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
353
445
  const [showProcessConfirm, setShowProcessConfirm] = useState(false);
446
+ const [showProgress, setShowProgress] = useState(false);
447
+ const [progressState, setProgressState] = useState({
448
+ current: 0,
449
+ total: 0,
450
+ percent: 0,
451
+ status: "processing"
452
+ });
354
453
  const [processCount, setProcessCount] = useState(0);
355
454
  const [processMode, setProcessMode] = useState("all");
356
455
  const [alertMessage, setAlertMessage] = useState(null);
@@ -425,12 +524,12 @@ function StudioToolbar() {
425
524
  setShowProcessConfirm(true);
426
525
  } else {
427
526
  try {
428
- const response = await fetch("/api/studio/count-unprocessed");
527
+ const response = await fetch("/api/studio/count-images");
429
528
  const data = await response.json();
430
529
  if (data.count === 0) {
431
530
  setAlertMessage({
432
- title: "All Images Processed",
433
- message: "All images in the public folder have already been processed."
531
+ title: "No Images Found",
532
+ message: "No images found in the public folder to process."
434
533
  });
435
534
  return;
436
535
  }
@@ -438,10 +537,10 @@ function StudioToolbar() {
438
537
  setProcessMode("all");
439
538
  setShowProcessConfirm(true);
440
539
  } catch (error) {
441
- console.error("Failed to count unprocessed images:", error);
540
+ console.error("Failed to count images:", error);
442
541
  setAlertMessage({
443
542
  title: "Error",
444
- message: "Failed to count unprocessed images."
543
+ message: "Failed to count images."
445
544
  });
446
545
  }
447
546
  }
@@ -451,28 +550,78 @@ function StudioToolbar() {
451
550
  setProcessing(true);
452
551
  try {
453
552
  if (processMode === "all") {
553
+ setShowProgress(true);
554
+ setProgressState({
555
+ current: 0,
556
+ total: processCount,
557
+ percent: 0,
558
+ status: "processing"
559
+ });
454
560
  const response = await fetch("/api/studio/process-all", {
455
561
  method: "POST"
456
562
  });
457
- const data = await response.json();
458
- if (response.ok) {
459
- const message = [
460
- `Processed ${data.processed?.length || 0} images.`,
461
- data.orphansRemoved?.length > 0 ? `Removed ${data.orphansRemoved.length} orphaned thumbnails.` : "",
462
- data.errors?.length > 0 ? `${data.errors.length} errors occurred.` : ""
463
- ].filter(Boolean).join(" ");
464
- setAlertMessage({
465
- title: "Processing Complete",
466
- message
467
- });
468
- triggerRefresh();
469
- } else {
470
- setAlertMessage({
471
- title: "Processing Failed",
472
- message: data.error || "Unknown error"
473
- });
563
+ if (!response.body) {
564
+ throw new Error("No response body");
565
+ }
566
+ const reader = response.body.getReader();
567
+ const decoder = new TextDecoder();
568
+ while (true) {
569
+ const { done, value } = await reader.read();
570
+ if (done) break;
571
+ const text = decoder.decode(value);
572
+ const lines = text.split("\n\n").filter((line) => line.startsWith("data: "));
573
+ for (const line of lines) {
574
+ try {
575
+ const data = JSON.parse(line.replace("data: ", ""));
576
+ if (data.type === "start") {
577
+ setProgressState((prev) => ({
578
+ ...prev,
579
+ total: data.total
580
+ }));
581
+ } else if (data.type === "progress") {
582
+ setProgressState({
583
+ current: data.current,
584
+ total: data.total,
585
+ percent: data.percent,
586
+ currentFile: data.currentFile,
587
+ status: "processing"
588
+ });
589
+ } else if (data.type === "cleanup") {
590
+ setProgressState((prev) => ({
591
+ ...prev,
592
+ status: "cleanup",
593
+ currentFile: void 0
594
+ }));
595
+ } else if (data.type === "complete") {
596
+ setProgressState({
597
+ current: data.processed,
598
+ total: data.processed,
599
+ percent: 100,
600
+ status: "complete",
601
+ processed: data.processed,
602
+ orphansRemoved: data.orphansRemoved,
603
+ errors: data.errors
604
+ });
605
+ triggerRefresh();
606
+ } else if (data.type === "error") {
607
+ setProgressState((prev) => ({
608
+ ...prev,
609
+ status: "error",
610
+ message: data.message
611
+ }));
612
+ }
613
+ } catch {
614
+ }
615
+ }
474
616
  }
475
617
  } else {
618
+ setShowProgress(true);
619
+ setProgressState({
620
+ current: 0,
621
+ total: processCount,
622
+ percent: 0,
623
+ status: "processing"
624
+ });
476
625
  const selectedImageKeys = Array.from(selectedItems).filter((p) => {
477
626
  const ext = p.split(".").pop()?.toLowerCase() || "";
478
627
  return ["jpg", "jpeg", "png", "gif", "webp", "svg", "ico", "bmp", "tiff", "tif"].includes(ext);
@@ -484,29 +633,39 @@ function StudioToolbar() {
484
633
  });
485
634
  const data = await response.json();
486
635
  if (response.ok) {
487
- setAlertMessage({
488
- title: "Processing Complete",
489
- message: `Processed ${data.processed?.length || 0} images.${data.errors?.length > 0 ? ` ${data.errors.length} errors occurred.` : ""}`
636
+ setProgressState({
637
+ current: data.processed?.length || 0,
638
+ total: data.processed?.length || 0,
639
+ percent: 100,
640
+ status: "complete",
641
+ processed: data.processed?.length || 0,
642
+ errors: data.errors?.length || 0
490
643
  });
491
644
  clearSelection();
492
645
  triggerRefresh();
493
646
  } else {
494
- setAlertMessage({
495
- title: "Processing Failed",
647
+ setProgressState({
648
+ current: 0,
649
+ total: 0,
650
+ percent: 0,
651
+ status: "error",
496
652
  message: data.error || "Unknown error"
497
653
  });
498
654
  }
499
655
  }
500
656
  } catch (error) {
501
657
  console.error("Processing error:", error);
502
- setAlertMessage({
503
- title: "Processing Failed",
658
+ setProgressState({
659
+ current: 0,
660
+ total: 0,
661
+ percent: 0,
662
+ status: "error",
504
663
  message: "Processing failed. Check console for details."
505
664
  });
506
665
  } finally {
507
666
  setProcessing(false);
508
667
  }
509
- }, [processMode, selectedItems, clearSelection, triggerRefresh]);
668
+ }, [processMode, processCount, selectedItems, clearSelection, triggerRefresh]);
510
669
  const handleDeleteClick = useCallback(() => {
511
670
  if (selectedItems.size === 0) return;
512
671
  setShowDeleteConfirm(true);
@@ -547,7 +706,7 @@ function StudioToolbar() {
547
706
  if (focusedItem) {
548
707
  return null;
549
708
  }
550
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
709
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
551
710
  showDeleteConfirm && /* @__PURE__ */ jsx2(
552
711
  ConfirmModal,
553
712
  {
@@ -563,12 +722,28 @@ function StudioToolbar() {
563
722
  ConfirmModal,
564
723
  {
565
724
  title: "Process Images",
566
- message: processMode === "all" ? `Found ${processCount} unprocessed image${processCount !== 1 ? "s" : ""} in the public folder. This will generate thumbnails and remove any orphaned files from the images folder.` : `Process ${processCount} selected image${processCount !== 1 ? "s" : ""}? This will regenerate thumbnails for these files.`,
725
+ message: processMode === "all" ? `Found ${processCount} image${processCount !== 1 ? "s" : ""} in the public folder. This will regenerate all thumbnails and remove any orphaned files from the images folder.` : `Process ${processCount} selected image${processCount !== 1 ? "s" : ""}? This will regenerate thumbnails for these files.`,
567
726
  confirmLabel: processing ? "Processing..." : "Process",
568
727
  onConfirm: handleProcessConfirm,
569
728
  onCancel: () => setShowProcessConfirm(false)
570
729
  }
571
730
  ),
731
+ showProgress && /* @__PURE__ */ jsx2(
732
+ ProgressModal,
733
+ {
734
+ title: "Processing Images",
735
+ progress: progressState,
736
+ onClose: () => {
737
+ setShowProgress(false);
738
+ setProgressState({
739
+ current: 0,
740
+ total: 0,
741
+ percent: 0,
742
+ status: "processing"
743
+ });
744
+ }
745
+ }
746
+ ),
572
747
  alertMessage && /* @__PURE__ */ jsx2(
573
748
  AlertModal,
574
749
  {
@@ -1434,7 +1609,7 @@ function formatFileSize2(bytes) {
1434
1609
  // src/components/StudioDetailView.tsx
1435
1610
  import { useState as useState4 } from "react";
1436
1611
  import { css as css5 } from "@emotion/react";
1437
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "@emotion/react/jsx-runtime";
1612
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "@emotion/react/jsx-runtime";
1438
1613
  var IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico", ".bmp", ".tiff", ".tif"];
1439
1614
  var VIDEO_EXTENSIONS = [".mp4", ".webm", ".mov", ".avi", ".mkv", ".m4v"];
1440
1615
  function isImageFile(filename) {
@@ -1675,7 +1850,7 @@ function StudioDetailView() {
1675
1850
  /* @__PURE__ */ jsx5("p", { css: styles5.fileName, children: focusedItem.name })
1676
1851
  ] });
1677
1852
  };
1678
- return /* @__PURE__ */ jsxs5(Fragment2, { children: [
1853
+ return /* @__PURE__ */ jsxs5(Fragment3, { children: [
1679
1854
  showDeleteConfirm && /* @__PURE__ */ jsx5(
1680
1855
  ConfirmModal,
1681
1856
  {
@@ -1757,7 +1932,7 @@ function formatFileSize3(bytes) {
1757
1932
  // src/components/StudioSettings.tsx
1758
1933
  import { useState as useState5 } from "react";
1759
1934
  import { css as css6 } from "@emotion/react";
1760
- import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs6 } from "@emotion/react/jsx-runtime";
1935
+ import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs6 } from "@emotion/react/jsx-runtime";
1761
1936
  var btnHeight2 = "36px";
1762
1937
  var styles6 = {
1763
1938
  btn: css6`
@@ -1941,7 +2116,7 @@ var styles6 = {
1941
2116
  };
1942
2117
  function StudioSettings() {
1943
2118
  const [isOpen, setIsOpen] = useState5(false);
1944
- return /* @__PURE__ */ jsxs6(Fragment3, { children: [
2119
+ return /* @__PURE__ */ jsxs6(Fragment4, { children: [
1945
2120
  /* @__PURE__ */ jsx6("button", { css: styles6.btn, onClick: () => setIsOpen(true), "aria-label": "Settings", children: /* @__PURE__ */ jsxs6(
1946
2121
  "svg",
1947
2122
  {
@@ -2222,4 +2397,4 @@ export {
2222
2397
  StudioUI,
2223
2398
  StudioUI_default as default
2224
2399
  };
2225
- //# sourceMappingURL=StudioUI-F2C4N66F.mjs.map
2400
+ //# sourceMappingURL=StudioUI-BPOKRRW7.mjs.map