@gallop.software/studio 0.1.88 → 0.1.89

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.
@@ -1289,7 +1289,7 @@ function StudioToolbar() {
1289
1289
  const fileInputRef = useRef(null);
1290
1290
  const abortControllerRef = useRef(null);
1291
1291
  const [uploading, setUploading] = useState3(false);
1292
- const [refreshing, setRefreshing] = useState3(false);
1292
+ const [scanning, setScanning] = useState3(false);
1293
1293
  const [processing, setProcessing] = useState3(false);
1294
1294
  const [showDeleteConfirm, setShowDeleteConfirm] = useState3(false);
1295
1295
  const [showProcessConfirm, setShowProcessConfirm] = useState3(false);
@@ -1315,10 +1315,81 @@ function StudioToolbar() {
1315
1315
  const handleUpload = useCallback(() => {
1316
1316
  fileInputRef.current?.click();
1317
1317
  }, []);
1318
- const handleRefresh = useCallback(() => {
1319
- setRefreshing(true);
1320
- triggerRefresh();
1321
- setTimeout(() => setRefreshing(false), 600);
1318
+ const handleScan = useCallback(async () => {
1319
+ setScanning(true);
1320
+ setShowProgress(true);
1321
+ setProgressState({
1322
+ current: 0,
1323
+ total: 0,
1324
+ percent: 0,
1325
+ status: "processing",
1326
+ message: "Scanning for files..."
1327
+ });
1328
+ try {
1329
+ const response = await fetch("/api/studio/scan", { method: "POST" });
1330
+ const reader = response.body?.getReader();
1331
+ if (!reader) throw new Error("No reader");
1332
+ const decoder = new TextDecoder();
1333
+ let buffer = "";
1334
+ while (true) {
1335
+ const { done, value } = await reader.read();
1336
+ if (done) break;
1337
+ buffer += decoder.decode(value, { stream: true });
1338
+ const lines = buffer.split("\n\n");
1339
+ buffer = lines.pop() || "";
1340
+ for (const line of lines) {
1341
+ if (!line.startsWith("data: ")) continue;
1342
+ const data = JSON.parse(line.slice(6));
1343
+ if (data.type === "start") {
1344
+ setProgressState({
1345
+ current: 0,
1346
+ total: data.total,
1347
+ percent: 0,
1348
+ status: "processing",
1349
+ message: `Scanning ${data.total} files...`
1350
+ });
1351
+ } else if (data.type === "progress") {
1352
+ setProgressState({
1353
+ current: data.current,
1354
+ total: data.total,
1355
+ percent: data.percent,
1356
+ status: "processing",
1357
+ currentFile: data.currentFile
1358
+ });
1359
+ } else if (data.type === "complete") {
1360
+ setProgressState({
1361
+ current: data.total || 0,
1362
+ total: data.total || 0,
1363
+ percent: 100,
1364
+ status: "complete",
1365
+ processed: data.added,
1366
+ errors: data.errors,
1367
+ message: data.renamed > 0 ? `${data.renamed} file(s) renamed due to conflicts` : void 0
1368
+ });
1369
+ triggerRefresh();
1370
+ } else if (data.type === "error") {
1371
+ setProgressState({
1372
+ current: 0,
1373
+ total: 0,
1374
+ percent: 0,
1375
+ status: "error",
1376
+ message: data.message || "Scan failed"
1377
+ });
1378
+ }
1379
+ }
1380
+ }
1381
+ } catch (error) {
1382
+ console.error("Scan error:", error);
1383
+ setProgressState({
1384
+ current: 0,
1385
+ total: 0,
1386
+ percent: 0,
1387
+ status: "error",
1388
+ message: "Scan failed"
1389
+ });
1390
+ } finally {
1391
+ setScanning(false);
1392
+ }
1322
1393
  }, [triggerRefresh]);
1323
1394
  const handleFileChange = useCallback(async (e) => {
1324
1395
  const files = e.target.files;
@@ -2104,12 +2175,16 @@ function StudioToolbar() {
2104
2175
  " selected",
2105
2176
  /* @__PURE__ */ jsx4("button", { css: styles4.clearBtn, onClick: clearSelection, children: "Clear" })
2106
2177
  ] }),
2107
- /* @__PURE__ */ jsx4(
2178
+ /* @__PURE__ */ jsxs4(
2108
2179
  "button",
2109
2180
  {
2110
- css: [styles4.btn, styles4.btnIconOnly],
2111
- onClick: handleRefresh,
2112
- children: /* @__PURE__ */ jsx4(RefreshIcon, { spinning: refreshing })
2181
+ css: styles4.btn,
2182
+ onClick: handleScan,
2183
+ disabled: scanning,
2184
+ children: [
2185
+ /* @__PURE__ */ jsx4(ScanIcon, { spinning: scanning }),
2186
+ "Scan"
2187
+ ]
2113
2188
  }
2114
2189
  ),
2115
2190
  /* @__PURE__ */ jsxs4("div", { css: styles4.viewToggle, children: [
@@ -2139,7 +2214,7 @@ function StudioToolbar() {
2139
2214
  function UploadIcon() {
2140
2215
  return /* @__PURE__ */ jsx4("svg", { css: styles4.icon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" }) });
2141
2216
  }
2142
- function RefreshIcon({ spinning }) {
2217
+ function ScanIcon({ spinning }) {
2143
2218
  return /* @__PURE__ */ jsx4("svg", { css: [styles4.icon, spinning && styles4.iconSpin], fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx4("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) });
2144
2219
  }
2145
2220
  function TrashIcon() {
@@ -2273,6 +2348,7 @@ function useFileList() {
2273
2348
  } = useStudio();
2274
2349
  const [items, setItems] = useState4([]);
2275
2350
  const [loading, setLoading] = useState4(true);
2351
+ const [metaEmpty, setMetaEmpty] = useState4(false);
2276
2352
  const isInitialLoad = useRef2(true);
2277
2353
  const lastPath = useRef2(currentPath);
2278
2354
  useEffect2(() => {
@@ -2285,10 +2361,12 @@ function useFileList() {
2285
2361
  try {
2286
2362
  const data = searchQuery && searchQuery.length >= 2 ? await studioApi.search(searchQuery) : await studioApi.list(currentPath);
2287
2363
  setItems(data.items || []);
2364
+ setMetaEmpty(data.isEmpty === true);
2288
2365
  } catch (error) {
2289
2366
  const message = error instanceof Error ? error.message : "Failed to load items";
2290
2367
  showError("Load Error", message);
2291
2368
  setItems([]);
2369
+ setMetaEmpty(false);
2292
2370
  }
2293
2371
  setLoading(false);
2294
2372
  isInitialLoad.current = false;
@@ -2340,6 +2418,7 @@ function useFileList() {
2340
2418
  items,
2341
2419
  loading,
2342
2420
  sortedItems,
2421
+ metaEmpty,
2343
2422
  // Computed
2344
2423
  isAtRoot,
2345
2424
  isSearching,
@@ -2400,6 +2479,27 @@ var styles5 = {
2400
2479
  font-size: ${fontSize.sm};
2401
2480
  }
2402
2481
  `,
2482
+ scanButton: css5`
2483
+ margin-top: 16px;
2484
+ padding: 10px 24px;
2485
+ font-size: ${fontSize.base};
2486
+ font-weight: 500;
2487
+ background: ${colors.primary};
2488
+ color: white;
2489
+ border: none;
2490
+ border-radius: 8px;
2491
+ cursor: pointer;
2492
+ transition: background 0.15s ease;
2493
+
2494
+ &:hover:not(:disabled) {
2495
+ background: ${colors.primaryHover};
2496
+ }
2497
+
2498
+ &:disabled {
2499
+ opacity: 0.6;
2500
+ cursor: not-allowed;
2501
+ }
2502
+ `,
2403
2503
  grid: css5`
2404
2504
  display: grid;
2405
2505
  grid-template-columns: 1fr;
@@ -2683,6 +2783,7 @@ function StudioFileGrid() {
2683
2783
  const {
2684
2784
  loading,
2685
2785
  sortedItems,
2786
+ metaEmpty,
2686
2787
  isAtRoot,
2687
2788
  isSearching,
2688
2789
  allItemsSelected,
@@ -2694,14 +2795,42 @@ function StudioFileGrid() {
2694
2795
  handleGenerateThumbnail,
2695
2796
  handleSelectAll
2696
2797
  } = useFileList();
2798
+ const [scanning, setScanning] = useState5(false);
2799
+ const handleScan = async () => {
2800
+ setScanning(true);
2801
+ try {
2802
+ await fetch("/api/studio/scan", { method: "POST" });
2803
+ window.location.reload();
2804
+ } catch (error) {
2805
+ console.error("Scan failed:", error);
2806
+ } finally {
2807
+ setScanning(false);
2808
+ }
2809
+ };
2697
2810
  if (loading) {
2698
2811
  return /* @__PURE__ */ jsx5("div", { css: styles5.loading, children: /* @__PURE__ */ jsx5("div", { css: styles5.spinner }) });
2699
2812
  }
2813
+ if (metaEmpty && isAtRoot) {
2814
+ return /* @__PURE__ */ jsxs5("div", { css: styles5.empty, children: [
2815
+ /* @__PURE__ */ jsx5("svg", { css: styles5.emptyIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx5("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" }) }),
2816
+ /* @__PURE__ */ jsx5("p", { css: styles5.emptyText, children: "No files tracked yet" }),
2817
+ /* @__PURE__ */ jsx5("p", { css: styles5.emptyText, children: "Click Scan to discover files in your public folder" }),
2818
+ /* @__PURE__ */ jsx5(
2819
+ "button",
2820
+ {
2821
+ css: styles5.scanButton,
2822
+ onClick: handleScan,
2823
+ disabled: scanning,
2824
+ children: scanning ? "Scanning..." : "Scan for Files"
2825
+ }
2826
+ )
2827
+ ] });
2828
+ }
2700
2829
  if (sortedItems.length === 0 && isAtRoot) {
2701
2830
  return /* @__PURE__ */ jsxs5("div", { css: styles5.empty, children: [
2702
2831
  /* @__PURE__ */ jsx5("svg", { css: styles5.emptyIcon, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx5("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1.5, 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" }) }),
2703
2832
  /* @__PURE__ */ jsx5("p", { css: styles5.emptyText, children: "No files in this folder" }),
2704
- /* @__PURE__ */ jsx5("p", { css: styles5.emptyText, children: "Upload images to get started" })
2833
+ /* @__PURE__ */ jsx5("p", { css: styles5.emptyText, children: "Upload images or click Scan in the toolbar" })
2705
2834
  ] });
2706
2835
  }
2707
2836
  return /* @__PURE__ */ jsxs5("div", { children: [
@@ -2898,6 +3027,32 @@ var styles6 = {
2898
3027
  height: 256px;
2899
3028
  color: ${colors.textSecondary};
2900
3029
  `,
3030
+ emptyHint: css6`
3031
+ font-size: ${fontSize.sm};
3032
+ color: ${colors.textMuted};
3033
+ margin-top: 4px;
3034
+ `,
3035
+ scanButton: css6`
3036
+ margin-top: 16px;
3037
+ padding: 10px 24px;
3038
+ font-size: ${fontSize.base};
3039
+ font-weight: 500;
3040
+ background: ${colors.primary};
3041
+ color: white;
3042
+ border: none;
3043
+ border-radius: 8px;
3044
+ cursor: pointer;
3045
+ transition: background 0.15s ease;
3046
+
3047
+ &:hover:not(:disabled) {
3048
+ background: ${colors.primaryHover};
3049
+ }
3050
+
3051
+ &:disabled {
3052
+ opacity: 0.6;
3053
+ cursor: not-allowed;
3054
+ }
3055
+ `,
2901
3056
  tableWrapper: css6`
2902
3057
  background: ${colors.surface};
2903
3058
  border-radius: 8px;
@@ -3184,6 +3339,7 @@ function StudioFileList() {
3184
3339
  const {
3185
3340
  loading,
3186
3341
  sortedItems,
3342
+ metaEmpty,
3187
3343
  isAtRoot,
3188
3344
  isSearching,
3189
3345
  allItemsSelected,
@@ -3195,11 +3351,41 @@ function StudioFileList() {
3195
3351
  handleGenerateThumbnail,
3196
3352
  handleSelectAll
3197
3353
  } = useFileList();
3354
+ const [scanning, setScanning] = useState6(false);
3355
+ const handleScan = async () => {
3356
+ setScanning(true);
3357
+ try {
3358
+ await fetch("/api/studio/scan", { method: "POST" });
3359
+ window.location.reload();
3360
+ } catch (error) {
3361
+ console.error("Scan failed:", error);
3362
+ } finally {
3363
+ setScanning(false);
3364
+ }
3365
+ };
3198
3366
  if (loading) {
3199
3367
  return /* @__PURE__ */ jsx6("div", { css: styles6.loading, children: /* @__PURE__ */ jsx6("div", { css: styles6.spinner }) });
3200
3368
  }
3369
+ if (metaEmpty && isAtRoot) {
3370
+ return /* @__PURE__ */ jsxs6("div", { css: styles6.empty, children: [
3371
+ /* @__PURE__ */ jsx6("p", { children: "No files tracked yet" }),
3372
+ /* @__PURE__ */ jsx6("p", { css: styles6.emptyHint, children: "Click Scan to discover files in your public folder" }),
3373
+ /* @__PURE__ */ jsx6(
3374
+ "button",
3375
+ {
3376
+ css: styles6.scanButton,
3377
+ onClick: handleScan,
3378
+ disabled: scanning,
3379
+ children: scanning ? "Scanning..." : "Scan for Files"
3380
+ }
3381
+ )
3382
+ ] });
3383
+ }
3201
3384
  if (sortedItems.length === 0 && isAtRoot) {
3202
- return /* @__PURE__ */ jsx6("div", { css: styles6.empty, children: /* @__PURE__ */ jsx6("p", { children: "No files in this folder" }) });
3385
+ return /* @__PURE__ */ jsxs6("div", { css: styles6.empty, children: [
3386
+ /* @__PURE__ */ jsx6("p", { children: "No files in this folder" }),
3387
+ /* @__PURE__ */ jsx6("p", { css: styles6.emptyHint, children: "Upload images or click Scan in the toolbar" })
3388
+ ] });
3203
3389
  }
3204
3390
  return /* @__PURE__ */ jsx6("div", { css: styles6.tableWrapper, children: /* @__PURE__ */ jsxs6("table", { css: styles6.table, children: [
3205
3391
  /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsxs6("tr", { children: [
@@ -4695,4 +4881,4 @@ export {
4695
4881
  StudioUI,
4696
4882
  StudioUI_default as default
4697
4883
  };
4698
- //# sourceMappingURL=StudioUI-6CQ7MX7R.mjs.map
4884
+ //# sourceMappingURL=StudioUI-T7FA7S7Z.mjs.map