open-nami 1.2.3 → 1.2.5

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 +685 -148
  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.5",
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,7 +77,7 @@ 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 useEffect7, useMemo as useMemo3 } from "react";
80
+ import { useState as useState9, useCallback as useCallback5, useRef as useRef5, useEffect as useEffect8, useMemo as useMemo3 } from "react";
81
81
  import { Box as Box15, Text as Text17, useApp, useInput as useInput6, useStdout as useStdout3 } from "ink";
82
82
  import Spinner4 from "ink-spinner";
83
83
 
@@ -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
  ] }) }),
@@ -5532,7 +5629,7 @@ function WatcherView({
5532
5629
  }
5533
5630
 
5534
5631
  // src/components/views/batch-view.tsx
5535
- import { useState as useState8, useRef as useRef4, useCallback as useCallback4 } from "react";
5632
+ import { useState as useState8, useRef as useRef4, useCallback as useCallback4, useEffect as useEffect7 } from "react";
5536
5633
  import { Box as Box14, Text as Text16, useInput as useInput5 } from "ink";
5537
5634
  import Spinner3 from "ink-spinner";
5538
5635
  import fs13 from "fs";
@@ -5570,13 +5667,19 @@ async function runBatchQueue(options) {
5570
5667
  const choice = options.choice || "all";
5571
5668
  const minDelay = options.minDelayMs ?? 2e3;
5572
5669
  const maxDelay = options.maxDelayMs ?? 5e3;
5573
- const items = rawEntries.map((raw) => ({
5574
- raw,
5575
- profile: parseProfileInput(raw),
5576
- status: "pending",
5577
- downloaded: 0,
5578
- skipped: 0
5579
- }));
5670
+ const items = rawEntries.map((raw) => {
5671
+ let profile = parseProfileInput(raw);
5672
+ if (options.defaultPlatform && profile.platform === "unknown" && profile.username) {
5673
+ profile = buildCanonicalProfile(options.defaultPlatform, profile.username, raw);
5674
+ }
5675
+ return {
5676
+ raw,
5677
+ profile,
5678
+ status: "pending",
5679
+ downloaded: 0,
5680
+ skipped: 0
5681
+ };
5682
+ });
5580
5683
  let totalDownloaded = 0;
5581
5684
  let totalSkipped = 0;
5582
5685
  let succeeded = 0;
@@ -5596,10 +5699,15 @@ async function runBatchQueue(options) {
5596
5699
  }
5597
5700
  options.onAccountStart?.(item, i, items.length);
5598
5701
  try {
5702
+ const targetDir = resolveDefaultDownloadDir(
5703
+ item.profile.platform,
5704
+ item.profile.username,
5705
+ options.outputDir
5706
+ );
5599
5707
  const res = await dispatchDownload({
5600
5708
  profile: item.profile,
5601
5709
  choice,
5602
- outputDir: options.outputDir,
5710
+ outputDir: targetDir,
5603
5711
  since: options.since,
5604
5712
  until: options.until,
5605
5713
  limit: options.limit,
@@ -5646,7 +5754,12 @@ async function runBatchQueue(options) {
5646
5754
  }
5647
5755
 
5648
5756
  // src/components/views/batch-view.tsx
5649
- import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
5757
+ import { Fragment as Fragment4, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
5758
+ 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"];
5762
+ var OPTION_CONTROLS = ["platform", "choice", "limit", "date", "meta", "resume", "start"];
5650
5763
  function BatchView({
5651
5764
  onBack,
5652
5765
  outputDir,
@@ -5655,13 +5768,18 @@ function BatchView({
5655
5768
  gapComponent: Gap2,
5656
5769
  defaultChoice = "all",
5657
5770
  defaultLimit,
5658
- defaultWriteMetadata = false
5771
+ defaultWriteMetadata = false,
5772
+ onRegisterClickTargets
5659
5773
  }) {
5660
5774
  const [phase, setPhase] = useState8("input");
5661
5775
  const [inputField, setInputField] = useState8("text");
5662
5776
  const [rawText, setRawText] = useState8("");
5663
5777
  const [filePath, setFilePath] = useState8("");
5664
5778
  const [errorMessage, setErrorMessage] = useState8(null);
5779
+ const [focusedControl, setFocusedControl] = useState8("input");
5780
+ const [customLimitText, setCustomLimitText] = useState8("");
5781
+ const [customDateText, setCustomDateText] = useState8("");
5782
+ const [defaultPlatform, setDefaultPlatform] = useState8("instagram");
5665
5783
  const [choice, setChoice] = useState8(defaultChoice);
5666
5784
  const [limit, setLimit] = useState8(defaultLimit);
5667
5785
  const [dateFilter, setDateFilter] = useState8("all");
@@ -5673,30 +5791,40 @@ function BatchView({
5673
5791
  const [isCooldown, setIsCooldown] = useState8(false);
5674
5792
  const [result, setResult] = useState8(null);
5675
5793
  const abortControllerRef = useRef4(null);
5794
+ const cyclePlatform = useCallback4(() => {
5795
+ setDefaultPlatform((prev) => {
5796
+ const idx = PLATFORMS.indexOf(prev);
5797
+ return PLATFORMS[(idx + 1) % PLATFORMS.length];
5798
+ });
5799
+ }, []);
5676
5800
  const cycleChoice = useCallback4(() => {
5677
5801
  setChoice((prev) => {
5678
- if (prev === "all") return "photos";
5679
- if (prev === "photos") return "videos";
5680
- return "all";
5802
+ const idx = CHOICES.indexOf(prev);
5803
+ return CHOICES[(idx + 1) % CHOICES.length];
5681
5804
  });
5682
5805
  }, []);
5683
5806
  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
- }, []);
5807
+ const curIdx = LIMIT_PRESETS2.findIndex((p) => p === limit);
5808
+ const nextIdx = (curIdx + 1) % LIMIT_PRESETS2.length;
5809
+ const nextVal = LIMIT_PRESETS2[nextIdx];
5810
+ if (nextVal === "custom") {
5811
+ setCustomLimitText(limit ? String(limit) : "24");
5812
+ setFocusedControl("custom-limit");
5813
+ } else {
5814
+ setLimit(nextVal);
5815
+ }
5816
+ }, [limit]);
5692
5817
  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
- }, []);
5818
+ const curIdx = DATE_PRESETS2.indexOf(dateFilter);
5819
+ const nextIdx = (curIdx + 1) % DATE_PRESETS2.length;
5820
+ const nextVal = DATE_PRESETS2[nextIdx];
5821
+ if (nextVal === "custom") {
5822
+ setCustomDateText(dateFilter !== "all" ? dateFilter : "2024");
5823
+ setFocusedControl("custom-date");
5824
+ } else {
5825
+ setDateFilter(nextVal);
5826
+ }
5827
+ }, [dateFilter]);
5700
5828
  const toggleMetadata = useCallback4(() => {
5701
5829
  setWriteMetadata((prev) => !prev);
5702
5830
  }, []);
@@ -5717,7 +5845,11 @@ function BatchView({
5717
5845
  return;
5718
5846
  }
5719
5847
  } else {
5720
- lines = rawText.split(/[\n,;]+/).map((s) => s.trim()).filter(Boolean);
5848
+ lines = Array.from(
5849
+ new Set(
5850
+ rawText.split(/[\r\n,;\s]+/).map((s) => s.trim()).filter(Boolean)
5851
+ )
5852
+ );
5721
5853
  if (lines.length === 0) {
5722
5854
  setErrorMessage("Please enter at least one profile URL or username");
5723
5855
  return;
@@ -5727,12 +5859,15 @@ function BatchView({
5727
5859
  abortControllerRef.current = controller;
5728
5860
  setPhase("running");
5729
5861
  try {
5862
+ const dateRange = resolveDateFilter(dateFilter);
5730
5863
  const queueRes = await runBatchQueue({
5731
5864
  filePath: inputField === "file" ? filePath.trim() : void 0,
5732
5865
  lines: inputField === "text" ? lines : void 0,
5733
5866
  choice,
5867
+ defaultPlatform,
5734
5868
  outputDir,
5735
- since: dateFilter === "all" ? void 0 : dateFilter,
5869
+ since: dateRange.since,
5870
+ until: dateRange.until,
5736
5871
  limit,
5737
5872
  resume,
5738
5873
  writeMetadata,
@@ -5740,7 +5875,7 @@ function BatchView({
5740
5875
  onAccountStart: (item, idx) => {
5741
5876
  setIsCooldown(false);
5742
5877
  setCurrentIndex(idx);
5743
- setCurrentProgressText(`Probing @${item.profile.username || item.raw}\u2026`);
5878
+ setCurrentProgressText(`Probing @${item.profile.username || item.raw} on ${item.profile.platform}\u2026`);
5744
5879
  setQueueItems((prev) => {
5745
5880
  const next = [...prev];
5746
5881
  if (next[idx]) next[idx] = { ...item };
@@ -5748,7 +5883,7 @@ function BatchView({
5748
5883
  return next;
5749
5884
  });
5750
5885
  },
5751
- onAccountProgress: (item, progress) => {
5886
+ onAccountProgress: (_item, progress) => {
5752
5887
  setCurrentProgressText(progress.statusText);
5753
5888
  },
5754
5889
  onAccountComplete: (item, idx) => {
@@ -5782,9 +5917,20 @@ function BatchView({
5782
5917
  setErrorMessage(msg);
5783
5918
  setPhase("input");
5784
5919
  }
5785
- }, [inputField, filePath, rawText, choice, outputDir, dateFilter, limit, resume, writeMetadata]);
5920
+ }, [inputField, filePath, rawText, choice, defaultPlatform, outputDir, dateFilter, limit, resume, writeMetadata]);
5786
5921
  useInput5((input, key) => {
5922
+ if (key.ctrl && (input === "c" || input === "C")) {
5923
+ return;
5924
+ }
5787
5925
  if (key.escape) {
5926
+ if (focusedControl === "custom-limit" || focusedControl === "custom-date") {
5927
+ setFocusedControl("start");
5928
+ return;
5929
+ }
5930
+ if (focusedControl !== "input" && phase === "input") {
5931
+ setFocusedControl("input");
5932
+ return;
5933
+ }
5788
5934
  if (phase === "running") {
5789
5935
  abortControllerRef.current?.abort();
5790
5936
  setPhase("input");
@@ -5805,98 +5951,466 @@ function BatchView({
5805
5951
  }
5806
5952
  return;
5807
5953
  }
5954
+ if (phase === "running") {
5955
+ return;
5956
+ }
5808
5957
  if (phase === "input") {
5809
- if (input === "c" || input === "C") {
5810
- cycleChoice();
5811
- return;
5812
- }
5813
- if (input === "l" || input === "L") {
5814
- cycleLimit();
5958
+ if (focusedControl === "custom-limit" || focusedControl === "custom-date") {
5815
5959
  return;
5816
5960
  }
5817
- if (input === "d" || input === "D") {
5818
- cycleDate();
5961
+ if (focusedControl === "input") {
5962
+ if (key.downArrow) {
5963
+ setFocusedControl("platform");
5964
+ return;
5965
+ }
5966
+ if (key.upArrow) {
5967
+ setFocusedControl("tabs");
5968
+ return;
5969
+ }
5819
5970
  return;
5820
5971
  }
5821
- if (input === "m" || input === "M") {
5822
- toggleMetadata();
5972
+ if (focusedControl === "tabs") {
5973
+ if (key.leftArrow || key.rightArrow || input === " " || key.return) {
5974
+ setInputField((prev) => prev === "text" ? "file" : "text");
5975
+ return;
5976
+ }
5977
+ if (key.downArrow || key.tab) {
5978
+ setFocusedControl("input");
5979
+ return;
5980
+ }
5823
5981
  return;
5824
5982
  }
5825
- if (input === "r" || input === "R") {
5826
- toggleResume();
5827
- return;
5983
+ if (OPTION_CONTROLS.includes(focusedControl)) {
5984
+ if (key.upArrow) {
5985
+ setFocusedControl("input");
5986
+ return;
5987
+ }
5988
+ if (key.rightArrow || key.tab && !key.shift) {
5989
+ const idx = OPTION_CONTROLS.indexOf(focusedControl);
5990
+ setFocusedControl(OPTION_CONTROLS[(idx + 1) % OPTION_CONTROLS.length]);
5991
+ return;
5992
+ }
5993
+ if (key.leftArrow || key.tab && key.shift) {
5994
+ const idx = OPTION_CONTROLS.indexOf(focusedControl);
5995
+ setFocusedControl(OPTION_CONTROLS[(idx - 1 + OPTION_CONTROLS.length) % OPTION_CONTROLS.length]);
5996
+ return;
5997
+ }
5998
+ if (input === " " || key.return) {
5999
+ switch (focusedControl) {
6000
+ case "platform":
6001
+ cyclePlatform();
6002
+ break;
6003
+ case "choice":
6004
+ cycleChoice();
6005
+ break;
6006
+ case "limit":
6007
+ cycleLimit();
6008
+ break;
6009
+ case "date":
6010
+ cycleDate();
6011
+ break;
6012
+ case "meta":
6013
+ toggleMetadata();
6014
+ break;
6015
+ case "resume":
6016
+ toggleResume();
6017
+ break;
6018
+ case "start":
6019
+ startQueue();
6020
+ break;
6021
+ }
6022
+ return;
6023
+ }
6024
+ if (input === "p" || input === "P") {
6025
+ cyclePlatform();
6026
+ return;
6027
+ }
6028
+ if (input === "c" || input === "C") {
6029
+ cycleChoice();
6030
+ return;
6031
+ }
6032
+ if (input === "l" || input === "L") {
6033
+ cycleLimit();
6034
+ return;
6035
+ }
6036
+ if (input === "d" || input === "D") {
6037
+ cycleDate();
6038
+ return;
6039
+ }
6040
+ if (input === "m" || input === "M") {
6041
+ toggleMetadata();
6042
+ return;
6043
+ }
6044
+ if (input === "r" || input === "R") {
6045
+ toggleResume();
6046
+ return;
6047
+ }
5828
6048
  }
5829
6049
  }
5830
6050
  }, { isActive: Boolean(process.stdin.isTTY) });
6051
+ useEffect7(() => {
6052
+ if (!onRegisterClickTargets) return;
6053
+ const targets = [];
6054
+ if (phase === "input") {
6055
+ targets.push({
6056
+ match: "Links / Handles",
6057
+ padX: 2,
6058
+ action: () => {
6059
+ setInputField("text");
6060
+ setFocusedControl("input");
6061
+ }
6062
+ });
6063
+ targets.push({
6064
+ match: "File (.txt)",
6065
+ padX: 2,
6066
+ action: () => {
6067
+ setInputField("file");
6068
+ setFocusedControl("input");
6069
+ }
6070
+ });
6071
+ targets.push({
6072
+ match: "Links or @handles",
6073
+ padX: 2,
6074
+ action: () => setFocusedControl("input")
6075
+ });
6076
+ targets.push({
6077
+ match: "Batch file (.txt)",
6078
+ padX: 2,
6079
+ action: () => setFocusedControl("input")
6080
+ });
6081
+ targets.push({
6082
+ match: "Platform:",
6083
+ padX: 2,
6084
+ action: () => {
6085
+ setFocusedControl("platform");
6086
+ cyclePlatform();
6087
+ }
6088
+ });
6089
+ targets.push({
6090
+ match: "Media:",
6091
+ padX: 2,
6092
+ action: () => {
6093
+ setFocusedControl("choice");
6094
+ cycleChoice();
6095
+ }
6096
+ });
6097
+ targets.push({
6098
+ match: "Posts:",
6099
+ padX: 2,
6100
+ action: () => {
6101
+ setFocusedControl("limit");
6102
+ cycleLimit();
6103
+ }
6104
+ });
6105
+ targets.push({
6106
+ match: "Date:",
6107
+ padX: 2,
6108
+ action: () => {
6109
+ setFocusedControl("date");
6110
+ cycleDate();
6111
+ }
6112
+ });
6113
+ targets.push({
6114
+ match: "Meta:",
6115
+ padX: 2,
6116
+ action: () => {
6117
+ setFocusedControl("meta");
6118
+ toggleMetadata();
6119
+ }
6120
+ });
6121
+ targets.push({
6122
+ match: "Resume:",
6123
+ padX: 2,
6124
+ action: () => {
6125
+ setFocusedControl("resume");
6126
+ toggleResume();
6127
+ }
6128
+ });
6129
+ targets.push({
6130
+ match: "Start Queue",
6131
+ padX: 3,
6132
+ action: () => {
6133
+ setFocusedControl("start");
6134
+ startQueue();
6135
+ }
6136
+ });
6137
+ targets.push({
6138
+ match: "tab switch field",
6139
+ action: () => setInputField((prev) => prev === "text" ? "file" : "text")
6140
+ });
6141
+ targets.push({
6142
+ match: "c target",
6143
+ action: cycleChoice
6144
+ });
6145
+ targets.push({
6146
+ match: "m sidecar",
6147
+ action: toggleMetadata
6148
+ });
6149
+ targets.push({
6150
+ match: "l limit",
6151
+ action: cycleLimit
6152
+ });
6153
+ targets.push({
6154
+ match: "d date",
6155
+ action: cycleDate
6156
+ });
6157
+ targets.push({
6158
+ match: "r resume",
6159
+ action: toggleResume
6160
+ });
6161
+ targets.push({
6162
+ match: "\u21B5 start",
6163
+ action: startQueue
6164
+ });
6165
+ }
6166
+ if (phase === "running") {
6167
+ targets.push({
6168
+ match: "Stop queue and return",
6169
+ padX: 2,
6170
+ action: () => {
6171
+ abortControllerRef.current?.abort();
6172
+ setPhase("input");
6173
+ }
6174
+ });
6175
+ targets.push({
6176
+ match: "Stop queue",
6177
+ padX: 2,
6178
+ action: () => {
6179
+ abortControllerRef.current?.abort();
6180
+ setPhase("input");
6181
+ }
6182
+ });
6183
+ }
6184
+ if (phase === "done") {
6185
+ targets.push({
6186
+ match: "Open Downloads Folder",
6187
+ padX: 2,
6188
+ action: () => {
6189
+ const target = outputDir || resolveDefaultDownloadDir("instagram", "batch");
6190
+ revealInFileManager(target);
6191
+ }
6192
+ });
6193
+ targets.push({
6194
+ match: "Return to Start",
6195
+ padX: 2,
6196
+ action: () => setPhase("input")
6197
+ });
6198
+ }
6199
+ onRegisterClickTargets(targets);
6200
+ return () => {
6201
+ onRegisterClickTargets([]);
6202
+ };
6203
+ }, [
6204
+ phase,
6205
+ inputField,
6206
+ focusedControl,
6207
+ onRegisterClickTargets,
6208
+ cyclePlatform,
6209
+ cycleChoice,
6210
+ cycleLimit,
6211
+ cycleDate,
6212
+ toggleMetadata,
6213
+ toggleResume,
6214
+ startQueue,
6215
+ outputDir
6216
+ ]);
5831
6217
  const currentItem = queueItems[currentIndex];
5832
6218
  const completedCount = queueItems.filter((i) => i.status === "success" || i.status === "failed").length;
5833
6219
  const totalCount = queueItems.length > 0 ? queueItems.length : 1;
5834
6220
  const percent = Math.min(100, Math.floor(completedCount / totalCount * 100));
5835
6221
  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)" })
6222
+ phase === "input" && /* @__PURE__ */ jsx17(Panel, { title: "Batch Queue", width: Math.max(60, boxWidth), children: /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", paddingY: 1, children: [
6223
+ /* @__PURE__ */ jsxs14(Box14, { marginBottom: 1, justifyContent: "center", children: [
6224
+ /* @__PURE__ */ jsx17(
6225
+ Text16,
6226
+ {
6227
+ bold: true,
6228
+ color: inputField === "text" ? theme.primary : theme.gray,
6229
+ inverse: focusedControl === "tabs" && inputField === "text",
6230
+ children: inputField === "text" ? "\u25CF Links / Handles" : "\u25CB Links / Handles"
6231
+ }
6232
+ ),
6233
+ /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: " \xB7 " }),
6234
+ /* @__PURE__ */ jsx17(
6235
+ Text16,
6236
+ {
6237
+ bold: true,
6238
+ color: inputField === "file" ? theme.primary : theme.gray,
6239
+ inverse: focusedControl === "tabs" && inputField === "file",
6240
+ children: inputField === "file" ? "\u25CF File (.txt)" : "\u25CB File (.txt)"
6241
+ }
6242
+ )
5843
6243
  ] }),
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(
6244
+ inputField === "text" ? /* @__PURE__ */ jsx17(FramedInput, { title: "Links or @handles", width: Math.max(56, boxWidth - 4), children: /* @__PURE__ */ jsx17(
5845
6245
  TextInput,
5846
6246
  {
5847
6247
  value: rawText,
5848
6248
  onChange: setRawText,
5849
6249
  onSubmit: startQueue,
5850
- placeholder: "e.g. instagram.com/user1, @user2, tiktok.com/@user3",
5851
- width: Math.max(54, boxWidth - 10),
5852
- onTab: () => setInputField("file"),
5853
- onArrowDown: () => setInputField("file")
6250
+ placeholder: "@user1, @user2, tiktok.com/@user3\u2026",
6251
+ width: Math.max(50, boxWidth - 10),
6252
+ focus: focusedControl === "input",
6253
+ onArrowDown: () => setFocusedControl("start"),
6254
+ onArrowUp: () => setFocusedControl("tabs"),
6255
+ onTab: () => setFocusedControl("start")
5854
6256
  }
5855
- ) }) : /* @__PURE__ */ jsx17(FramedInput, { title: "Enter path to .txt batch file", width: Math.max(60, boxWidth - 4), children: /* @__PURE__ */ jsx17(
6257
+ ) }) : /* @__PURE__ */ jsx17(FramedInput, { title: "Batch file (.txt)", width: Math.max(56, boxWidth - 4), children: /* @__PURE__ */ jsx17(
5856
6258
  TextInput,
5857
6259
  {
5858
6260
  value: filePath,
5859
6261
  onChange: setFilePath,
5860
6262
  onSubmit: startQueue,
5861
- placeholder: "e.g. targets.txt or C:\\path\\to\\creators.txt",
5862
- width: Math.max(54, boxWidth - 10),
5863
- onTab: () => setInputField("text"),
5864
- onArrowUp: () => setInputField("text")
6263
+ placeholder: "e.g. creators.txt or C:\\path\\creators.txt",
6264
+ width: Math.max(50, boxWidth - 10),
6265
+ focus: focusedControl === "input",
6266
+ onArrowDown: () => setFocusedControl("start"),
6267
+ onArrowUp: () => setFocusedControl("tabs"),
6268
+ onTab: () => setFocusedControl("start")
5865
6269
  }
5866
6270
  ) }),
5867
6271
  errorMessage ? /* @__PURE__ */ jsx17(Box14, { marginTop: 1, children: /* @__PURE__ */ jsxs14(Text16, { color: "red", children: [
5868
6272
  "\u2717 ",
5869
6273
  errorMessage
5870
6274
  ] }) }) : 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
- ] })
6275
+ /* @__PURE__ */ jsx17(
6276
+ Box14,
6277
+ {
6278
+ flexDirection: "column",
6279
+ marginTop: 1,
6280
+ borderStyle: "round",
6281
+ borderColor: focusedControl !== "input" && focusedControl !== "tabs" ? theme.primary : theme.gray,
6282
+ borderDimColor: focusedControl === "input" || focusedControl === "tabs",
6283
+ paddingX: 1,
6284
+ children: focusedControl === "custom-limit" ? /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
6285
+ /* @__PURE__ */ jsx17(Text16, { bold: true, color: theme.primary, children: "Set post limit (e.g. 24, all):" }),
6286
+ /* @__PURE__ */ jsx17(Box14, { marginTop: 1, children: /* @__PURE__ */ jsx17(
6287
+ TextInput,
6288
+ {
6289
+ value: customLimitText,
6290
+ onChange: setCustomLimitText,
6291
+ onSubmit: (val) => {
6292
+ const parsed = parseLimit(val);
6293
+ setLimit(parsed);
6294
+ setFocusedControl("start");
6295
+ },
6296
+ width: 24
6297
+ }
6298
+ ) }),
6299
+ /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: "\u21B5 confirm \xB7 esc cancel" })
6300
+ ] }) : focusedControl === "custom-date" ? /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
6301
+ /* @__PURE__ */ jsx17(Text16, { bold: true, color: theme.primary, children: "Set date or year (e.g. 2024, 7d):" }),
6302
+ /* @__PURE__ */ jsx17(Box14, { marginTop: 1, children: /* @__PURE__ */ jsx17(
6303
+ TextInput,
6304
+ {
6305
+ value: customDateText,
6306
+ onChange: setCustomDateText,
6307
+ onSubmit: (val) => {
6308
+ const trimmed = val.trim();
6309
+ setDateFilter(trimmed ? trimmed : "all");
6310
+ setFocusedControl("start");
6311
+ },
6312
+ width: 24
6313
+ }
6314
+ ) }),
6315
+ /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: "\u21B5 confirm \xB7 esc cancel" })
6316
+ ] }) : /* @__PURE__ */ jsxs14(Fragment4, { children: [
6317
+ /* @__PURE__ */ jsxs14(Box14, { justifyContent: "space-between", children: [
6318
+ /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
6319
+ "[p] Platform:",
6320
+ " ",
6321
+ /* @__PURE__ */ jsx17(
6322
+ Text16,
6323
+ {
6324
+ color: theme.primary,
6325
+ bold: true,
6326
+ inverse: focusedControl === "platform",
6327
+ children: defaultPlatform.toUpperCase()
6328
+ }
6329
+ )
6330
+ ] }),
6331
+ /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
6332
+ "[c] Media:",
6333
+ " ",
6334
+ /* @__PURE__ */ jsx17(
6335
+ Text16,
6336
+ {
6337
+ color: theme.primary,
6338
+ bold: true,
6339
+ inverse: focusedControl === "choice",
6340
+ children: choice === "all" ? "EVERYTHING" : choice.toUpperCase()
6341
+ }
6342
+ )
6343
+ ] }),
6344
+ /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
6345
+ "[l] Posts:",
6346
+ " ",
6347
+ /* @__PURE__ */ jsx17(
6348
+ Text16,
6349
+ {
6350
+ color: theme.primary,
6351
+ bold: true,
6352
+ inverse: focusedControl === "limit",
6353
+ children: limit ? `${limit}` : "ALL"
6354
+ }
6355
+ )
6356
+ ] })
6357
+ ] }),
6358
+ /* @__PURE__ */ jsxs14(Box14, { justifyContent: "space-between", marginTop: 1, children: [
6359
+ /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
6360
+ "[d] Date:",
6361
+ " ",
6362
+ /* @__PURE__ */ jsx17(
6363
+ Text16,
6364
+ {
6365
+ color: theme.primary,
6366
+ bold: true,
6367
+ inverse: focusedControl === "date",
6368
+ children: dateFilter.toUpperCase()
6369
+ }
6370
+ )
6371
+ ] }),
6372
+ /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
6373
+ "[m] Meta:",
6374
+ " ",
6375
+ /* @__PURE__ */ jsx17(
6376
+ Text16,
6377
+ {
6378
+ color: theme.primary,
6379
+ bold: true,
6380
+ inverse: focusedControl === "meta",
6381
+ children: writeMetadata ? "ON" : "OFF"
6382
+ }
6383
+ )
6384
+ ] }),
6385
+ /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
6386
+ "[r] Resume:",
6387
+ " ",
6388
+ /* @__PURE__ */ jsx17(
6389
+ Text16,
6390
+ {
6391
+ color: theme.primary,
6392
+ bold: true,
6393
+ inverse: focusedControl === "resume",
6394
+ children: resume ? "ON" : "OFF"
6395
+ }
6396
+ )
6397
+ ] }),
6398
+ /* @__PURE__ */ jsx17(
6399
+ Text16,
6400
+ {
6401
+ color: theme.primary,
6402
+ bold: true,
6403
+ inverse: focusedControl === "start",
6404
+ children: "[\u21B5] Start Queue"
6405
+ }
6406
+ )
6407
+ ] })
6408
+ ] })
6409
+ }
6410
+ ),
6411
+ /* @__PURE__ */ jsx17(Box14, { marginTop: 1, justifyContent: "center", children: /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: focusedControl === "input" ? "Type handles or URLs \xB7 \u21B5 to start \xB7 \u2193 to customize options" : focusedControl === "tabs" ? "Space to switch mode \xB7 \u2193 to edit input" : "\u2190/\u2192/Tab choose option \xB7 Space/Enter toggle \xB7 \u21B5 start queue \xB7 \u2191 edit input" }) })
5898
6412
  ] }) }),
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: [
6413
+ phase === "running" && /* @__PURE__ */ jsx17(Panel, { title: "Batch Queue Running", width: Math.max(60, boxWidth), children: /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", paddingY: 1, children: [
5900
6414
  /* @__PURE__ */ jsxs14(Box14, { justifyContent: "space-between", marginBottom: 1, children: [
5901
6415
  /* @__PURE__ */ jsxs14(Text16, { color: theme.primary, bold: true, children: [
5902
6416
  "Account ",
@@ -5907,9 +6421,9 @@ function BatchView({
5907
6421
  percent,
5908
6422
  "%)"
5909
6423
  ] }),
5910
- /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: isCooldown ? "\u23F3 Anti-ban cooldown active" : "\u25B6 Downloading" })
6424
+ /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: isCooldown ? "\u23F3 Cooldown active" : "\u25B6 Downloading" })
5911
6425
  ] }),
5912
- /* @__PURE__ */ jsx17(ProgressBar, { percent, width: 58 }),
6426
+ /* @__PURE__ */ jsx17(ProgressBar, { percent, width: 54 }),
5913
6427
  /* @__PURE__ */ jsx17(Gap2, {}),
5914
6428
  currentItem ? /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", borderStyle: "round", borderColor: theme.primary, paddingX: 1, paddingY: 1, children: [
5915
6429
  /* @__PURE__ */ jsxs14(Box14, { children: [
@@ -5931,7 +6445,7 @@ function BatchView({
5931
6445
  /* @__PURE__ */ jsx17(Gap2, {}),
5932
6446
  /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: "[esc] Stop queue and return" })
5933
6447
  ] }) }),
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: [
6448
+ 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
6449
  /* @__PURE__ */ jsxs14(Text16, { bold: true, color: theme.primary, children: [
5936
6450
  "\u2713 Successfully processed ",
5937
6451
  result.total,
@@ -5951,7 +6465,7 @@ function BatchView({
5951
6465
  result.totalDownloaded
5952
6466
  ] }),
5953
6467
  /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5954
- "\u2022 Total items skipped (already archived): ",
6468
+ "\u2022 Total items skipped: ",
5955
6469
  result.totalSkipped
5956
6470
  ] }),
5957
6471
  /* @__PURE__ */ jsx17(Gap2, {}),
@@ -6015,7 +6529,7 @@ function frameRowSpan(row) {
6015
6529
  }
6016
6530
 
6017
6531
  // src/app.tsx
6018
- import { Fragment as Fragment3, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
6532
+ import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
6019
6533
  var TAGLINE = "grab any profile. paste. download. done.";
6020
6534
  var DOWNLOAD_BUTTON = "download";
6021
6535
  var Gap = ({ lines = 1 }) => /* @__PURE__ */ jsx18(Box15, { flexDirection: "column", flexShrink: 0, children: Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsx18(Text17, { children: " " }, i)) });
@@ -6134,6 +6648,7 @@ function InnerApp({
6134
6648
  const [urlInput, setUrlInput] = useState9(initialInput ?? "");
6135
6649
  const [clipboardUrl, setClipboardUrl] = useState9(void 0);
6136
6650
  const [settingsFocus, setSettingsFocus] = useState9("cookies");
6651
+ const [batchClickTargets, setBatchClickTargets] = useState9([]);
6137
6652
  const columns = stdout?.columns && stdout.columns > 0 ? stdout.columns : 80;
6138
6653
  const boxWidth = Math.max(14, Math.min(64, columns - 6));
6139
6654
  const contentWidth = Math.max(10, Math.min(columns - 4, 78));
@@ -6149,7 +6664,7 @@ function InnerApp({
6149
6664
  }
6150
6665
  return { name: "input" };
6151
6666
  });
6152
- useEffect7(() => {
6667
+ useEffect8(() => {
6153
6668
  try {
6154
6669
  const clip = readClipboard();
6155
6670
  if (clip && (clip.startsWith("http://") || clip.startsWith("https://") || clip.startsWith("@"))) {
@@ -6162,7 +6677,7 @@ function InnerApp({
6162
6677
  const clipboardAccepted = Boolean(clipboardUrl) && urlInput === clipboardUrl;
6163
6678
  const abortControllerRef = useRef5(null);
6164
6679
  const probingTimeoutRef = useRef5(null);
6165
- useEffect7(() => {
6680
+ useEffect8(() => {
6166
6681
  return () => {
6167
6682
  abortControllerRef.current?.abort();
6168
6683
  if (probingTimeoutRef.current) {
@@ -6187,6 +6702,7 @@ function InnerApp({
6187
6702
  const targetChoice = typeof choiceOrOptions === "string" ? choiceOrOptions : choiceOrOptions.choice;
6188
6703
  const targetLimit = typeof choiceOrOptions === "string" ? effectiveLimit : choiceOrOptions.limit ?? effectiveLimit;
6189
6704
  const targetSince = typeof choiceOrOptions === "string" ? since : choiceOrOptions.since ?? since;
6705
+ const targetUntil = typeof choiceOrOptions === "string" ? until : choiceOrOptions.until ?? until;
6190
6706
  const targetResume = typeof choiceOrOptions === "string" ? resume : choiceOrOptions.resume ?? resume;
6191
6707
  const targetWriteMetadata = typeof choiceOrOptions === "string" ? effectiveWriteMetadata : choiceOrOptions.writeMetadata ?? effectiveWriteMetadata;
6192
6708
  setPhase({ name: "downloading", profile, choice: targetChoice });
@@ -6197,7 +6713,7 @@ function InnerApp({
6197
6713
  choice: targetChoice,
6198
6714
  outputDir: targetDir,
6199
6715
  since: targetSince,
6200
- until,
6716
+ until: targetUntil,
6201
6717
  limit: targetLimit,
6202
6718
  resume: targetResume,
6203
6719
  writeMetadata: targetWriteMetadata,
@@ -6302,10 +6818,13 @@ function InnerApp({
6302
6818
  return;
6303
6819
  }
6304
6820
  if (key.escape) {
6305
- if (phase.name === "settings" || phase.name === "watcher" || phase.name === "batch") {
6821
+ if (phase.name === "settings" || phase.name === "watcher") {
6306
6822
  setPhase({ name: "input" });
6307
6823
  return;
6308
6824
  }
6825
+ if (phase.name === "batch") {
6826
+ return;
6827
+ }
6309
6828
  if (phase.name === "platform") {
6310
6829
  setPhase({ name: "input" });
6311
6830
  return;
@@ -6397,6 +6916,9 @@ function InnerApp({
6397
6916
  targets.push({ match: "Cookies Folder", padX: 2, padY: 1, action: () => setSettingsFocus("cookies") });
6398
6917
  targets.push({ match: "Download Directory", padX: 2, padY: 1, action: () => setSettingsFocus("download") });
6399
6918
  }
6919
+ if (phase.name === "batch") {
6920
+ targets.push(...batchClickTargets);
6921
+ }
6400
6922
  for (const [key, label] of hints) {
6401
6923
  const action = hintAction(key);
6402
6924
  if (action) targets.push({ match: `${key} ${label}`, action });
@@ -6416,7 +6938,8 @@ function InnerApp({
6416
6938
  handleInputSubmit,
6417
6939
  handlePlatformSelect,
6418
6940
  handleModeSelect,
6419
- resetToInput
6941
+ resetToInput,
6942
+ batchClickTargets
6420
6943
  ]);
6421
6944
  useMouseClick(
6422
6945
  (x, y) => {
@@ -6483,6 +7006,7 @@ function InnerApp({
6483
7006
  outputDir,
6484
7007
  initialLimit: effectiveLimit,
6485
7008
  initialSince: since,
7009
+ initialUntil: until,
6486
7010
  initialWriteMetadata: effectiveWriteMetadata,
6487
7011
  initialResume: resume,
6488
7012
  onSelect: (choiceOrOptions) => handleModeSelect(choiceOrOptions, phase.profile),
@@ -6556,10 +7080,11 @@ function InnerApp({
6556
7080
  gapComponent: Gap,
6557
7081
  defaultChoice: initialChoice,
6558
7082
  defaultLimit: effectiveLimit,
6559
- defaultWriteMetadata: effectiveWriteMetadata
7083
+ defaultWriteMetadata: effectiveWriteMetadata,
7084
+ onRegisterClickTargets: setBatchClickTargets
6560
7085
  }
6561
7086
  ),
6562
- hints.length > 0 ? /* @__PURE__ */ jsxs15(Fragment3, { children: [
7087
+ hints.length > 0 ? /* @__PURE__ */ jsxs15(Fragment5, { children: [
6563
7088
  /* @__PURE__ */ jsx18(Gap, { lines: 2 }),
6564
7089
  /* @__PURE__ */ jsx18(
6565
7090
  Shortcuts,
@@ -6909,6 +7434,7 @@ async function main() {
6909
7434
  let themeMode = "auto";
6910
7435
  let isWatchMode = false;
6911
7436
  let batchFile;
7437
+ let cliPlatform;
6912
7438
  let since;
6913
7439
  let until;
6914
7440
  let limit;
@@ -6958,6 +7484,16 @@ async function main() {
6958
7484
  batchFile = args[++i];
6959
7485
  } else if (arg.startsWith("--batch=")) {
6960
7486
  batchFile = arg.slice("--batch=".length);
7487
+ } else if (arg === "-p" || arg === "--platform") {
7488
+ const val = args[++i]?.toLowerCase();
7489
+ if (val === "instagram" || val === "tiktok" || val === "facebook" || val === "x") {
7490
+ cliPlatform = val;
7491
+ }
7492
+ } else if (arg.startsWith("--platform=")) {
7493
+ const val = arg.slice("--platform=".length).toLowerCase();
7494
+ if (val === "instagram" || val === "tiktok" || val === "facebook" || val === "x") {
7495
+ cliPlatform = val;
7496
+ }
6961
7497
  } else if (!arg.startsWith("-") && !initialInput) {
6962
7498
  initialInput = arg;
6963
7499
  }
@@ -7021,6 +7557,7 @@ async function main() {
7021
7557
  const result = await runBatchQueue({
7022
7558
  filePath: batchFile,
7023
7559
  choice,
7560
+ defaultPlatform: cliPlatform || "instagram",
7024
7561
  outputDir,
7025
7562
  since,
7026
7563
  until,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-nami",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "Open Nami: terminal-first bulk social media profile downloader for Instagram, TikTok, Facebook & X.",
5
5
  "type": "module",
6
6
  "bin": {