open-nami 1.2.6 → 1.2.8

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.
Files changed (2) hide show
  1. package/dist/cli.js +186 -230
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ var require_package = __commonJS({
9
9
  "package.json"(exports, module) {
10
10
  module.exports = {
11
11
  name: "open-nami",
12
- version: "1.2.6",
12
+ version: "1.2.8",
13
13
  description: "Open Nami: terminal-first bulk social media profile downloader for Instagram, TikTok, Facebook & X.",
14
14
  type: "module",
15
15
  bin: {
@@ -77,8 +77,8 @@ import { createRequire } from "module";
77
77
  import { render } from "ink";
78
78
 
79
79
  // src/app.tsx
80
- import { useState as useState9, useCallback as useCallback5, useRef as useRef5, useEffect as useEffect8, useMemo as useMemo3 } from "react";
81
- import { Box as Box15, Text as Text17, useApp, useInput as useInput6, useStdout as useStdout3 } from "ink";
80
+ import { useState as useState9, useCallback as useCallback4, useRef as useRef5, useEffect as useEffect8, useMemo as useMemo3 } from "react";
81
+ import { Box as Box15, Text as Text17, useApp, useInput as useInput5, useStdout as useStdout3 } from "ink";
82
82
  import Spinner4 from "ink-spinner";
83
83
 
84
84
  // src/components/fullscreen.tsx
@@ -691,8 +691,8 @@ function Panel({ title, width, children }) {
691
691
  }
692
692
 
693
693
  // src/components/views/picking-view.tsx
694
- import { useState as useState4, useEffect as useEffect4, useCallback } from "react";
695
- import { Box as Box6, Text as Text7, useInput as useInput2 } from "ink";
694
+ import { useState as useState4, useEffect as useEffect4 } from "react";
695
+ import { Box as Box6, Text as Text7 } from "ink";
696
696
  import SelectInput from "ink-select-input";
697
697
 
698
698
  // src/lib/engines/checkpoint.ts
@@ -4368,7 +4368,7 @@ async function dispatchDownload(options) {
4368
4368
  }
4369
4369
 
4370
4370
  // src/components/views/picking-view.tsx
4371
- import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
4371
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
4372
4372
  function ChoiceIndicator({ isSelected }) {
4373
4373
  const theme = useTheme();
4374
4374
  return /* @__PURE__ */ jsx8(Box6, { marginRight: 1, children: /* @__PURE__ */ jsx8(Text7, { color: theme.primary, children: isSelected ? "\u276F" : " " }) });
@@ -4392,14 +4392,13 @@ function getPickingOptions(platform) {
4392
4392
  });
4393
4393
  return items;
4394
4394
  }
4395
- var DATE_PRESETS = ["all", "24h", "7d", "30d", "2024", "2023", "2022", "2021", "custom"];
4396
- var LIMIT_PRESETS = [void 0, 10, 24, 50, 100, "custom"];
4397
4395
  function PickingView({
4398
4396
  contentWidth,
4399
4397
  profile,
4400
4398
  outputDir,
4401
4399
  initialLimit,
4402
4400
  initialSince,
4401
+ initialUntil,
4403
4402
  initialWriteMetadata = false,
4404
4403
  initialResume = true,
4405
4404
  onSelect,
@@ -4409,26 +4408,13 @@ function PickingView({
4409
4408
  }) {
4410
4409
  const platformName = profile.platform.charAt(0).toUpperCase() + profile.platform.slice(1);
4411
4410
  const items = getPickingOptions(profile.platform);
4412
- const [limit, setLimit] = useState4(initialLimit);
4413
- const [dateFilter, setDateFilter] = useState4(() => {
4414
- if (typeof initialSince === "string" && initialSince.trim()) {
4415
- return initialSince.trim();
4416
- }
4417
- return "all";
4418
- });
4419
- const [writeMetadata, setWriteMetadata] = useState4(initialWriteMetadata);
4420
- const [resume, setResume] = useState4(initialResume);
4421
4411
  const [hasCheckpoint, setHasCheckpoint] = useState4(false);
4422
- const [editingModal, setEditingModal] = useState4(null);
4423
- const [customLimitText, setCustomLimitText] = useState4("");
4424
- const [customDateText, setCustomDateText] = useState4("");
4425
4412
  const targetDir = resolveDefaultDownloadDir(profile.platform, profile.username, outputDir);
4426
4413
  useEffect4(() => {
4427
4414
  let isMounted = true;
4428
4415
  loadCheckpoint(targetDir).then((cp) => {
4429
4416
  if (isMounted && cp && !cp.completed && Object.keys(cp.cursors || {}).length > 0) {
4430
4417
  setHasCheckpoint(true);
4431
- setResume(true);
4432
4418
  }
4433
4419
  }).catch(() => {
4434
4420
  });
@@ -4436,68 +4422,14 @@ function PickingView({
4436
4422
  isMounted = false;
4437
4423
  };
4438
4424
  }, [targetDir]);
4439
- const cycleLimit = useCallback(() => {
4440
- const curIdx = LIMIT_PRESETS.findIndex((p) => p === limit);
4441
- const nextIdx = (curIdx + 1) % LIMIT_PRESETS.length;
4442
- const nextVal = LIMIT_PRESETS[nextIdx];
4443
- if (nextVal === "custom") {
4444
- setCustomLimitText(limit ? String(limit) : "24");
4445
- setEditingModal("limit");
4446
- } else {
4447
- setLimit(nextVal);
4448
- }
4449
- }, [limit]);
4450
- const cycleDate = useCallback(() => {
4451
- const curIdx = DATE_PRESETS.indexOf(dateFilter);
4452
- const nextIdx = (curIdx + 1) % DATE_PRESETS.length;
4453
- const nextVal = DATE_PRESETS[nextIdx];
4454
- if (nextVal === "custom") {
4455
- setCustomDateText(dateFilter !== "all" ? dateFilter : "2021");
4456
- setEditingModal("date");
4457
- } else {
4458
- setDateFilter(nextVal);
4459
- }
4460
- }, [dateFilter]);
4461
- const toggleMetadata = useCallback(() => {
4462
- setWriteMetadata((prev) => !prev);
4463
- }, []);
4464
- const toggleResume = useCallback(() => {
4465
- setResume((prev) => !prev);
4466
- }, []);
4467
- useInput2((input, key) => {
4468
- if (editingModal) {
4469
- if (key.escape) {
4470
- setEditingModal(null);
4471
- }
4472
- return;
4473
- }
4474
- if (input === "l" || input === "L") {
4475
- cycleLimit();
4476
- return;
4477
- }
4478
- if (input === "d" || input === "D") {
4479
- cycleDate();
4480
- return;
4481
- }
4482
- if (input === "m" || input === "M") {
4483
- toggleMetadata();
4484
- return;
4485
- }
4486
- if (input === "r" || input === "R") {
4487
- toggleResume();
4488
- return;
4489
- }
4490
- }, { isActive: Boolean(process.stdin.isTTY) });
4491
4425
  const handleItemSelect = (selectedChoice) => {
4492
- if (editingModal) return;
4493
- const dateRange = resolveDateFilter(dateFilter);
4494
4426
  onSelect({
4495
4427
  choice: selectedChoice,
4496
- limit,
4497
- since: dateRange.since,
4498
- until: dateRange.until,
4499
- writeMetadata,
4500
- resume
4428
+ limit: initialLimit,
4429
+ since: typeof initialSince === "string" ? initialSince : void 0,
4430
+ until: typeof initialUntil === "string" ? initialUntil : void 0,
4431
+ writeMetadata: initialWriteMetadata,
4432
+ resume: initialResume
4501
4433
  });
4502
4434
  };
4503
4435
  return /* @__PURE__ */ jsxs6(Box6, { width: contentWidth, children: [
@@ -4513,90 +4445,23 @@ function PickingView({
4513
4445
  profile.cleanUrl ? ` \xB7 ${profile.cleanUrl}` : ""
4514
4446
  ] }),
4515
4447
  /* @__PURE__ */ jsx8(Gap2, {}),
4516
- /* @__PURE__ */ jsx8(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: "Select media type. Hotkeys [l], [d], [m], [r] adjust options." }),
4448
+ /* @__PURE__ */ jsx8(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: "Select what to download from this profile." }),
4517
4449
  hasCheckpoint ? /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text7, { color: theme.primary, bold: true, children: "\u26A1 Checkpoint found (cursor saved)" }) }) : null
4518
4450
  ] }),
4519
- /* @__PURE__ */ jsxs6(Panel, { title: "Download", width: 38, children: [
4520
- !editingModal && /* @__PURE__ */ jsx8(
4521
- SelectInput,
4522
- {
4523
- indicatorComponent: ChoiceIndicator,
4524
- itemComponent: ChoiceItem,
4525
- items: items.map((item, index) => ({
4526
- key: String(index),
4527
- label: item.label,
4528
- value: item.value
4529
- })),
4530
- onSelect: (item) => handleItemSelect(item.value),
4531
- onHighlight: (item) => onHighlight?.(item.value)
4532
- }
4533
- ),
4534
- /* @__PURE__ */ jsx8(
4535
- Box6,
4536
- {
4537
- flexDirection: "column",
4538
- marginTop: 1,
4539
- borderStyle: "round",
4540
- borderColor: editingModal ? theme.primary : theme.gray,
4541
- borderDimColor: !editingModal,
4542
- paddingX: 1,
4543
- children: editingModal === "limit" ? /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
4544
- /* @__PURE__ */ jsx8(Text7, { bold: true, color: theme.primary, children: "Set post limit (e.g. 24, all):" }),
4545
- /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(
4546
- TextInput,
4547
- {
4548
- value: customLimitText,
4549
- onChange: setCustomLimitText,
4550
- onSubmit: (val) => {
4551
- const parsed = parseLimit(val);
4552
- setLimit(parsed);
4553
- setEditingModal(null);
4554
- },
4555
- width: 24
4556
- }
4557
- ) }),
4558
- /* @__PURE__ */ jsx8(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: "\u21B5 confirm \xB7 esc cancel" })
4559
- ] }) : editingModal === "date" ? /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
4560
- /* @__PURE__ */ jsx8(Text7, { bold: true, color: theme.primary, children: "Set date or year (e.g. 2021, 7d):" }),
4561
- /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(
4562
- TextInput,
4563
- {
4564
- value: customDateText,
4565
- onChange: setCustomDateText,
4566
- onSubmit: (val) => {
4567
- const trimmed = val.trim();
4568
- setDateFilter(trimmed ? trimmed : "all");
4569
- setEditingModal(null);
4570
- },
4571
- width: 24
4572
- }
4573
- ) }),
4574
- /* @__PURE__ */ jsx8(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: "\u21B5 confirm \xB7 esc cancel" })
4575
- ] }) : /* @__PURE__ */ jsxs6(Fragment2, { children: [
4576
- /* @__PURE__ */ jsxs6(Box6, { justifyContent: "space-between", children: [
4577
- /* @__PURE__ */ jsxs6(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: [
4578
- "[l] Posts: ",
4579
- /* @__PURE__ */ jsx8(Text7, { color: theme.primary, bold: true, children: limit ? `${limit}` : "ALL" })
4580
- ] }),
4581
- /* @__PURE__ */ jsxs6(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: [
4582
- "[d] Date: ",
4583
- /* @__PURE__ */ jsx8(Text7, { color: theme.primary, bold: true, children: dateFilter.toUpperCase() })
4584
- ] })
4585
- ] }),
4586
- /* @__PURE__ */ jsxs6(Box6, { justifyContent: "space-between", children: [
4587
- /* @__PURE__ */ jsxs6(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: [
4588
- "[m] Meta: ",
4589
- /* @__PURE__ */ jsx8(Text7, { color: theme.primary, bold: true, children: writeMetadata ? "ON" : "OFF" })
4590
- ] }),
4591
- /* @__PURE__ */ jsxs6(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: [
4592
- "[r] Resume: ",
4593
- /* @__PURE__ */ jsx8(Text7, { color: theme.primary, bold: true, children: resume ? "ON" : "OFF" })
4594
- ] })
4595
- ] })
4596
- ] })
4597
- }
4598
- )
4599
- ] })
4451
+ /* @__PURE__ */ jsx8(Panel, { title: "Download", width: 38, children: /* @__PURE__ */ jsx8(
4452
+ SelectInput,
4453
+ {
4454
+ indicatorComponent: ChoiceIndicator,
4455
+ itemComponent: ChoiceItem,
4456
+ items: items.map((item, index) => ({
4457
+ key: String(index),
4458
+ label: item.label,
4459
+ value: item.value
4460
+ })),
4461
+ onSelect: (item) => handleItemSelect(item.value),
4462
+ onHighlight: (item) => onHighlight?.(item.value)
4463
+ }
4464
+ ) })
4600
4465
  ] });
4601
4466
  }
4602
4467
 
@@ -4808,7 +4673,7 @@ function DownloadingView({
4808
4673
  // src/components/views/completion-view.tsx
4809
4674
  import os7 from "os";
4810
4675
  import { Box as Box10, Text as Text12 } from "ink";
4811
- import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
4676
+ import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
4812
4677
  var DONE_LABEL = "\u21B5 download another";
4813
4678
  function CompletionView({
4814
4679
  profile,
@@ -4820,10 +4685,10 @@ function CompletionView({
4820
4685
  const plural = result.downloadedCount === 1 ? "" : "s";
4821
4686
  const skipText = result.skippedCount > 0 ? ` (${result.skippedCount} archived)` : "";
4822
4687
  return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", alignItems: "center", children: [
4823
- /* @__PURE__ */ jsx13(Text12, { children: isZero ? /* @__PURE__ */ jsxs10(Fragment3, { children: [
4688
+ /* @__PURE__ */ jsx13(Text12, { children: isZero ? /* @__PURE__ */ jsxs10(Fragment2, { children: [
4824
4689
  /* @__PURE__ */ jsx13(Text12, { bold: true, color: theme.primary, children: "\u2713 profile scanned " }),
4825
4690
  /* @__PURE__ */ jsx13(Text12, { color: theme.primary, children: "\u2014 0 media items found" })
4826
- ] }) : /* @__PURE__ */ jsxs10(Fragment3, { children: [
4691
+ ] }) : /* @__PURE__ */ jsxs10(Fragment2, { children: [
4827
4692
  /* @__PURE__ */ jsx13(Text12, { bold: true, color: theme.primary, children: "\u2713 profile downloaded! " }),
4828
4693
  /* @__PURE__ */ jsx13(Text12, { color: theme.primary, children: "saved to:" })
4829
4694
  ] }) }),
@@ -4870,8 +4735,8 @@ function ErrorView({ message, hint, columns, theme }) {
4870
4735
  }
4871
4736
 
4872
4737
  // src/components/views/settings-view.tsx
4873
- import { useState as useState6, useMemo as useMemo2, useCallback as useCallback2 } from "react";
4874
- import { Box as Box12, Text as Text14, useInput as useInput3 } from "ink";
4738
+ import { useState as useState6, useMemo as useMemo2, useCallback } from "react";
4739
+ import { Box as Box12, Text as Text14, useInput as useInput2 } from "ink";
4875
4740
  import fs11 from "fs";
4876
4741
  import path11 from "path";
4877
4742
  import os8 from "os";
@@ -4895,7 +4760,7 @@ function SettingsView({
4895
4760
  const initialConfig = useMemo2(() => loadConfig(), []);
4896
4761
  const [internalFocus, setInternalFocus] = useState6("cookies");
4897
4762
  const focusedField = controlledFocus ?? internalFocus;
4898
- const setField = useCallback2(
4763
+ const setField = useCallback(
4899
4764
  (field) => {
4900
4765
  setInternalFocus(field);
4901
4766
  onFocusChange?.(field);
@@ -4907,7 +4772,7 @@ function SettingsView({
4907
4772
  const [writeMetadata, setWriteMetadata] = useState6(Boolean(initialConfig.writeMetadata));
4908
4773
  const [defaultLimit, setDefaultLimit] = useState6(initialConfig.defaultLimit);
4909
4774
  const [saved, setSaved] = useState6(false);
4910
- const handleSave = useCallback2(() => {
4775
+ const handleSave = useCallback(() => {
4911
4776
  const existing = loadConfig();
4912
4777
  const nextConfig = {
4913
4778
  ...existing,
@@ -4955,20 +4820,20 @@ function SettingsView({
4955
4820
  }
4956
4821
  return `target: ${path11.resolve(expandDir(dir))}`;
4957
4822
  }, [downloadDir]);
4958
- const cycleFocus = useCallback2((forward = true) => {
4823
+ const cycleFocus = useCallback((forward = true) => {
4959
4824
  const fields = ["cookies", "download", "metadata", "limit"];
4960
4825
  const idx = fields.indexOf(focusedField);
4961
4826
  const nextIdx = forward ? (idx + 1) % fields.length : (idx - 1 + fields.length) % fields.length;
4962
4827
  setField(fields[nextIdx]);
4963
4828
  }, [focusedField, setField]);
4964
- const cycleLimit = useCallback2(() => {
4829
+ const cycleLimit = useCallback(() => {
4965
4830
  setDefaultLimit((prev) => {
4966
4831
  const idx = LIMIT_OPTIONS.indexOf(prev);
4967
4832
  const nextIdx = (idx + 1) % LIMIT_OPTIONS.length;
4968
4833
  return LIMIT_OPTIONS[nextIdx];
4969
4834
  });
4970
4835
  }, []);
4971
- useInput3((input, key) => {
4836
+ useInput2((input, key) => {
4972
4837
  if (key.escape) {
4973
4838
  onCancel();
4974
4839
  return;
@@ -5096,8 +4961,8 @@ function SettingsView({
5096
4961
  }
5097
4962
 
5098
4963
  // src/components/views/watcher-view.tsx
5099
- import { useState as useState7, useEffect as useEffect6, useRef as useRef3, useCallback as useCallback3 } from "react";
5100
- import { Box as Box13, Text as Text15, useInput as useInput4 } from "ink";
4964
+ import { useState as useState7, useEffect as useEffect6, useRef as useRef3, useCallback as useCallback2 } from "react";
4965
+ import { Box as Box13, Text as Text15, useInput as useInput3 } from "ink";
5101
4966
  import Spinner2 from "ink-spinner";
5102
4967
 
5103
4968
  // src/lib/clipboard.ts
@@ -5352,9 +5217,9 @@ function startClipboardWatcher(options = {}) {
5352
5217
  try {
5353
5218
  const res = await dispatch({
5354
5219
  profile: parsed,
5355
- choice,
5356
- outputDir: options.outputDir,
5357
- writeMetadata: options.writeMetadata,
5220
+ choice: options.getChoice ? options.getChoice() : options.choice || "all",
5221
+ outputDir: resolveDefaultDownloadDir(parsed.platform, parsed.username, options.outputDir),
5222
+ writeMetadata: options.getWriteMetadata ? options.getWriteMetadata() : options.writeMetadata ?? false,
5358
5223
  signal: options.signal,
5359
5224
  onProgress: (p) => {
5360
5225
  if (p.statusText) {
@@ -5444,16 +5309,19 @@ function revealInFileManager(targetPath) {
5444
5309
 
5445
5310
  // src/components/views/watcher-view.tsx
5446
5311
  import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
5312
+ var CHOICES = ["all", "photos", "videos", "stories", "highlights"];
5447
5313
  function WatcherView({
5448
5314
  onBack,
5449
5315
  outputDir,
5450
5316
  theme,
5451
5317
  gapComponent: Gap2,
5452
5318
  defaultChoice = "all",
5453
- defaultWriteMetadata = false
5319
+ defaultWriteMetadata = false,
5320
+ onRegisterClickTargets
5454
5321
  }) {
5455
5322
  const [mediaChoice, setMediaChoice] = useState7(defaultChoice);
5456
5323
  const [writeMetadata, setWriteMetadata] = useState7(defaultWriteMetadata);
5324
+ const [focusedIndex, setFocusedIndex] = useState7(0);
5457
5325
  const [history, setHistory] = useState7([]);
5458
5326
  const [activeJob, setActiveJob] = useState7(null);
5459
5327
  const [stats, setStats] = useState7({ detected: 0, downloaded: 0, skipped: 0 });
@@ -5471,8 +5339,10 @@ function WatcherView({
5471
5339
  useEffect6(() => {
5472
5340
  const watcher = startClipboardWatcher({
5473
5341
  choice: mediaChoiceRef.current,
5342
+ getChoice: () => mediaChoiceRef.current,
5474
5343
  outputDir,
5475
5344
  writeMetadata: writeMetadataRef.current,
5345
+ getWriteMetadata: () => writeMetadataRef.current,
5476
5346
  onDetected: (profile) => {
5477
5347
  setStats((prev) => ({ ...prev, detected: prev.detected + 1 }));
5478
5348
  const newItem = {
@@ -5526,34 +5396,82 @@ function WatcherView({
5526
5396
  watcher.stop();
5527
5397
  };
5528
5398
  }, [outputDir]);
5529
- const cycleChoice = useCallback3(() => {
5399
+ const cycleChoice = useCallback2(() => {
5530
5400
  setMediaChoice((prev) => {
5531
- if (prev === "all") return "photos";
5532
- if (prev === "photos") return "videos";
5533
- return "all";
5401
+ const idx = CHOICES.indexOf(prev);
5402
+ const nextChoice = CHOICES[(idx + 1) % CHOICES.length] || "all";
5403
+ return nextChoice;
5534
5404
  });
5535
5405
  }, []);
5536
- const toggleMetadata = useCallback3(() => {
5406
+ const toggleMetadata = useCallback2(() => {
5537
5407
  setWriteMetadata((prev) => !prev);
5538
5408
  }, []);
5539
- const openFolder = useCallback3(() => {
5409
+ const openFolder = useCallback2(() => {
5540
5410
  const target = outputDir || resolveDefaultDownloadDir("instagram", "watcher");
5541
5411
  revealInFileManager(target);
5542
5412
  }, [outputDir]);
5543
- useInput4((input, key) => {
5413
+ useEffect6(() => {
5414
+ if (!onRegisterClickTargets) return;
5415
+ const targets = [
5416
+ { match: "Target:", padX: 1, action: () => {
5417
+ setFocusedIndex(0);
5418
+ cycleChoice();
5419
+ } },
5420
+ { match: "Sidecars:", padX: 1, action: () => {
5421
+ setFocusedIndex(1);
5422
+ toggleMetadata();
5423
+ } },
5424
+ { match: "Open Folder", padX: 1, action: () => {
5425
+ setFocusedIndex(2);
5426
+ openFolder();
5427
+ } },
5428
+ { match: "c target", padX: 1, action: () => {
5429
+ setFocusedIndex(0);
5430
+ cycleChoice();
5431
+ } },
5432
+ { match: "m sidecar", padX: 1, action: () => {
5433
+ setFocusedIndex(1);
5434
+ toggleMetadata();
5435
+ } },
5436
+ { match: "o open dir", padX: 1, action: () => {
5437
+ setFocusedIndex(2);
5438
+ openFolder();
5439
+ } },
5440
+ { match: "esc back", padX: 1, action: onBack }
5441
+ ];
5442
+ onRegisterClickTargets(targets);
5443
+ }, [onRegisterClickTargets, cycleChoice, toggleMetadata, openFolder, onBack, mediaChoice, writeMetadata]);
5444
+ useInput3((input, key) => {
5544
5445
  if (key.escape) {
5545
5446
  onBack();
5546
5447
  return;
5547
5448
  }
5449
+ if (key.leftArrow) {
5450
+ setFocusedIndex((prev) => prev === 0 ? 2 : prev - 1);
5451
+ return;
5452
+ }
5453
+ if (key.rightArrow || key.tab) {
5454
+ setFocusedIndex((prev) => prev === 2 ? 0 : prev + 1);
5455
+ return;
5456
+ }
5457
+ if (key.return || input === " ") {
5458
+ if (focusedIndex === 0) cycleChoice();
5459
+ else if (focusedIndex === 1) toggleMetadata();
5460
+ else if (focusedIndex === 2) openFolder();
5461
+ return;
5462
+ }
5548
5463
  if (input === "c" || input === "C") {
5464
+ setFocusedIndex(0);
5549
5465
  cycleChoice();
5550
5466
  return;
5551
5467
  }
5552
5468
  if (input === "m" || input === "M") {
5469
+ setFocusedIndex(1);
5553
5470
  toggleMetadata();
5554
5471
  return;
5555
5472
  }
5556
5473
  if (input === "o" || input === "O") {
5474
+ setFocusedIndex(2);
5557
5475
  openFolder();
5558
5476
  return;
5559
5477
  }
@@ -5569,8 +5487,10 @@ function WatcherView({
5569
5487
  ] }),
5570
5488
  /* @__PURE__ */ jsx16(ProgressBar, { indeterminate: true, width: 58, tick: pulseTick }),
5571
5489
  /* @__PURE__ */ jsx16(Gap2, {}),
5572
- /* @__PURE__ */ jsx16(Text15, { color: theme.gray, dimColor: theme.dimSecondary, children: "Listening for Instagram, TikTok, Facebook, or X profile links in clipboard." }),
5573
- /* @__PURE__ */ jsx16(Text15, { color: theme.gray, dimColor: theme.dimSecondary, children: "Simply copy any profile link in your browser and Open Nami will download it!" }),
5490
+ /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", children: [
5491
+ /* @__PURE__ */ jsx16(Text15, { color: theme.gray, dimColor: theme.dimSecondary, children: "Auto-monitoring clipboard for profile links (Instagram, TikTok, FB, X)." }),
5492
+ /* @__PURE__ */ jsx16(Text15, { color: theme.gray, dimColor: theme.dimSecondary, children: "Copy any profile link in your browser: Open Nami downloads it in background." })
5493
+ ] }),
5574
5494
  /* @__PURE__ */ jsx16(Gap2, {}),
5575
5495
  /* @__PURE__ */ jsx16(
5576
5496
  Box13,
@@ -5606,15 +5526,42 @@ function WatcherView({
5606
5526
  ] }) : null,
5607
5527
  /* @__PURE__ */ jsx16(Gap2, {}),
5608
5528
  /* @__PURE__ */ jsxs13(Box13, { justifyContent: "space-between", children: [
5609
- /* @__PURE__ */ jsxs13(Text15, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5610
- "[c] Target: ",
5611
- /* @__PURE__ */ jsx16(Text15, { color: theme.primary, bold: true, children: mediaChoice.toUpperCase() })
5612
- ] }),
5613
- /* @__PURE__ */ jsxs13(Text15, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5614
- "[m] Sidecars: ",
5615
- /* @__PURE__ */ jsx16(Text15, { color: theme.primary, bold: true, children: writeMetadata ? "ON" : "OFF" })
5616
- ] }),
5617
- /* @__PURE__ */ jsx16(Text15, { color: theme.gray, dimColor: theme.dimSecondary, children: "[o] Open Folder" })
5529
+ /* @__PURE__ */ jsx16(
5530
+ Box13,
5531
+ {
5532
+ borderStyle: "round",
5533
+ borderColor: focusedIndex === 0 ? theme.primary : theme.gray,
5534
+ borderDimColor: focusedIndex !== 0,
5535
+ paddingX: 1,
5536
+ children: /* @__PURE__ */ jsxs13(Text15, { color: focusedIndex === 0 ? theme.primary : theme.gray, bold: focusedIndex === 0, children: [
5537
+ "[c] Target: ",
5538
+ /* @__PURE__ */ jsx16(Text15, { color: theme.primary, bold: true, children: mediaChoice === "all" ? "EVERYTHING" : mediaChoice.toUpperCase() })
5539
+ ] })
5540
+ }
5541
+ ),
5542
+ /* @__PURE__ */ jsx16(
5543
+ Box13,
5544
+ {
5545
+ borderStyle: "round",
5546
+ borderColor: focusedIndex === 1 ? theme.primary : theme.gray,
5547
+ borderDimColor: focusedIndex !== 1,
5548
+ paddingX: 1,
5549
+ children: /* @__PURE__ */ jsxs13(Text15, { color: focusedIndex === 1 ? theme.primary : theme.gray, bold: focusedIndex === 1, children: [
5550
+ "[m] Sidecars: ",
5551
+ /* @__PURE__ */ jsx16(Text15, { color: theme.primary, bold: true, children: writeMetadata ? "ON" : "OFF" })
5552
+ ] })
5553
+ }
5554
+ ),
5555
+ /* @__PURE__ */ jsx16(
5556
+ Box13,
5557
+ {
5558
+ borderStyle: "round",
5559
+ borderColor: focusedIndex === 2 ? theme.primary : theme.gray,
5560
+ borderDimColor: focusedIndex !== 2,
5561
+ paddingX: 1,
5562
+ children: /* @__PURE__ */ jsx16(Text15, { color: focusedIndex === 2 ? theme.primary : theme.gray, bold: focusedIndex === 2, children: "[o] Open Folder" })
5563
+ }
5564
+ )
5618
5565
  ] }),
5619
5566
  /* @__PURE__ */ jsx16(Box13, { marginTop: 1, justifyContent: "center", children: /* @__PURE__ */ jsxs13(Text15, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5620
5567
  "Session Stats: ",
@@ -5629,8 +5576,8 @@ function WatcherView({
5629
5576
  }
5630
5577
 
5631
5578
  // src/components/views/batch-view.tsx
5632
- import { useState as useState8, useRef as useRef4, useCallback as useCallback4, useEffect as useEffect7 } from "react";
5633
- import { Box as Box14, Text as Text16, useInput as useInput5 } from "ink";
5579
+ import { useState as useState8, useRef as useRef4, useCallback as useCallback3, useEffect as useEffect7 } from "react";
5580
+ import { Box as Box14, Text as Text16, useInput as useInput4 } from "ink";
5634
5581
  import Spinner3 from "ink-spinner";
5635
5582
  import fs13 from "fs";
5636
5583
 
@@ -5754,11 +5701,11 @@ async function runBatchQueue(options) {
5754
5701
  }
5755
5702
 
5756
5703
  // src/components/views/batch-view.tsx
5757
- import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
5704
+ import { Fragment as Fragment3, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
5758
5705
  var PLATFORMS = ["instagram", "tiktok", "facebook", "x"];
5759
- var CHOICES = ["all", "photos", "videos", "stories", "highlights"];
5760
- var DATE_PRESETS2 = ["all", "24h", "7d", "30d", "2024", "2023", "2022", "2021", "custom"];
5761
- var LIMIT_PRESETS2 = [void 0, 10, 24, 50, 100, "custom"];
5706
+ var CHOICES2 = ["all", "photos", "videos", "stories", "highlights"];
5707
+ var DATE_PRESETS = ["all", "24h", "7d", "30d", "2024", "2023", "2022", "2021", "custom"];
5708
+ var LIMIT_PRESETS = [void 0, 10, 24, 50, 100, "custom"];
5762
5709
  function BatchView({
5763
5710
  onBack,
5764
5711
  outputDir,
@@ -5790,22 +5737,22 @@ function BatchView({
5790
5737
  const [isCooldown, setIsCooldown] = useState8(false);
5791
5738
  const [result, setResult] = useState8(null);
5792
5739
  const abortControllerRef = useRef4(null);
5793
- const cyclePlatform = useCallback4(() => {
5740
+ const cyclePlatform = useCallback3(() => {
5794
5741
  setDefaultPlatform((prev) => {
5795
5742
  const idx = PLATFORMS.indexOf(prev);
5796
5743
  return PLATFORMS[(idx + 1) % PLATFORMS.length];
5797
5744
  });
5798
5745
  }, []);
5799
- const cycleChoice = useCallback4(() => {
5746
+ const cycleChoice = useCallback3(() => {
5800
5747
  setChoice((prev) => {
5801
- const idx = CHOICES.indexOf(prev);
5802
- return CHOICES[(idx + 1) % CHOICES.length];
5748
+ const idx = CHOICES2.indexOf(prev);
5749
+ return CHOICES2[(idx + 1) % CHOICES2.length];
5803
5750
  });
5804
5751
  }, []);
5805
- const cycleLimit = useCallback4(() => {
5806
- const curIdx = LIMIT_PRESETS2.findIndex((p) => p === limit);
5807
- const nextIdx = (curIdx + 1) % LIMIT_PRESETS2.length;
5808
- const nextVal = LIMIT_PRESETS2[nextIdx];
5752
+ const cycleLimit = useCallback3(() => {
5753
+ const curIdx = LIMIT_PRESETS.findIndex((p) => p === limit);
5754
+ const nextIdx = (curIdx + 1) % LIMIT_PRESETS.length;
5755
+ const nextVal = LIMIT_PRESETS[nextIdx];
5809
5756
  if (nextVal === "custom") {
5810
5757
  setCustomLimitText(limit ? String(limit) : "24");
5811
5758
  setFocusedControl("custom-limit");
@@ -5813,10 +5760,10 @@ function BatchView({
5813
5760
  setLimit(nextVal);
5814
5761
  }
5815
5762
  }, [limit]);
5816
- const cycleDate = useCallback4(() => {
5817
- const curIdx = DATE_PRESETS2.indexOf(dateFilter);
5818
- const nextIdx = (curIdx + 1) % DATE_PRESETS2.length;
5819
- const nextVal = DATE_PRESETS2[nextIdx];
5763
+ const cycleDate = useCallback3(() => {
5764
+ const curIdx = DATE_PRESETS.indexOf(dateFilter);
5765
+ const nextIdx = (curIdx + 1) % DATE_PRESETS.length;
5766
+ const nextVal = DATE_PRESETS[nextIdx];
5820
5767
  if (nextVal === "custom") {
5821
5768
  setCustomDateText(dateFilter !== "all" ? dateFilter : "2024");
5822
5769
  setFocusedControl("custom-date");
@@ -5824,13 +5771,13 @@ function BatchView({
5824
5771
  setDateFilter(nextVal);
5825
5772
  }
5826
5773
  }, [dateFilter]);
5827
- const toggleMetadata = useCallback4(() => {
5774
+ const toggleMetadata = useCallback3(() => {
5828
5775
  setWriteMetadata((prev) => !prev);
5829
5776
  }, []);
5830
- const toggleResume = useCallback4(() => {
5777
+ const toggleResume = useCallback3(() => {
5831
5778
  setResume((prev) => !prev);
5832
5779
  }, []);
5833
- const startQueue = useCallback4(async () => {
5780
+ const startQueue = useCallback3(async () => {
5834
5781
  setErrorMessage(null);
5835
5782
  let lines = [];
5836
5783
  if (inputField === "file") {
@@ -5917,7 +5864,7 @@ function BatchView({
5917
5864
  setPhase("input");
5918
5865
  }
5919
5866
  }, [inputField, filePath, rawText, choice, defaultPlatform, outputDir, dateFilter, limit, resume, writeMetadata]);
5920
- useInput5((input, key) => {
5867
+ useInput4((input, key) => {
5921
5868
  if (key.ctrl && (input === "c" || input === "C")) {
5922
5869
  return;
5923
5870
  }
@@ -6461,7 +6408,7 @@ function BatchView({
6461
6408
  }
6462
6409
  ) }),
6463
6410
  /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: "\u21B5 confirm \xB7 esc cancel" })
6464
- ] }) : /* @__PURE__ */ jsxs14(Fragment4, { children: [
6411
+ ] }) : /* @__PURE__ */ jsxs14(Fragment3, { children: [
6465
6412
  /* @__PURE__ */ jsxs14(Box14, { justifyContent: "space-between", width: "100%", children: [
6466
6413
  /* @__PURE__ */ jsxs14(Box14, { width: colWidth, children: [
6467
6414
  /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: "[p] Platform: " }),
@@ -6673,7 +6620,7 @@ function frameRowSpan(row) {
6673
6620
  }
6674
6621
 
6675
6622
  // src/app.tsx
6676
- import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
6623
+ import { Fragment as Fragment4, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
6677
6624
  var TAGLINE = "grab any profile. paste. download. done.";
6678
6625
  var DOWNLOAD_BUTTON = "download";
6679
6626
  var Gap = ({ lines = 1 }) => /* @__PURE__ */ jsx18(Box15, { flexDirection: "column", flexShrink: 0, children: Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsx18(Text17, { children: " " }, i)) });
@@ -6752,7 +6699,7 @@ function App({
6752
6699
  version = "1.0.0"
6753
6700
  }) {
6754
6701
  const [themeMode, setThemeMode] = useState9(initialThemeMode);
6755
- const cycleTheme = useCallback5(() => {
6702
+ const cycleTheme = useCallback4(() => {
6756
6703
  setThemeMode(nextThemeMode);
6757
6704
  }, []);
6758
6705
  return /* @__PURE__ */ jsx18(ThemeProvider, { mode: themeMode, children: /* @__PURE__ */ jsx18(
@@ -6793,6 +6740,7 @@ function InnerApp({
6793
6740
  const [clipboardUrl, setClipboardUrl] = useState9(void 0);
6794
6741
  const [settingsFocus, setSettingsFocus] = useState9("cookies");
6795
6742
  const [batchClickTargets, setBatchClickTargets] = useState9([]);
6743
+ const [watcherClickTargets, setWatcherClickTargets] = useState9([]);
6796
6744
  const columns = stdout?.columns && stdout.columns > 0 ? stdout.columns : 80;
6797
6745
  const boxWidth = Math.max(14, Math.min(64, columns - 6));
6798
6746
  const contentWidth = Math.max(10, Math.min(columns - 4, 78));
@@ -6829,7 +6777,7 @@ function InnerApp({
6829
6777
  }
6830
6778
  };
6831
6779
  }, []);
6832
- const resetToInput = useCallback5(() => {
6780
+ const resetToInput = useCallback4(() => {
6833
6781
  if (probingTimeoutRef.current) {
6834
6782
  clearTimeout(probingTimeoutRef.current);
6835
6783
  probingTimeoutRef.current = null;
@@ -6839,7 +6787,7 @@ function InnerApp({
6839
6787
  setUrlInput("");
6840
6788
  setPhase({ name: "input" });
6841
6789
  }, []);
6842
- const handleModeSelect = useCallback5(async (choiceOrOptions, profile) => {
6790
+ const handleModeSelect = useCallback4(async (choiceOrOptions, profile) => {
6843
6791
  abortControllerRef.current?.abort();
6844
6792
  const controller = new AbortController();
6845
6793
  abortControllerRef.current = controller;
@@ -6889,7 +6837,7 @@ function InnerApp({
6889
6837
  });
6890
6838
  }
6891
6839
  }, [outputDir, since, until, effectiveLimit, resume, effectiveWriteMetadata]);
6892
- const handlePlatformSelect = useCallback5((platform, username, rawInput) => {
6840
+ const handlePlatformSelect = useCallback4((platform, username, rawInput) => {
6893
6841
  const canonical = buildCanonicalProfile(platform, username, rawInput);
6894
6842
  setPhase({ name: "probing", target: canonical.cleanUrl, status: "probing profile\u2026" });
6895
6843
  if (probingTimeoutRef.current) clearTimeout(probingTimeoutRef.current);
@@ -6902,7 +6850,7 @@ function InnerApp({
6902
6850
  }
6903
6851
  }, 450);
6904
6852
  }, [initialChoice, handleModeSelect]);
6905
- const handleInputSubmit = useCallback5((input) => {
6853
+ const handleInputSubmit = useCallback4((input) => {
6906
6854
  const trimmed = input.trim();
6907
6855
  if (!trimmed) return;
6908
6856
  const parsed = parseProfileInput(trimmed);
@@ -6928,7 +6876,7 @@ function InnerApp({
6928
6876
  warning: "paste a full profile url (e.g. instagram.com/profile) or @profile"
6929
6877
  });
6930
6878
  }, [initialChoice, handleModeSelect]);
6931
- useInput6((input, key) => {
6879
+ useInput5((input, key) => {
6932
6880
  if (key.ctrl && input === "t") {
6933
6881
  cycleTheme();
6934
6882
  return;
@@ -6944,8 +6892,11 @@ function InnerApp({
6944
6892
  if (key.ctrl && (input === "w" || input === "W")) {
6945
6893
  if (phase.name === "watcher") {
6946
6894
  setPhase({ name: "input" });
6947
- } else if (phase.name === "input") {
6895
+ return;
6896
+ }
6897
+ if (phase.name === "input" && urlInput.length === 0) {
6948
6898
  setPhase({ name: "watcher" });
6899
+ return;
6949
6900
  }
6950
6901
  return;
6951
6902
  }
@@ -7063,6 +7014,9 @@ function InnerApp({
7063
7014
  if (phase.name === "batch") {
7064
7015
  targets.push(...batchClickTargets);
7065
7016
  }
7017
+ if (phase.name === "watcher") {
7018
+ targets.push(...watcherClickTargets);
7019
+ }
7066
7020
  for (const [key, label] of hints) {
7067
7021
  const action = hintAction(key);
7068
7022
  if (action) targets.push({ match: `${key} ${label}`, action });
@@ -7083,7 +7037,8 @@ function InnerApp({
7083
7037
  handlePlatformSelect,
7084
7038
  handleModeSelect,
7085
7039
  resetToInput,
7086
- batchClickTargets
7040
+ batchClickTargets,
7041
+ watcherClickTargets
7087
7042
  ]);
7088
7043
  useMouseClick(
7089
7044
  (x, y) => {
@@ -7210,7 +7165,8 @@ function InnerApp({
7210
7165
  theme,
7211
7166
  gapComponent: Gap,
7212
7167
  defaultChoice: initialChoice,
7213
- defaultWriteMetadata: effectiveWriteMetadata
7168
+ defaultWriteMetadata: effectiveWriteMetadata,
7169
+ onRegisterClickTargets: setWatcherClickTargets
7214
7170
  }
7215
7171
  ),
7216
7172
  phase.name === "batch" && /* @__PURE__ */ jsx18(
@@ -7228,7 +7184,7 @@ function InnerApp({
7228
7184
  onRegisterClickTargets: setBatchClickTargets
7229
7185
  }
7230
7186
  ),
7231
- hints.length > 0 ? /* @__PURE__ */ jsxs15(Fragment5, { children: [
7187
+ hints.length > 0 ? /* @__PURE__ */ jsxs15(Fragment4, { children: [
7232
7188
  /* @__PURE__ */ jsx18(Gap, { lines: 2 }),
7233
7189
  /* @__PURE__ */ jsx18(
7234
7190
  Shortcuts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-nami",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "description": "Open Nami: terminal-first bulk social media profile downloader for Instagram, TikTok, Facebook & X.",
5
5
  "type": "module",
6
6
  "bin": {