@gallop.software/studio 1.2.8 → 1.3.1

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.
@@ -12,6 +12,19 @@ var _react3 = require('@emotion/react');
12
12
 
13
13
  // src/components/StudioContext.tsx
14
14
 
15
+ var defaultActionState = {
16
+ showProgress: false,
17
+ progressTitle: "",
18
+ progressState: { current: 0, total: 0, percent: 0, status: "processing" },
19
+ showDeleteConfirm: false,
20
+ showMoveModal: false,
21
+ showSyncConfirm: false,
22
+ showProcessConfirm: false,
23
+ actionPaths: [],
24
+ syncImageCount: 0,
25
+ syncHasRemote: false,
26
+ syncHasLocal: false
27
+ };
15
28
  var defaultState = {
16
29
  isOpen: false,
17
30
  openStudio: () => {
@@ -65,6 +78,34 @@ var defaultState = {
65
78
  },
66
79
  fileItems: [],
67
80
  setFileItems: () => {
81
+ },
82
+ // Shared action state
83
+ actionState: defaultActionState,
84
+ // Shared action handlers
85
+ requestDelete: () => {
86
+ },
87
+ requestMove: () => {
88
+ },
89
+ requestSync: () => {
90
+ },
91
+ requestProcess: () => {
92
+ },
93
+ confirmDelete: async () => {
94
+ },
95
+ confirmMove: async () => {
96
+ },
97
+ confirmSync: async () => {
98
+ },
99
+ confirmProcess: async () => {
100
+ },
101
+ cancelAction: () => {
102
+ },
103
+ closeProgress: () => {
104
+ },
105
+ stopProcessing: () => {
106
+ },
107
+ abortController: null,
108
+ deleteOrphans: async () => {
68
109
  }
69
110
  };
70
111
  var StudioContext = _react.createContext.call(void 0, defaultState);
@@ -4392,17 +4433,23 @@ var styles8 = {
4392
4433
  `
4393
4434
  };
4394
4435
  function StudioDetailView() {
4395
- const { focusedItem, setFocusedItem, triggerRefresh, clearSelection } = useStudio();
4396
- const [showDeleteConfirm, setShowDeleteConfirm] = _react.useState.call(void 0, false);
4436
+ const {
4437
+ focusedItem,
4438
+ setFocusedItem,
4439
+ triggerRefresh,
4440
+ fileItems,
4441
+ // Shared action handlers
4442
+ requestDelete,
4443
+ requestMove,
4444
+ requestSync,
4445
+ requestProcess,
4446
+ actionState
4447
+ } = useStudio();
4397
4448
  const [showRenameModal, setShowRenameModal] = _react.useState.call(void 0, false);
4398
- const [showMoveModal, setShowMoveModal] = _react.useState.call(void 0, false);
4399
- const [showProcessConfirm, setShowProcessConfirm] = _react.useState.call(void 0, false);
4400
4449
  const [showR2SetupModal, setShowR2SetupModal] = _react.useState.call(void 0, false);
4401
- const [processProgress, setProcessProgress] = _react.useState.call(void 0, null);
4402
4450
  const [alertMessage, setAlertMessage] = _react.useState.call(void 0, null);
4403
4451
  const [showCopied, setShowCopied] = _react.useState.call(void 0, false);
4404
- const [pushing, setPushing] = _react.useState.call(void 0, false);
4405
- const [moving, setMoving] = _react.useState.call(void 0, false);
4452
+ const isActionInProgress = actionState.showProgress;
4406
4453
  if (!focusedItem) return null;
4407
4454
  const isImage = isImageFile(focusedItem.name);
4408
4455
  const isVideo = isVideoFile(focusedItem.name);
@@ -4449,177 +4496,6 @@ function StudioDetailView() {
4449
4496
  }
4450
4497
  }
4451
4498
  };
4452
- const handleDelete = async () => {
4453
- setShowDeleteConfirm(false);
4454
- try {
4455
- const response = await fetch("/api/studio/delete", {
4456
- method: "POST",
4457
- headers: { "Content-Type": "application/json" },
4458
- body: JSON.stringify({ paths: [focusedItem.path] })
4459
- });
4460
- if (response.ok) {
4461
- clearSelection();
4462
- triggerRefresh();
4463
- setFocusedItem(null);
4464
- } else {
4465
- const error = await response.json();
4466
- setAlertMessage({
4467
- title: "Delete Failed",
4468
- message: error.error || "Unknown error"
4469
- });
4470
- }
4471
- } catch (error) {
4472
- console.error("Delete error:", error);
4473
- setAlertMessage({
4474
- title: "Delete Failed",
4475
- message: "Delete failed. Check console for details."
4476
- });
4477
- }
4478
- };
4479
- const handleMove = async (destination) => {
4480
- setShowMoveModal(false);
4481
- setMoving(true);
4482
- try {
4483
- const response = await fetch("/api/studio/move", {
4484
- method: "POST",
4485
- headers: { "Content-Type": "application/json" },
4486
- body: JSON.stringify({ paths: [focusedItem.path], destination })
4487
- });
4488
- if (!response.body) {
4489
- throw new Error("No response body");
4490
- }
4491
- const reader = response.body.getReader();
4492
- const decoder = new TextDecoder();
4493
- let buffer = "";
4494
- while (true) {
4495
- const { done, value } = await reader.read();
4496
- if (done) break;
4497
- buffer += decoder.decode(value, { stream: true });
4498
- const lines = buffer.split("\n\n");
4499
- buffer = lines.pop() || "";
4500
- for (const line of lines) {
4501
- if (!line.startsWith("data: ")) continue;
4502
- try {
4503
- const data = JSON.parse(line.slice(6));
4504
- if (data.type === "complete") {
4505
- if (data.errors > 0 && _optionalChain([data, 'access', _50 => _50.errorMessages, 'optionalAccess', _51 => _51.length]) > 0) {
4506
- setAlertMessage({
4507
- title: "Move Failed",
4508
- message: data.errorMessages.join("\n")
4509
- });
4510
- } else {
4511
- clearSelection();
4512
- triggerRefresh();
4513
- setFocusedItem(null);
4514
- }
4515
- } else if (data.type === "error") {
4516
- setAlertMessage({
4517
- title: "Move Failed",
4518
- message: data.message || "Unknown error"
4519
- });
4520
- }
4521
- } catch (e5) {
4522
- }
4523
- }
4524
- }
4525
- } catch (error) {
4526
- console.error("Move error:", error);
4527
- setAlertMessage({
4528
- title: "Move Failed",
4529
- message: "Failed to move file. Check console for details."
4530
- });
4531
- } finally {
4532
- setMoving(false);
4533
- }
4534
- };
4535
- const handleSync = async () => {
4536
- const imageKey = "/" + focusedItem.path.replace(/^public\//, "");
4537
- setPushing(true);
4538
- try {
4539
- const response = await fetch("/api/studio/sync", {
4540
- method: "POST",
4541
- headers: { "Content-Type": "application/json" },
4542
- body: JSON.stringify({ imageKeys: [imageKey] })
4543
- });
4544
- const data = await response.json();
4545
- if (response.ok) {
4546
- setAlertMessage({
4547
- title: "Push Complete",
4548
- message: "Successfully pushed to CDN."
4549
- });
4550
- triggerRefresh();
4551
- } else {
4552
- if (_optionalChain([data, 'access', _52 => _52.error, 'optionalAccess', _53 => _53.includes, 'call', _54 => _54("R2 not configured")]) || _optionalChain([data, 'access', _55 => _55.error, 'optionalAccess', _56 => _56.includes, 'call', _57 => _57("CLOUDFLARE_R2")])) {
4553
- setShowR2SetupModal(true);
4554
- } else {
4555
- setAlertMessage({
4556
- title: "Push Failed",
4557
- message: data.error || "Failed to push to CDN."
4558
- });
4559
- }
4560
- }
4561
- } catch (error) {
4562
- console.error("Push error:", error);
4563
- setAlertMessage({
4564
- title: "Push Failed",
4565
- message: "Failed to push to CDN. Check console for details."
4566
- });
4567
- } finally {
4568
- setPushing(false);
4569
- }
4570
- };
4571
- const handleProcessImage = async () => {
4572
- setShowProcessConfirm(false);
4573
- setProcessProgress({
4574
- current: 0,
4575
- total: 1,
4576
- percent: 0,
4577
- status: "processing",
4578
- currentFile: focusedItem.name
4579
- });
4580
- try {
4581
- const imageKey = focusedItem.path.replace(/^public\//, "");
4582
- const formattedKey = imageKey.startsWith("/") ? imageKey : `/${imageKey}`;
4583
- const response = await fetch("/api/studio/reprocess", {
4584
- method: "POST",
4585
- headers: { "Content-Type": "application/json" },
4586
- body: JSON.stringify({
4587
- imageKeys: [formattedKey]
4588
- })
4589
- });
4590
- const data = await response.json();
4591
- if (!response.ok) {
4592
- throw new Error(data.error || "Processing failed");
4593
- }
4594
- if (_optionalChain([data, 'access', _58 => _58.processed, 'optionalAccess', _59 => _59.length]) > 0) {
4595
- setProcessProgress({
4596
- current: 1,
4597
- total: 1,
4598
- percent: 100,
4599
- status: "complete",
4600
- message: `Processed ${focusedItem.name}`
4601
- });
4602
- } else if (_optionalChain([data, 'access', _60 => _60.errors, 'optionalAccess', _61 => _61.length]) > 0) {
4603
- setProcessProgress({
4604
- current: 0,
4605
- total: 1,
4606
- percent: 0,
4607
- status: "error",
4608
- message: `Failed to process: ${data.errors.join(", ")}`
4609
- });
4610
- }
4611
- triggerRefresh();
4612
- } catch (error) {
4613
- console.error("Process error:", error);
4614
- setProcessProgress({
4615
- current: 0,
4616
- total: 1,
4617
- percent: 0,
4618
- status: "error",
4619
- message: error instanceof Error ? error.message : "Failed to process image"
4620
- });
4621
- }
4622
- };
4623
4499
  const renderMedia = () => {
4624
4500
  if (isImage) {
4625
4501
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "img", { css: styles8.image, src: imageSrc, alt: focusedItem.name });
@@ -4633,17 +4509,6 @@ function StudioDetailView() {
4633
4509
  ] });
4634
4510
  };
4635
4511
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
4636
- showDeleteConfirm && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
4637
- ConfirmModal,
4638
- {
4639
- title: "Delete File",
4640
- message: `Are you sure you want to delete "${focusedItem.name}"? This action cannot be undone.`,
4641
- confirmLabel: "Delete",
4642
- variant: "danger",
4643
- onConfirm: handleDelete,
4644
- onCancel: () => setShowDeleteConfirm(false)
4645
- }
4646
- ),
4647
4512
  alertMessage && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
4648
4513
  AlertModal,
4649
4514
  {
@@ -4671,33 +4536,6 @@ function StudioDetailView() {
4671
4536
  onCancel: () => setShowRenameModal(false)
4672
4537
  }
4673
4538
  ),
4674
- showMoveModal && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
4675
- StudioFolderPicker,
4676
- {
4677
- selectedItems: /* @__PURE__ */ new Set([focusedItem.path]),
4678
- currentPath: focusedItem.path.split("/").slice(0, -1).join("/"),
4679
- onMove: handleMove,
4680
- onCancel: () => setShowMoveModal(false)
4681
- }
4682
- ),
4683
- showProcessConfirm && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
4684
- ConfirmModal,
4685
- {
4686
- title: "Process Image",
4687
- message: `Generate thumbnails for "${focusedItem.name}"?`,
4688
- confirmLabel: "Process",
4689
- onConfirm: handleProcessImage,
4690
- onCancel: () => setShowProcessConfirm(false)
4691
- }
4692
- ),
4693
- processProgress && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
4694
- ProgressModal,
4695
- {
4696
- title: "Processing Image",
4697
- progress: processProgress,
4698
- onClose: () => setProcessProgress(null)
4699
- }
4700
- ),
4701
4539
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles8.overlay, onClick: handleClose, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles8.container, onClick: (e) => e.stopPropagation(), children: [
4702
4540
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles8.main, children: [
4703
4541
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles8.headerButtons, children: [
@@ -4771,11 +4609,11 @@ function StudioDetailView() {
4771
4609
  "button",
4772
4610
  {
4773
4611
  css: styles8.actionBtn,
4774
- onClick: () => setShowMoveModal(true),
4775
- disabled: moving || focusedItem.isProtected,
4612
+ onClick: () => requestMove([focusedItem.path]),
4613
+ disabled: isActionInProgress || focusedItem.isProtected,
4776
4614
  children: [
4777
4615
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { css: styles8.actionIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" }) }),
4778
- moving ? "Moving..." : "Move"
4616
+ "Move"
4779
4617
  ]
4780
4618
  }
4781
4619
  ),
@@ -4783,12 +4621,12 @@ function StudioDetailView() {
4783
4621
  "button",
4784
4622
  {
4785
4623
  css: styles8.actionBtn,
4786
- onClick: handleSync,
4787
- disabled: pushing || focusedItem.isProtected || focusedItem.cdnPushed && !focusedItem.isRemote,
4624
+ onClick: () => requestSync([focusedItem.path], fileItems),
4625
+ disabled: isActionInProgress || focusedItem.isProtected || focusedItem.cdnPushed && !focusedItem.isRemote,
4788
4626
  title: focusedItem.cdnPushed && !focusedItem.isRemote ? "Already in R2" : void 0,
4789
4627
  children: [
4790
4628
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { css: styles8.actionIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "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" }) }),
4791
- pushing ? "Pushing..." : "Push to CDN"
4629
+ "Push to CDN"
4792
4630
  ]
4793
4631
  }
4794
4632
  ),
@@ -4796,8 +4634,8 @@ function StudioDetailView() {
4796
4634
  "button",
4797
4635
  {
4798
4636
  css: styles8.actionBtn,
4799
- onClick: () => setShowProcessConfirm(true),
4800
- disabled: focusedItem.isProtected,
4637
+ onClick: () => requestProcess([focusedItem.path]),
4638
+ disabled: isActionInProgress || focusedItem.isProtected,
4801
4639
  children: [
4802
4640
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { css: styles8.actionIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" }) }),
4803
4641
  "Process Image"
@@ -4808,8 +4646,8 @@ function StudioDetailView() {
4808
4646
  "button",
4809
4647
  {
4810
4648
  css: [styles8.actionBtn, styles8.actionBtnDanger],
4811
- onClick: () => setShowDeleteConfirm(true),
4812
- disabled: focusedItem.isProtected,
4649
+ onClick: () => requestDelete([focusedItem.path]),
4650
+ disabled: isActionInProgress || focusedItem.isProtected,
4813
4651
  children: [
4814
4652
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { css: styles8.actionIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" }) }),
4815
4653
  "Delete"
@@ -5393,6 +5231,362 @@ function ErrorModal() {
5393
5231
  ] }) });
5394
5232
  }
5395
5233
 
5234
+ // src/components/useStudioActions.tsx
5235
+
5236
+ var defaultActionState2 = {
5237
+ showProgress: false,
5238
+ progressTitle: "",
5239
+ progressState: { current: 0, total: 0, percent: 0, status: "processing" },
5240
+ showDeleteConfirm: false,
5241
+ showMoveModal: false,
5242
+ showSyncConfirm: false,
5243
+ showProcessConfirm: false,
5244
+ actionPaths: [],
5245
+ syncImageCount: 0,
5246
+ syncHasRemote: false,
5247
+ syncHasLocal: false
5248
+ };
5249
+ function useStudioActions({
5250
+ triggerRefresh,
5251
+ clearSelection,
5252
+ setFocusedItem,
5253
+ showError
5254
+ }) {
5255
+ const [actionState, setActionState] = _react.useState.call(void 0, defaultActionState2);
5256
+ const abortControllerRef = _react.useRef.call(void 0, null);
5257
+ const setProgressState = _react.useCallback.call(void 0, (update) => {
5258
+ setActionState((prev) => ({
5259
+ ...prev,
5260
+ progressState: typeof update === "function" ? update(prev.progressState) : { ...prev.progressState, ...update }
5261
+ }));
5262
+ }, []);
5263
+ const requestDelete = _react.useCallback.call(void 0, (paths) => {
5264
+ setActionState((prev) => ({
5265
+ ...prev,
5266
+ actionPaths: paths,
5267
+ showDeleteConfirm: true
5268
+ }));
5269
+ }, []);
5270
+ const requestMove = _react.useCallback.call(void 0, (paths) => {
5271
+ setActionState((prev) => ({
5272
+ ...prev,
5273
+ actionPaths: paths,
5274
+ showMoveModal: true
5275
+ }));
5276
+ }, []);
5277
+ const requestSync = _react.useCallback.call(void 0, (paths, fileItems) => {
5278
+ const imageKeys = paths.map((p) => "/" + p.replace(/^public\//, ""));
5279
+ let hasRemote = false;
5280
+ let hasLocal = false;
5281
+ for (const path of paths) {
5282
+ const item = fileItems.find((f) => f.path === path);
5283
+ if (item) {
5284
+ if (item.isRemote) {
5285
+ hasRemote = true;
5286
+ } else if (!item.cdnPushed) {
5287
+ hasLocal = true;
5288
+ }
5289
+ }
5290
+ }
5291
+ setActionState((prev) => ({
5292
+ ...prev,
5293
+ actionPaths: paths,
5294
+ syncImageCount: imageKeys.length,
5295
+ syncHasRemote: hasRemote,
5296
+ syncHasLocal: hasLocal,
5297
+ showSyncConfirm: true
5298
+ }));
5299
+ }, []);
5300
+ const requestProcess = _react.useCallback.call(void 0, (paths) => {
5301
+ setActionState((prev) => ({
5302
+ ...prev,
5303
+ actionPaths: paths,
5304
+ showProcessConfirm: true
5305
+ }));
5306
+ }, []);
5307
+ const cancelAction = _react.useCallback.call(void 0, () => {
5308
+ setActionState((prev) => ({
5309
+ ...prev,
5310
+ showDeleteConfirm: false,
5311
+ showMoveModal: false,
5312
+ showSyncConfirm: false,
5313
+ showProcessConfirm: false
5314
+ }));
5315
+ }, []);
5316
+ const closeProgress = _react.useCallback.call(void 0, () => {
5317
+ setActionState(defaultActionState2);
5318
+ }, []);
5319
+ const stopProcessing = _react.useCallback.call(void 0, () => {
5320
+ if (abortControllerRef.current) {
5321
+ abortControllerRef.current.abort();
5322
+ }
5323
+ }, []);
5324
+ const confirmDelete = _react.useCallback.call(void 0, async () => {
5325
+ const paths = actionState.actionPaths;
5326
+ setActionState((prev) => ({ ...prev, showDeleteConfirm: false }));
5327
+ try {
5328
+ const response = await fetch("/api/studio/delete", {
5329
+ method: "POST",
5330
+ headers: { "Content-Type": "application/json" },
5331
+ body: JSON.stringify({ paths })
5332
+ });
5333
+ if (response.ok) {
5334
+ clearSelection();
5335
+ setFocusedItem(null);
5336
+ triggerRefresh();
5337
+ } else {
5338
+ const error = await response.json();
5339
+ showError("Delete Failed", error.error || "Unknown error");
5340
+ }
5341
+ } catch (error) {
5342
+ console.error("Delete error:", error);
5343
+ showError("Delete Failed", "Delete failed. Check console for details.");
5344
+ }
5345
+ }, [actionState.actionPaths, clearSelection, setFocusedItem, triggerRefresh, showError]);
5346
+ const confirmMove = _react.useCallback.call(void 0, async (destination) => {
5347
+ const paths = actionState.actionPaths;
5348
+ setActionState((prev) => ({
5349
+ ...prev,
5350
+ showMoveModal: false,
5351
+ showProgress: true,
5352
+ progressTitle: "Moving Files",
5353
+ progressState: {
5354
+ current: 0,
5355
+ total: paths.length,
5356
+ percent: 0,
5357
+ status: "processing",
5358
+ message: "Moving files..."
5359
+ }
5360
+ }));
5361
+ try {
5362
+ const response = await fetch("/api/studio/move", {
5363
+ method: "POST",
5364
+ headers: { "Content-Type": "application/json" },
5365
+ body: JSON.stringify({ paths, destination })
5366
+ });
5367
+ if (!response.ok) {
5368
+ const error = await response.json();
5369
+ setProgressState({
5370
+ status: "error",
5371
+ message: error.error || "Move failed"
5372
+ });
5373
+ return;
5374
+ }
5375
+ const reader = _optionalChain([response, 'access', _50 => _50.body, 'optionalAccess', _51 => _51.getReader, 'call', _52 => _52()]);
5376
+ const decoder = new TextDecoder();
5377
+ if (reader) {
5378
+ let buffer = "";
5379
+ while (true) {
5380
+ const { done, value } = await reader.read();
5381
+ if (done) break;
5382
+ buffer += decoder.decode(value, { stream: true });
5383
+ const lines = buffer.split("\n");
5384
+ buffer = lines.pop() || "";
5385
+ for (const line of lines) {
5386
+ if (line.startsWith("data: ")) {
5387
+ try {
5388
+ const data = JSON.parse(line.slice(6));
5389
+ if (data.type === "start") {
5390
+ setProgressState((prev) => ({ ...prev, total: data.total }));
5391
+ } else if (data.type === "progress") {
5392
+ setProgressState({
5393
+ current: data.current,
5394
+ total: data.total,
5395
+ percent: Math.round(data.current / data.total * 100),
5396
+ status: "processing",
5397
+ message: data.message
5398
+ });
5399
+ } else if (data.type === "complete") {
5400
+ setProgressState((prev) => ({
5401
+ ...prev,
5402
+ status: "complete",
5403
+ message: `Moved ${data.moved} file${data.moved !== 1 ? "s" : ""}${data.errors > 0 ? `, ${data.errors} error${data.errors !== 1 ? "s" : ""}` : ""}`
5404
+ }));
5405
+ if (data.errors > 0 && _optionalChain([data, 'access', _53 => _53.errorMessages, 'optionalAccess', _54 => _54.length]) > 0) {
5406
+ showError("Move Failed", data.errorMessages.join("\n"));
5407
+ }
5408
+ clearSelection();
5409
+ setFocusedItem(null);
5410
+ triggerRefresh();
5411
+ } else if (data.type === "error") {
5412
+ setProgressState((prev) => ({
5413
+ ...prev,
5414
+ status: "error",
5415
+ message: data.message || "Unknown error"
5416
+ }));
5417
+ }
5418
+ } catch (e5) {
5419
+ }
5420
+ }
5421
+ }
5422
+ }
5423
+ }
5424
+ } catch (error) {
5425
+ console.error("Move error:", error);
5426
+ setProgressState((prev) => ({
5427
+ ...prev,
5428
+ status: "error",
5429
+ message: "Failed to move files. Check console for details."
5430
+ }));
5431
+ }
5432
+ }, [actionState.actionPaths, clearSelection, setFocusedItem, triggerRefresh, showError, setProgressState]);
5433
+ const confirmSync = _react.useCallback.call(void 0, async () => {
5434
+ const paths = actionState.actionPaths;
5435
+ const imageKeys = paths.map((p) => "/" + p.replace(/^public\//, ""));
5436
+ setActionState((prev) => ({
5437
+ ...prev,
5438
+ showSyncConfirm: false,
5439
+ showProgress: true,
5440
+ progressTitle: "Pushing to CDN",
5441
+ progressState: {
5442
+ current: 0,
5443
+ total: imageKeys.length,
5444
+ percent: 0,
5445
+ status: "processing",
5446
+ message: "Pushing to CDN..."
5447
+ }
5448
+ }));
5449
+ let pushed = 0;
5450
+ const errors = [];
5451
+ for (let i = 0; i < imageKeys.length; i++) {
5452
+ const imageKey = imageKeys[i];
5453
+ setProgressState({
5454
+ current: i + 1,
5455
+ total: imageKeys.length,
5456
+ percent: Math.round((i + 1) / imageKeys.length * 100),
5457
+ status: "processing",
5458
+ message: `Pushing ${imageKey}...`
5459
+ });
5460
+ try {
5461
+ const response = await fetch("/api/studio/sync", {
5462
+ method: "POST",
5463
+ headers: { "Content-Type": "application/json" },
5464
+ body: JSON.stringify({ imageKeys: [imageKey] })
5465
+ });
5466
+ if (response.ok) {
5467
+ pushed++;
5468
+ } else {
5469
+ const data = await response.json();
5470
+ errors.push(`${imageKey}: ${data.error || "Unknown error"}`);
5471
+ }
5472
+ } catch (error) {
5473
+ errors.push(`${imageKey}: ${error instanceof Error ? error.message : "Unknown error"}`);
5474
+ }
5475
+ }
5476
+ setProgressState({
5477
+ current: imageKeys.length,
5478
+ total: imageKeys.length,
5479
+ percent: 100,
5480
+ status: errors.length > 0 ? "error" : "complete",
5481
+ message: `Pushed ${pushed} file${pushed !== 1 ? "s" : ""}${errors.length > 0 ? `, ${errors.length} error${errors.length !== 1 ? "s" : ""}` : ""}`
5482
+ });
5483
+ if (errors.length > 0) {
5484
+ showError("Push Errors", errors.join("\n"));
5485
+ }
5486
+ clearSelection();
5487
+ triggerRefresh();
5488
+ }, [actionState.actionPaths, clearSelection, triggerRefresh, showError, setProgressState]);
5489
+ const confirmProcess = _react.useCallback.call(void 0, async () => {
5490
+ const paths = actionState.actionPaths;
5491
+ const imageKeys = paths.map((p) => {
5492
+ const key = p.replace(/^public\//, "");
5493
+ return key.startsWith("/") ? key : `/${key}`;
5494
+ });
5495
+ setActionState((prev) => ({
5496
+ ...prev,
5497
+ showProcessConfirm: false,
5498
+ showProgress: true,
5499
+ progressTitle: "Processing Images",
5500
+ progressState: {
5501
+ current: 0,
5502
+ total: imageKeys.length,
5503
+ percent: 0,
5504
+ status: "processing",
5505
+ message: "Processing images..."
5506
+ }
5507
+ }));
5508
+ try {
5509
+ const response = await fetch("/api/studio/reprocess", {
5510
+ method: "POST",
5511
+ headers: { "Content-Type": "application/json" },
5512
+ body: JSON.stringify({ imageKeys })
5513
+ });
5514
+ const data = await response.json();
5515
+ if (!response.ok) {
5516
+ setProgressState({
5517
+ current: 0,
5518
+ total: imageKeys.length,
5519
+ percent: 0,
5520
+ status: "error",
5521
+ message: data.error || "Processing failed"
5522
+ });
5523
+ return;
5524
+ }
5525
+ const processed = _optionalChain([data, 'access', _55 => _55.processed, 'optionalAccess', _56 => _56.length]) || 0;
5526
+ const errors = _optionalChain([data, 'access', _57 => _57.errors, 'optionalAccess', _58 => _58.length]) || 0;
5527
+ setProgressState({
5528
+ current: processed,
5529
+ total: imageKeys.length,
5530
+ percent: 100,
5531
+ status: errors > 0 ? "error" : "complete",
5532
+ message: `Processed ${processed} image${processed !== 1 ? "s" : ""}${errors > 0 ? `, ${errors} error${errors !== 1 ? "s" : ""}` : ""}`
5533
+ });
5534
+ triggerRefresh();
5535
+ } catch (error) {
5536
+ console.error("Processing error:", error);
5537
+ setProgressState({
5538
+ current: 0,
5539
+ total: imageKeys.length,
5540
+ percent: 0,
5541
+ status: "error",
5542
+ message: "Processing failed. Check console for details."
5543
+ });
5544
+ }
5545
+ }, [actionState.actionPaths, triggerRefresh, setProgressState]);
5546
+ const deleteOrphans = _react.useCallback.call(void 0, async () => {
5547
+ const orphanedFiles = actionState.progressState.orphanedFiles;
5548
+ if (!orphanedFiles || orphanedFiles.length === 0) return;
5549
+ try {
5550
+ const response = await fetch("/api/studio/delete-orphans", {
5551
+ method: "POST",
5552
+ headers: { "Content-Type": "application/json" },
5553
+ body: JSON.stringify({ files: orphanedFiles })
5554
+ });
5555
+ if (response.ok) {
5556
+ setProgressState((prev) => ({
5557
+ ...prev,
5558
+ orphanedFiles: void 0,
5559
+ message: _optionalChain([prev, 'access', _59 => _59.message, 'optionalAccess', _60 => _60.replace, 'call', _61 => _61(/Found \d+ orphaned thumbnail\(s\).*/, "Orphaned thumbnails deleted.")])
5560
+ }));
5561
+ triggerRefresh();
5562
+ } else {
5563
+ const error = await response.json();
5564
+ showError("Delete Failed", error.error || "Failed to delete orphaned files");
5565
+ }
5566
+ } catch (error) {
5567
+ console.error("Delete orphans error:", error);
5568
+ showError("Delete Failed", "Failed to delete orphaned files. Check console for details.");
5569
+ }
5570
+ }, [actionState.progressState.orphanedFiles, triggerRefresh, showError, setProgressState]);
5571
+ return {
5572
+ actionState,
5573
+ setActionState,
5574
+ abortController: abortControllerRef.current,
5575
+ requestDelete,
5576
+ requestMove,
5577
+ requestSync,
5578
+ requestProcess,
5579
+ cancelAction,
5580
+ closeProgress,
5581
+ stopProcessing,
5582
+ confirmDelete,
5583
+ confirmMove,
5584
+ confirmSync,
5585
+ confirmProcess,
5586
+ deleteOrphans
5587
+ };
5588
+ }
5589
+
5396
5590
  // src/components/StudioUI.tsx
5397
5591
 
5398
5592
  var btnHeight3 = "36px";
@@ -5643,6 +5837,15 @@ function StudioUI({ onClose, isVisible = true }) {
5643
5837
  const clearSelection = _react.useCallback.call(void 0, () => {
5644
5838
  setSelectedItems(/* @__PURE__ */ new Set());
5645
5839
  }, []);
5840
+ const setFocusedItemCallback = _react.useCallback.call(void 0, (item) => {
5841
+ setFocusedItem(item);
5842
+ }, []);
5843
+ const actions = useStudioActions({
5844
+ triggerRefresh,
5845
+ clearSelection,
5846
+ setFocusedItem: setFocusedItemCallback,
5847
+ showError
5848
+ });
5646
5849
  const handleKeyDown = _react.useCallback.call(void 0,
5647
5850
  (e) => {
5648
5851
  if (e.key === "Escape") {
@@ -5703,7 +5906,22 @@ function StudioUI({ onClose, isVisible = true }) {
5703
5906
  showError,
5704
5907
  clearError,
5705
5908
  fileItems,
5706
- setFileItems
5909
+ setFileItems,
5910
+ // Shared action state and handlers
5911
+ actionState: actions.actionState,
5912
+ requestDelete: actions.requestDelete,
5913
+ requestMove: actions.requestMove,
5914
+ requestSync: actions.requestSync,
5915
+ requestProcess: actions.requestProcess,
5916
+ confirmDelete: actions.confirmDelete,
5917
+ confirmMove: actions.confirmMove,
5918
+ confirmSync: actions.confirmSync,
5919
+ confirmProcess: actions.confirmProcess,
5920
+ cancelAction: actions.cancelAction,
5921
+ closeProgress: actions.closeProgress,
5922
+ stopProcessing: actions.stopProcessing,
5923
+ abortController: actions.abortController,
5924
+ deleteOrphans: actions.deleteOrphans
5707
5925
  };
5708
5926
  return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, StudioContext.Provider, { value: contextValue, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles11.container, children: [
5709
5927
  /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: styles11.header, children: [
@@ -5740,7 +5958,57 @@ function StudioUI({ onClose, isVisible = true }) {
5740
5958
  }
5741
5959
  ),
5742
5960
  focusedItem && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, StudioDetailView, {}),
5743
- /* @__PURE__ */ _jsxruntime.jsx.call(void 0, ErrorModal, {})
5961
+ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, ErrorModal, {}),
5962
+ actions.actionState.showDeleteConfirm && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
5963
+ ConfirmModal,
5964
+ {
5965
+ title: "Delete Files",
5966
+ message: `Are you sure you want to delete ${actions.actionState.actionPaths.length} item${actions.actionState.actionPaths.length !== 1 ? "s" : ""}? This action cannot be undone.`,
5967
+ confirmLabel: "Delete",
5968
+ variant: "danger",
5969
+ onConfirm: actions.confirmDelete,
5970
+ onCancel: actions.cancelAction
5971
+ }
5972
+ ),
5973
+ actions.actionState.showSyncConfirm && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
5974
+ ConfirmModal,
5975
+ {
5976
+ title: "Push to CDN",
5977
+ message: `Push ${actions.actionState.syncImageCount} image${actions.actionState.syncImageCount !== 1 ? "s" : ""} to Cloudflare R2?${actions.actionState.syncHasRemote ? " Remote images will be downloaded first." : ""}${actions.actionState.syncHasLocal ? " After pushing, local files will be deleted." : ""}`,
5978
+ confirmLabel: "Push",
5979
+ onConfirm: actions.confirmSync,
5980
+ onCancel: actions.cancelAction
5981
+ }
5982
+ ),
5983
+ actions.actionState.showProcessConfirm && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
5984
+ ConfirmModal,
5985
+ {
5986
+ title: "Process Images",
5987
+ message: `Generate thumbnails for ${actions.actionState.actionPaths.length} image${actions.actionState.actionPaths.length !== 1 ? "s" : ""}?`,
5988
+ confirmLabel: "Process",
5989
+ onConfirm: actions.confirmProcess,
5990
+ onCancel: actions.cancelAction
5991
+ }
5992
+ ),
5993
+ actions.actionState.showMoveModal && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
5994
+ StudioFolderPicker,
5995
+ {
5996
+ selectedItems: new Set(actions.actionState.actionPaths),
5997
+ currentPath,
5998
+ onMove: (destination) => actions.confirmMove(destination),
5999
+ onCancel: actions.cancelAction
6000
+ }
6001
+ ),
6002
+ actions.actionState.showProgress && /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
6003
+ ProgressModal,
6004
+ {
6005
+ title: actions.actionState.progressTitle,
6006
+ progress: actions.actionState.progressState,
6007
+ onStop: actions.stopProcessing,
6008
+ onDeleteOrphans: actions.deleteOrphans,
6009
+ onClose: actions.closeProgress
6010
+ }
6011
+ )
5744
6012
  ] }) });
5745
6013
  }
5746
6014
  function Breadcrumbs({ currentPath, onNavigate }) {
@@ -5785,4 +6053,4 @@ var StudioUI_default = StudioUI;
5785
6053
 
5786
6054
 
5787
6055
  exports.StudioUI = StudioUI; exports.default = StudioUI_default;
5788
- //# sourceMappingURL=StudioUI-2MWJ4M3B.js.map
6056
+ //# sourceMappingURL=StudioUI-LORP7MIK.js.map