open-nami 1.2.3 → 1.2.4

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 +332 -127
  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.3",
12
+ version: "1.2.4",
13
13
  description: "Open Nami: terminal-first bulk social media profile downloader for Instagram, TikTok, Facebook & X.",
14
14
  type: "module",
15
15
  bin: {
@@ -287,15 +287,17 @@ function FramedInput({
287
287
  children
288
288
  }) {
289
289
  const theme = useTheme();
290
- const inner = width - 2;
291
- const tail = Math.max(0, inner - title.length - 3);
290
+ const inner = Math.max(0, width - 2);
291
+ const maxTitleLen = Math.max(0, inner - 4);
292
+ const displayTitle = title.length > maxTitleLen ? title.slice(0, Math.max(0, maxTitleLen - 1)) + "\u2026" : title;
293
+ const tail = Math.max(0, inner - displayTitle.length - 3);
292
294
  const buttonW = button ? frameButtonWidth(button) : 0;
293
295
  const fillColor = buttonDim ? theme.gray : theme.primary;
294
296
  return /* @__PURE__ */ jsxs2(Box3, { width: width + buttonW, children: [
295
297
  /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", width, children: [
296
- /* @__PURE__ */ jsxs2(Text3, { children: [
298
+ /* @__PURE__ */ jsxs2(Text3, { wrap: "truncate-end", children: [
297
299
  /* @__PURE__ */ jsx4(Text3, { color: theme.gray, dimColor: theme.dimSecondary, children: "\u256D\u2500 " }),
298
- /* @__PURE__ */ jsx4(Text3, { color: theme.primary, children: title }),
300
+ /* @__PURE__ */ jsx4(Text3, { color: theme.primary, children: displayTitle }),
299
301
  /* @__PURE__ */ jsx4(Text3, { color: theme.gray, dimColor: theme.dimSecondary, children: ` ${"\u2500".repeat(tail)}${button ? "\u2500" : "\u256E"}` })
300
302
  ] }),
301
303
  /* @__PURE__ */ jsxs2(Box3, { width, height: 1, overflow: "hidden", children: [
@@ -661,12 +663,14 @@ import { Box as Box5, Text as Text6 } from "ink";
661
663
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
662
664
  function Panel({ title, width, children }) {
663
665
  const theme = useTheme();
664
- const inner = width - 2;
665
- const tail = Math.max(0, inner - title.length - 3);
666
+ const inner = Math.max(0, width - 2);
667
+ const maxTitleLen = Math.max(0, inner - 4);
668
+ const displayTitle = title.length > maxTitleLen ? title.slice(0, Math.max(0, maxTitleLen - 1)) + "\u2026" : title;
669
+ const tail = Math.max(0, inner - displayTitle.length - 3);
666
670
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width, children: [
667
- /* @__PURE__ */ jsxs5(Text6, { children: [
671
+ /* @__PURE__ */ jsxs5(Text6, { wrap: "truncate-end", children: [
668
672
  /* @__PURE__ */ jsx7(Text6, { color: theme.gray, dimColor: theme.dimSecondary, children: "\u256D\u2500 " }),
669
- /* @__PURE__ */ jsx7(Text6, { color: theme.primary, children: title }),
673
+ /* @__PURE__ */ jsx7(Text6, { color: theme.primary, children: displayTitle }),
670
674
  /* @__PURE__ */ jsx7(Text6, { color: theme.gray, dimColor: theme.dimSecondary, children: ` ${"\u2500".repeat(tail)}\u256E` })
671
675
  ] }),
672
676
  /* @__PURE__ */ jsx7(
@@ -3323,6 +3327,15 @@ function parseDateBoundary(input, boundary) {
3323
3327
  return nowSeconds - offsetSeconds;
3324
3328
  }
3325
3329
  if (/^\d+$/.test(trimmed)) {
3330
+ if (trimmed.length === 4) {
3331
+ const year = parseInt(trimmed, 10);
3332
+ if (year >= 1990 && year <= 2100) {
3333
+ const date = boundary === "until" ? new Date(Date.UTC(year, 11, 31, 23, 59, 59, 999)) : new Date(Date.UTC(year, 0, 1, 0, 0, 0, 0));
3334
+ if (!isNaN(date.getTime())) {
3335
+ return Math.floor(date.getTime() / 1e3);
3336
+ }
3337
+ }
3338
+ }
3326
3339
  if (trimmed.length === 8) {
3327
3340
  const year = parseInt(trimmed.slice(0, 4), 10);
3328
3341
  const month = parseInt(trimmed.slice(4, 6), 10) - 1;
@@ -3338,6 +3351,14 @@ function parseDateBoundary(input, boundary) {
3338
3351
  }
3339
3352
  return num;
3340
3353
  }
3354
+ if (/^\d{4}-\d{2}$/.test(trimmed)) {
3355
+ const year = parseInt(trimmed.slice(0, 4), 10);
3356
+ const month = parseInt(trimmed.slice(5, 7), 10);
3357
+ const date = boundary === "until" ? new Date(Date.UTC(year, month, 0, 23, 59, 59, 999)) : new Date(Date.UTC(year, month - 1, 1, 0, 0, 0, 0));
3358
+ if (!isNaN(date.getTime())) {
3359
+ return Math.floor(date.getTime() / 1e3);
3360
+ }
3361
+ }
3341
3362
  if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
3342
3363
  const timeStr = boundary === "until" ? "T23:59:59.999Z" : "T00:00:00.000Z";
3343
3364
  const parsed2 = /* @__PURE__ */ new Date(`${trimmed}${timeStr}`);
@@ -3351,8 +3372,34 @@ function parseDateBoundary(input, boundary) {
3351
3372
  }
3352
3373
  return void 0;
3353
3374
  }
3375
+ function resolveDateFilter(input) {
3376
+ if (!input) return {};
3377
+ const trimmed = input.trim();
3378
+ if (!trimmed || trimmed.toLowerCase() === "all" || trimmed.toLowerCase() === "none") return {};
3379
+ if (trimmed.includes("..") || /\s+-\s+/.test(trimmed)) {
3380
+ const parts = trimmed.split(/\.\.|\s+-\s+/);
3381
+ return {
3382
+ since: parts[0]?.trim(),
3383
+ until: parts[1]?.trim()
3384
+ };
3385
+ }
3386
+ if (/^\d{4}$/.test(trimmed) || /^\d{4}-\d{2}$/.test(trimmed)) {
3387
+ return {
3388
+ since: trimmed,
3389
+ until: trimmed
3390
+ };
3391
+ }
3392
+ return {
3393
+ since: trimmed,
3394
+ until: void 0
3395
+ };
3396
+ }
3354
3397
  function parseLimit(input) {
3355
3398
  if (input === void 0 || input === null) return void 0;
3399
+ if (typeof input === "string") {
3400
+ const trimmed = input.trim().toLowerCase();
3401
+ if (!trimmed || trimmed === "all" || trimmed === "none" || trimmed === "0") return void 0;
3402
+ }
3356
3403
  const num = typeof input === "number" ? input : parseInt(String(input).trim(), 10);
3357
3404
  if (isNaN(num) || num <= 0) return void 0;
3358
3405
  return Math.floor(num);
@@ -4321,7 +4368,7 @@ async function dispatchDownload(options) {
4321
4368
  }
4322
4369
 
4323
4370
  // src/components/views/picking-view.tsx
4324
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
4371
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
4325
4372
  function ChoiceIndicator({ isSelected }) {
4326
4373
  const theme = useTheme();
4327
4374
  return /* @__PURE__ */ jsx8(Box6, { marginRight: 1, children: /* @__PURE__ */ jsx8(Text7, { color: theme.primary, children: isSelected ? "\u276F" : " " }) });
@@ -4345,6 +4392,8 @@ function getPickingOptions(platform) {
4345
4392
  });
4346
4393
  return items;
4347
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"];
4348
4397
  function PickingView({
4349
4398
  contentWidth,
4350
4399
  profile,
@@ -4362,14 +4411,17 @@ function PickingView({
4362
4411
  const items = getPickingOptions(profile.platform);
4363
4412
  const [limit, setLimit] = useState4(initialLimit);
4364
4413
  const [dateFilter, setDateFilter] = useState4(() => {
4365
- if (typeof initialSince === "string" && (initialSince === "24h" || initialSince === "7d" || initialSince === "30d")) {
4366
- return initialSince;
4414
+ if (typeof initialSince === "string" && initialSince.trim()) {
4415
+ return initialSince.trim();
4367
4416
  }
4368
4417
  return "all";
4369
4418
  });
4370
4419
  const [writeMetadata, setWriteMetadata] = useState4(initialWriteMetadata);
4371
4420
  const [resume, setResume] = useState4(initialResume);
4372
4421
  const [hasCheckpoint, setHasCheckpoint] = useState4(false);
4422
+ const [editingModal, setEditingModal] = useState4(null);
4423
+ const [customLimitText, setCustomLimitText] = useState4("");
4424
+ const [customDateText, setCustomDateText] = useState4("");
4373
4425
  const targetDir = resolveDefaultDownloadDir(profile.platform, profile.username, outputDir);
4374
4426
  useEffect4(() => {
4375
4427
  let isMounted = true;
@@ -4385,29 +4437,40 @@ function PickingView({
4385
4437
  };
4386
4438
  }, [targetDir]);
4387
4439
  const cycleLimit = useCallback(() => {
4388
- setLimit((prev) => {
4389
- if (!prev) return 10;
4390
- if (prev === 10) return 25;
4391
- if (prev === 25) return 50;
4392
- if (prev === 50) return 100;
4393
- return void 0;
4394
- });
4395
- }, []);
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]);
4396
4450
  const cycleDate = useCallback(() => {
4397
- setDateFilter((prev) => {
4398
- if (prev === "all") return "24h";
4399
- if (prev === "24h") return "7d";
4400
- if (prev === "7d") return "30d";
4401
- return "all";
4402
- });
4403
- }, []);
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]);
4404
4461
  const toggleMetadata = useCallback(() => {
4405
4462
  setWriteMetadata((prev) => !prev);
4406
4463
  }, []);
4407
4464
  const toggleResume = useCallback(() => {
4408
4465
  setResume((prev) => !prev);
4409
4466
  }, []);
4410
- useInput2((input) => {
4467
+ useInput2((input, key) => {
4468
+ if (editingModal) {
4469
+ if (key.escape) {
4470
+ setEditingModal(null);
4471
+ }
4472
+ return;
4473
+ }
4411
4474
  if (input === "l" || input === "L") {
4412
4475
  cycleLimit();
4413
4476
  return;
@@ -4426,10 +4489,13 @@ function PickingView({
4426
4489
  }
4427
4490
  }, { isActive: Boolean(process.stdin.isTTY) });
4428
4491
  const handleItemSelect = (selectedChoice) => {
4492
+ if (editingModal) return;
4493
+ const dateRange = resolveDateFilter(dateFilter);
4429
4494
  onSelect({
4430
4495
  choice: selectedChoice,
4431
4496
  limit,
4432
- since: dateFilter === "all" ? void 0 : dateFilter,
4497
+ since: dateRange.since,
4498
+ until: dateRange.until,
4433
4499
  writeMetadata,
4434
4500
  resume
4435
4501
  });
@@ -4447,12 +4513,11 @@ function PickingView({
4447
4513
  profile.cleanUrl ? ` \xB7 ${profile.cleanUrl}` : ""
4448
4514
  ] }),
4449
4515
  /* @__PURE__ */ jsx8(Gap2, {}),
4450
- /* @__PURE__ */ jsx8(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: "Choose media category to download." }),
4451
- /* @__PURE__ */ jsx8(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: "Use hotkeys [l], [d], [m], [r] to adjust options." }),
4516
+ /* @__PURE__ */ jsx8(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: "Select media type. Hotkeys [l], [d], [m], [r] adjust options." }),
4452
4517
  hasCheckpoint ? /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text7, { color: theme.primary, bold: true, children: "\u26A1 Checkpoint found (cursor saved)" }) }) : null
4453
4518
  ] }),
4454
4519
  /* @__PURE__ */ jsxs6(Panel, { title: "Download", width: 38, children: [
4455
- /* @__PURE__ */ jsx8(
4520
+ !editingModal && /* @__PURE__ */ jsx8(
4456
4521
  SelectInput,
4457
4522
  {
4458
4523
  indicatorComponent: ChoiceIndicator,
@@ -4466,29 +4531,61 @@ function PickingView({
4466
4531
  onHighlight: (item) => onHighlight?.(item.value)
4467
4532
  }
4468
4533
  ),
4469
- /* @__PURE__ */ jsxs6(
4534
+ /* @__PURE__ */ jsx8(
4470
4535
  Box6,
4471
4536
  {
4472
4537
  flexDirection: "column",
4473
4538
  marginTop: 1,
4474
4539
  borderStyle: "round",
4475
- borderColor: theme.gray,
4476
- borderDimColor: true,
4540
+ borderColor: editingModal ? theme.primary : theme.gray,
4541
+ borderDimColor: !editingModal,
4477
4542
  paddingX: 1,
4478
- children: [
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: [
4479
4576
  /* @__PURE__ */ jsxs6(Box6, { justifyContent: "space-between", children: [
4480
4577
  /* @__PURE__ */ jsxs6(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: [
4481
- "[l] Limit: ",
4578
+ "[l] Posts: ",
4482
4579
  /* @__PURE__ */ jsx8(Text7, { color: theme.primary, bold: true, children: limit ? `${limit}` : "ALL" })
4483
4580
  ] }),
4484
4581
  /* @__PURE__ */ jsxs6(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: [
4485
4582
  "[d] Date: ",
4486
- /* @__PURE__ */ jsx8(Text7, { color: theme.primary, bold: true, children: dateFilter === "all" ? "ALL" : dateFilter.toUpperCase() })
4583
+ /* @__PURE__ */ jsx8(Text7, { color: theme.primary, bold: true, children: dateFilter.toUpperCase() })
4487
4584
  ] })
4488
4585
  ] }),
4489
4586
  /* @__PURE__ */ jsxs6(Box6, { justifyContent: "space-between", children: [
4490
4587
  /* @__PURE__ */ jsxs6(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: [
4491
- "[m] Sidecars: ",
4588
+ "[m] Meta: ",
4492
4589
  /* @__PURE__ */ jsx8(Text7, { color: theme.primary, bold: true, children: writeMetadata ? "ON" : "OFF" })
4493
4590
  ] }),
4494
4591
  /* @__PURE__ */ jsxs6(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: [
@@ -4496,7 +4593,7 @@ function PickingView({
4496
4593
  /* @__PURE__ */ jsx8(Text7, { color: theme.primary, bold: true, children: resume ? "ON" : "OFF" })
4497
4594
  ] })
4498
4595
  ] })
4499
- ]
4596
+ ] })
4500
4597
  }
4501
4598
  )
4502
4599
  ] })
@@ -4711,7 +4808,7 @@ function DownloadingView({
4711
4808
  // src/components/views/completion-view.tsx
4712
4809
  import os7 from "os";
4713
4810
  import { Box as Box10, Text as Text12 } from "ink";
4714
- import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
4811
+ import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
4715
4812
  var DONE_LABEL = "\u21B5 download another";
4716
4813
  function CompletionView({
4717
4814
  profile,
@@ -4723,10 +4820,10 @@ function CompletionView({
4723
4820
  const plural = result.downloadedCount === 1 ? "" : "s";
4724
4821
  const skipText = result.skippedCount > 0 ? ` (${result.skippedCount} archived)` : "";
4725
4822
  return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", alignItems: "center", children: [
4726
- /* @__PURE__ */ jsx13(Text12, { children: isZero ? /* @__PURE__ */ jsxs10(Fragment2, { children: [
4823
+ /* @__PURE__ */ jsx13(Text12, { children: isZero ? /* @__PURE__ */ jsxs10(Fragment3, { children: [
4727
4824
  /* @__PURE__ */ jsx13(Text12, { bold: true, color: theme.primary, children: "\u2713 profile scanned " }),
4728
4825
  /* @__PURE__ */ jsx13(Text12, { color: theme.primary, children: "\u2014 0 media items found" })
4729
- ] }) : /* @__PURE__ */ jsxs10(Fragment2, { children: [
4826
+ ] }) : /* @__PURE__ */ jsxs10(Fragment3, { children: [
4730
4827
  /* @__PURE__ */ jsx13(Text12, { bold: true, color: theme.primary, children: "\u2713 profile downloaded! " }),
4731
4828
  /* @__PURE__ */ jsx13(Text12, { color: theme.primary, children: "saved to:" })
4732
4829
  ] }) }),
@@ -5646,7 +5743,9 @@ async function runBatchQueue(options) {
5646
5743
  }
5647
5744
 
5648
5745
  // src/components/views/batch-view.tsx
5649
- import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
5746
+ import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
5747
+ var DATE_PRESETS2 = ["all", "24h", "7d", "30d", "2024", "2023", "2022", "2021", "custom"];
5748
+ var LIMIT_PRESETS2 = [void 0, 10, 24, 50, 100, "custom"];
5650
5749
  function BatchView({
5651
5750
  onBack,
5652
5751
  outputDir,
@@ -5662,6 +5761,9 @@ function BatchView({
5662
5761
  const [rawText, setRawText] = useState8("");
5663
5762
  const [filePath, setFilePath] = useState8("");
5664
5763
  const [errorMessage, setErrorMessage] = useState8(null);
5764
+ const [focusSection, setFocusSection] = useState8("input");
5765
+ const [customLimitText, setCustomLimitText] = useState8("");
5766
+ const [customDateText, setCustomDateText] = useState8("");
5665
5767
  const [choice, setChoice] = useState8(defaultChoice);
5666
5768
  const [limit, setLimit] = useState8(defaultLimit);
5667
5769
  const [dateFilter, setDateFilter] = useState8("all");
@@ -5681,22 +5783,27 @@ function BatchView({
5681
5783
  });
5682
5784
  }, []);
5683
5785
  const cycleLimit = useCallback4(() => {
5684
- setLimit((prev) => {
5685
- if (!prev) return 10;
5686
- if (prev === 10) return 25;
5687
- if (prev === 25) return 50;
5688
- if (prev === 50) return 100;
5689
- return void 0;
5690
- });
5691
- }, []);
5786
+ const curIdx = LIMIT_PRESETS2.findIndex((p) => p === limit);
5787
+ const nextIdx = (curIdx + 1) % LIMIT_PRESETS2.length;
5788
+ const nextVal = LIMIT_PRESETS2[nextIdx];
5789
+ if (nextVal === "custom") {
5790
+ setCustomLimitText(limit ? String(limit) : "24");
5791
+ setFocusSection("custom-limit");
5792
+ } else {
5793
+ setLimit(nextVal);
5794
+ }
5795
+ }, [limit]);
5692
5796
  const cycleDate = useCallback4(() => {
5693
- setDateFilter((prev) => {
5694
- if (prev === "all") return "24h";
5695
- if (prev === "24h") return "7d";
5696
- if (prev === "7d") return "30d";
5697
- return "all";
5698
- });
5699
- }, []);
5797
+ const curIdx = DATE_PRESETS2.indexOf(dateFilter);
5798
+ const nextIdx = (curIdx + 1) % DATE_PRESETS2.length;
5799
+ const nextVal = DATE_PRESETS2[nextIdx];
5800
+ if (nextVal === "custom") {
5801
+ setCustomDateText(dateFilter !== "all" ? dateFilter : "2021");
5802
+ setFocusSection("custom-date");
5803
+ } else {
5804
+ setDateFilter(nextVal);
5805
+ }
5806
+ }, [dateFilter]);
5700
5807
  const toggleMetadata = useCallback4(() => {
5701
5808
  setWriteMetadata((prev) => !prev);
5702
5809
  }, []);
@@ -5727,12 +5834,14 @@ function BatchView({
5727
5834
  abortControllerRef.current = controller;
5728
5835
  setPhase("running");
5729
5836
  try {
5837
+ const dateRange = resolveDateFilter(dateFilter);
5730
5838
  const queueRes = await runBatchQueue({
5731
5839
  filePath: inputField === "file" ? filePath.trim() : void 0,
5732
5840
  lines: inputField === "text" ? lines : void 0,
5733
5841
  choice,
5734
5842
  outputDir,
5735
- since: dateFilter === "all" ? void 0 : dateFilter,
5843
+ since: dateRange.since,
5844
+ until: dateRange.until,
5736
5845
  limit,
5737
5846
  resume,
5738
5847
  writeMetadata,
@@ -5748,7 +5857,7 @@ function BatchView({
5748
5857
  return next;
5749
5858
  });
5750
5859
  },
5751
- onAccountProgress: (item, progress) => {
5860
+ onAccountProgress: (_item, progress) => {
5752
5861
  setCurrentProgressText(progress.statusText);
5753
5862
  },
5754
5863
  onAccountComplete: (item, idx) => {
@@ -5785,6 +5894,14 @@ function BatchView({
5785
5894
  }, [inputField, filePath, rawText, choice, outputDir, dateFilter, limit, resume, writeMetadata]);
5786
5895
  useInput5((input, key) => {
5787
5896
  if (key.escape) {
5897
+ if (focusSection === "custom-limit" || focusSection === "custom-date") {
5898
+ setFocusSection("options");
5899
+ return;
5900
+ }
5901
+ if (focusSection === "options") {
5902
+ setFocusSection("input");
5903
+ return;
5904
+ }
5788
5905
  if (phase === "running") {
5789
5906
  abortControllerRef.current?.abort();
5790
5907
  setPhase("input");
@@ -5806,25 +5923,67 @@ function BatchView({
5806
5923
  return;
5807
5924
  }
5808
5925
  if (phase === "input") {
5809
- if (input === "c" || input === "C") {
5810
- cycleChoice();
5926
+ if (focusSection === "custom-limit" || focusSection === "custom-date") {
5811
5927
  return;
5812
5928
  }
5813
- if (input === "l" || input === "L") {
5814
- cycleLimit();
5815
- return;
5816
- }
5817
- if (input === "d" || input === "D") {
5818
- cycleDate();
5819
- return;
5820
- }
5821
- if (input === "m" || input === "M") {
5822
- toggleMetadata();
5823
- return;
5929
+ if (key.ctrl) {
5930
+ if (input === "l" || input === "L") {
5931
+ setCustomLimitText(limit ? String(limit) : "24");
5932
+ setFocusSection("custom-limit");
5933
+ return;
5934
+ }
5935
+ if (input === "d" || input === "D") {
5936
+ setCustomDateText(dateFilter !== "all" ? dateFilter : "2021");
5937
+ setFocusSection("custom-date");
5938
+ return;
5939
+ }
5940
+ if (input === "c" || input === "C") {
5941
+ cycleChoice();
5942
+ return;
5943
+ }
5944
+ if (input === "m" || input === "M") {
5945
+ toggleMetadata();
5946
+ return;
5947
+ }
5948
+ if (input === "r" || input === "R") {
5949
+ toggleResume();
5950
+ return;
5951
+ }
5824
5952
  }
5825
- if (input === "r" || input === "R") {
5826
- toggleResume();
5827
- return;
5953
+ if (focusSection === "input") {
5954
+ if (key.downArrow) {
5955
+ setFocusSection("options");
5956
+ return;
5957
+ }
5958
+ } else if (focusSection === "options") {
5959
+ if (key.upArrow) {
5960
+ setFocusSection("input");
5961
+ return;
5962
+ }
5963
+ if (key.return) {
5964
+ startQueue();
5965
+ return;
5966
+ }
5967
+ if (input === "c" || input === "C") {
5968
+ cycleChoice();
5969
+ return;
5970
+ }
5971
+ if (input === "l" || input === "L") {
5972
+ cycleLimit();
5973
+ return;
5974
+ }
5975
+ if (input === "d" || input === "D") {
5976
+ cycleDate();
5977
+ return;
5978
+ }
5979
+ if (input === "m" || input === "M") {
5980
+ toggleMetadata();
5981
+ return;
5982
+ }
5983
+ if (input === "r" || input === "R") {
5984
+ toggleResume();
5985
+ return;
5986
+ }
5828
5987
  }
5829
5988
  }
5830
5989
  }, { isActive: Boolean(process.stdin.isTTY) });
@@ -5833,70 +5992,114 @@ function BatchView({
5833
5992
  const totalCount = queueItems.length > 0 ? queueItems.length : 1;
5834
5993
  const percent = Math.min(100, Math.floor(completedCount / totalCount * 100));
5835
5994
  return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", alignItems: "center", children: [
5836
- phase === "input" && /* @__PURE__ */ jsx17(Panel, { title: "Batch Multi-Account Queue Manager", width: Math.max(64, boxWidth), children: /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", paddingY: 1, children: [
5837
- /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: "Queue multiple creator profiles with automatic anti-ban cooldown between downloads." }),
5838
- /* @__PURE__ */ jsx17(Gap2, {}),
5839
- /* @__PURE__ */ jsxs14(Box14, { marginBottom: 1, children: [
5840
- /* @__PURE__ */ jsx17(Text16, { bold: true, color: inputField === "text" ? theme.primary : theme.gray, children: inputField === "text" ? "\u25CF Paste Links / Usernames" : "\u25CB Paste Links / Usernames" }),
5841
- /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: " (Tab to switch) " }),
5842
- /* @__PURE__ */ jsx17(Text16, { bold: true, color: inputField === "file" ? theme.primary : theme.gray, children: inputField === "file" ? "\u25CF Load Batch File (.txt)" : "\u25CB Load Batch File (.txt)" })
5995
+ phase === "input" && /* @__PURE__ */ jsx17(Panel, { title: "Batch Queue", width: Math.max(60, boxWidth), children: /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", paddingY: 1, children: [
5996
+ /* @__PURE__ */ jsxs14(Box14, { marginBottom: 1, justifyContent: "center", children: [
5997
+ /* @__PURE__ */ jsx17(Text16, { bold: true, color: inputField === "text" ? theme.primary : theme.gray, children: inputField === "text" ? "\u25CF Links / Handles" : "\u25CB Links / Handles" }),
5998
+ /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: " \xB7 " }),
5999
+ /* @__PURE__ */ jsx17(Text16, { bold: true, color: inputField === "file" ? theme.primary : theme.gray, children: inputField === "file" ? "\u25CF File (.txt)" : "\u25CB File (.txt)" })
5843
6000
  ] }),
5844
- inputField === "text" ? /* @__PURE__ */ jsx17(FramedInput, { title: "Enter links or @handles separated by commas or spaces", width: Math.max(60, boxWidth - 4), children: /* @__PURE__ */ jsx17(
6001
+ inputField === "text" ? /* @__PURE__ */ jsx17(FramedInput, { title: "Links or @handles", width: Math.max(56, boxWidth - 4), children: /* @__PURE__ */ jsx17(
5845
6002
  TextInput,
5846
6003
  {
5847
6004
  value: rawText,
5848
6005
  onChange: setRawText,
5849
6006
  onSubmit: startQueue,
5850
- placeholder: "e.g. instagram.com/user1, @user2, tiktok.com/@user3",
5851
- width: Math.max(54, boxWidth - 10),
6007
+ placeholder: "@user1, @user2, tiktok.com/@user3\u2026",
6008
+ width: Math.max(50, boxWidth - 10),
6009
+ focus: focusSection === "input",
5852
6010
  onTab: () => setInputField("file"),
5853
- onArrowDown: () => setInputField("file")
6011
+ onArrowDown: () => setFocusSection("options")
5854
6012
  }
5855
- ) }) : /* @__PURE__ */ jsx17(FramedInput, { title: "Enter path to .txt batch file", width: Math.max(60, boxWidth - 4), children: /* @__PURE__ */ jsx17(
6013
+ ) }) : /* @__PURE__ */ jsx17(FramedInput, { title: "Batch file (.txt)", width: Math.max(56, boxWidth - 4), children: /* @__PURE__ */ jsx17(
5856
6014
  TextInput,
5857
6015
  {
5858
6016
  value: filePath,
5859
6017
  onChange: setFilePath,
5860
6018
  onSubmit: startQueue,
5861
- placeholder: "e.g. targets.txt or C:\\path\\to\\creators.txt",
5862
- width: Math.max(54, boxWidth - 10),
6019
+ placeholder: "e.g. creators.txt or C:\\path\\creators.txt",
6020
+ width: Math.max(50, boxWidth - 10),
6021
+ focus: focusSection === "input",
5863
6022
  onTab: () => setInputField("text"),
5864
- onArrowUp: () => setInputField("text")
6023
+ onArrowDown: () => setFocusSection("options")
5865
6024
  }
5866
6025
  ) }),
5867
6026
  errorMessage ? /* @__PURE__ */ jsx17(Box14, { marginTop: 1, children: /* @__PURE__ */ jsxs14(Text16, { color: "red", children: [
5868
6027
  "\u2717 ",
5869
6028
  errorMessage
5870
6029
  ] }) }) : null,
5871
- /* @__PURE__ */ jsx17(Gap2, {}),
5872
- /* @__PURE__ */ jsx17(Text16, { bold: true, color: theme.primary, children: "Batch Queue Options:" }),
5873
- /* @__PURE__ */ jsxs14(Box14, { justifyContent: "space-between", marginTop: 1, children: [
5874
- /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5875
- "[c] Target: ",
5876
- /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: choice.toUpperCase() })
5877
- ] }),
5878
- /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5879
- "[l] Limit: ",
5880
- /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: limit ? `${limit} posts` : "ALL" })
5881
- ] }),
5882
- /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5883
- "[d] Date: ",
5884
- /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: dateFilter === "all" ? "ALL TIME" : dateFilter.toUpperCase() })
5885
- ] })
5886
- ] }),
5887
- /* @__PURE__ */ jsxs14(Box14, { justifyContent: "space-between", marginTop: 1, children: [
5888
- /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5889
- "[m] Sidecars: ",
5890
- /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: writeMetadata ? "ON" : "OFF" })
5891
- ] }),
5892
- /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5893
- "[r] Resume Checkpoint: ",
5894
- /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: resume ? "ON" : "OFF" })
5895
- ] }),
5896
- /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: "[\u21B5] Start Queue" })
5897
- ] })
6030
+ /* @__PURE__ */ jsx17(
6031
+ Box14,
6032
+ {
6033
+ flexDirection: "column",
6034
+ marginTop: 1,
6035
+ borderStyle: "round",
6036
+ borderColor: focusSection === "options" || focusSection === "custom-limit" || focusSection === "custom-date" ? theme.primary : theme.gray,
6037
+ borderDimColor: focusSection === "input",
6038
+ paddingX: 1,
6039
+ children: focusSection === "custom-limit" ? /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
6040
+ /* @__PURE__ */ jsx17(Text16, { bold: true, color: theme.primary, children: "Set post limit (e.g. 24, all):" }),
6041
+ /* @__PURE__ */ jsx17(Box14, { marginTop: 1, children: /* @__PURE__ */ jsx17(
6042
+ TextInput,
6043
+ {
6044
+ value: customLimitText,
6045
+ onChange: setCustomLimitText,
6046
+ onSubmit: (val) => {
6047
+ const parsed = parseLimit(val);
6048
+ setLimit(parsed);
6049
+ setFocusSection("options");
6050
+ },
6051
+ width: 24
6052
+ }
6053
+ ) }),
6054
+ /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: "\u21B5 confirm \xB7 esc cancel" })
6055
+ ] }) : focusSection === "custom-date" ? /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
6056
+ /* @__PURE__ */ jsx17(Text16, { bold: true, color: theme.primary, children: "Set date or year (e.g. 2021, 2022, 7d):" }),
6057
+ /* @__PURE__ */ jsx17(Box14, { marginTop: 1, children: /* @__PURE__ */ jsx17(
6058
+ TextInput,
6059
+ {
6060
+ value: customDateText,
6061
+ onChange: setCustomDateText,
6062
+ onSubmit: (val) => {
6063
+ const trimmed = val.trim();
6064
+ setDateFilter(trimmed ? trimmed : "all");
6065
+ setFocusSection("options");
6066
+ },
6067
+ width: 24
6068
+ }
6069
+ ) }),
6070
+ /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: "\u21B5 confirm \xB7 esc cancel" })
6071
+ ] }) : /* @__PURE__ */ jsxs14(Fragment4, { children: [
6072
+ /* @__PURE__ */ jsxs14(Box14, { justifyContent: "space-between", children: [
6073
+ /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
6074
+ "[c] Media: ",
6075
+ /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: choice.toUpperCase() })
6076
+ ] }),
6077
+ /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
6078
+ "[l] Posts: ",
6079
+ /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: limit ? `${limit}` : "ALL" })
6080
+ ] }),
6081
+ /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
6082
+ "[d] Date: ",
6083
+ /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: dateFilter.toUpperCase() })
6084
+ ] })
6085
+ ] }),
6086
+ /* @__PURE__ */ jsxs14(Box14, { justifyContent: "space-between", marginTop: 1, children: [
6087
+ /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
6088
+ "[m] Meta: ",
6089
+ /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: writeMetadata ? "ON" : "OFF" })
6090
+ ] }),
6091
+ /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
6092
+ "[r] Resume: ",
6093
+ /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: resume ? "ON" : "OFF" })
6094
+ ] }),
6095
+ /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: "[\u21B5] Start Queue" })
6096
+ ] })
6097
+ ] })
6098
+ }
6099
+ ),
6100
+ /* @__PURE__ */ jsx17(Box14, { marginTop: 1, justifyContent: "center", children: /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: focusSection === "input" ? "Press \u2193 to adjust options \xB7 \u21B5 to start queue" : "Hotkeys [c] [l] [d] [m] [r] \xB7 \u2191 to edit text \xB7 \u21B5 to start" }) })
5898
6101
  ] }) }),
5899
- phase === "running" && /* @__PURE__ */ jsx17(Panel, { title: "Batch Queue Running (Anti-Ban Hardened)", width: Math.max(64, boxWidth), children: /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", paddingY: 1, children: [
6102
+ phase === "running" && /* @__PURE__ */ jsx17(Panel, { title: "Batch Queue Running", width: Math.max(60, boxWidth), children: /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", paddingY: 1, children: [
5900
6103
  /* @__PURE__ */ jsxs14(Box14, { justifyContent: "space-between", marginBottom: 1, children: [
5901
6104
  /* @__PURE__ */ jsxs14(Text16, { color: theme.primary, bold: true, children: [
5902
6105
  "Account ",
@@ -5907,9 +6110,9 @@ function BatchView({
5907
6110
  percent,
5908
6111
  "%)"
5909
6112
  ] }),
5910
- /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: isCooldown ? "\u23F3 Anti-ban cooldown active" : "\u25B6 Downloading" })
6113
+ /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: isCooldown ? "\u23F3 Cooldown active" : "\u25B6 Downloading" })
5911
6114
  ] }),
5912
- /* @__PURE__ */ jsx17(ProgressBar, { percent, width: 58 }),
6115
+ /* @__PURE__ */ jsx17(ProgressBar, { percent, width: 54 }),
5913
6116
  /* @__PURE__ */ jsx17(Gap2, {}),
5914
6117
  currentItem ? /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", borderStyle: "round", borderColor: theme.primary, paddingX: 1, paddingY: 1, children: [
5915
6118
  /* @__PURE__ */ jsxs14(Box14, { children: [
@@ -5931,7 +6134,7 @@ function BatchView({
5931
6134
  /* @__PURE__ */ jsx17(Gap2, {}),
5932
6135
  /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: "[esc] Stop queue and return" })
5933
6136
  ] }) }),
5934
- phase === "done" && result && /* @__PURE__ */ jsx17(Panel, { title: "Batch Queue Completed", width: Math.max(64, boxWidth), children: /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", paddingY: 1, children: [
6137
+ phase === "done" && result && /* @__PURE__ */ jsx17(Panel, { title: "Batch Queue Completed", width: Math.max(60, boxWidth), children: /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", paddingY: 1, children: [
5935
6138
  /* @__PURE__ */ jsxs14(Text16, { bold: true, color: theme.primary, children: [
5936
6139
  "\u2713 Successfully processed ",
5937
6140
  result.total,
@@ -5951,7 +6154,7 @@ function BatchView({
5951
6154
  result.totalDownloaded
5952
6155
  ] }),
5953
6156
  /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5954
- "\u2022 Total items skipped (already archived): ",
6157
+ "\u2022 Total items skipped: ",
5955
6158
  result.totalSkipped
5956
6159
  ] }),
5957
6160
  /* @__PURE__ */ jsx17(Gap2, {}),
@@ -6015,7 +6218,7 @@ function frameRowSpan(row) {
6015
6218
  }
6016
6219
 
6017
6220
  // src/app.tsx
6018
- import { Fragment as Fragment3, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
6221
+ import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
6019
6222
  var TAGLINE = "grab any profile. paste. download. done.";
6020
6223
  var DOWNLOAD_BUTTON = "download";
6021
6224
  var Gap = ({ lines = 1 }) => /* @__PURE__ */ jsx18(Box15, { flexDirection: "column", flexShrink: 0, children: Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsx18(Text17, { children: " " }, i)) });
@@ -6187,6 +6390,7 @@ function InnerApp({
6187
6390
  const targetChoice = typeof choiceOrOptions === "string" ? choiceOrOptions : choiceOrOptions.choice;
6188
6391
  const targetLimit = typeof choiceOrOptions === "string" ? effectiveLimit : choiceOrOptions.limit ?? effectiveLimit;
6189
6392
  const targetSince = typeof choiceOrOptions === "string" ? since : choiceOrOptions.since ?? since;
6393
+ const targetUntil = typeof choiceOrOptions === "string" ? until : choiceOrOptions.until ?? until;
6190
6394
  const targetResume = typeof choiceOrOptions === "string" ? resume : choiceOrOptions.resume ?? resume;
6191
6395
  const targetWriteMetadata = typeof choiceOrOptions === "string" ? effectiveWriteMetadata : choiceOrOptions.writeMetadata ?? effectiveWriteMetadata;
6192
6396
  setPhase({ name: "downloading", profile, choice: targetChoice });
@@ -6197,7 +6401,7 @@ function InnerApp({
6197
6401
  choice: targetChoice,
6198
6402
  outputDir: targetDir,
6199
6403
  since: targetSince,
6200
- until,
6404
+ until: targetUntil,
6201
6405
  limit: targetLimit,
6202
6406
  resume: targetResume,
6203
6407
  writeMetadata: targetWriteMetadata,
@@ -6483,6 +6687,7 @@ function InnerApp({
6483
6687
  outputDir,
6484
6688
  initialLimit: effectiveLimit,
6485
6689
  initialSince: since,
6690
+ initialUntil: until,
6486
6691
  initialWriteMetadata: effectiveWriteMetadata,
6487
6692
  initialResume: resume,
6488
6693
  onSelect: (choiceOrOptions) => handleModeSelect(choiceOrOptions, phase.profile),
@@ -6559,7 +6764,7 @@ function InnerApp({
6559
6764
  defaultWriteMetadata: effectiveWriteMetadata
6560
6765
  }
6561
6766
  ),
6562
- hints.length > 0 ? /* @__PURE__ */ jsxs15(Fragment3, { children: [
6767
+ hints.length > 0 ? /* @__PURE__ */ jsxs15(Fragment5, { children: [
6563
6768
  /* @__PURE__ */ jsx18(Gap, { lines: 2 }),
6564
6769
  /* @__PURE__ */ jsx18(
6565
6770
  Shortcuts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-nami",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "Open Nami: terminal-first bulk social media profile downloader for Instagram, TikTok, Facebook & X.",
5
5
  "type": "module",
6
6
  "bin": {