open-nami 1.2.2 → 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 +363 -153
  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.2",
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(
@@ -1205,7 +1209,8 @@ var IG_QUERIES = {
1205
1209
  PolarisProfilePostsTabContentQuery_connection: "39229011733413759",
1206
1210
  PolarisProfileReelsTabContentQuery_connection: "28989741540630190",
1207
1211
  PolarisProfileTaggedTabContentQuery_connection: "28794602806842110",
1208
- PolarisProfileStoryHighlightsTrayContentQuery: "26970053832668570",
1212
+ ProfileStoryHighlightsTrayContentQuery_connection: "38168009559465141",
1213
+ PolarisProfileStoryHighlightsTrayContentQuery: "38168009559465141",
1209
1214
  PolarisStoriesV3ReelPageGalleryQuery: "28262315486766731",
1210
1215
  PolarisStoriesV3TrayContainerQuery: "27703822975903310",
1211
1216
  PolarisProfilePageContentQuery: "28649589871350527"
@@ -1392,12 +1397,18 @@ var InstagramExtractor = class {
1392
1397
  __user: "0",
1393
1398
  __a: "1",
1394
1399
  __req: "0",
1400
+ __hs: "20731.HYP:instagram_web_pkg.2.1...0",
1395
1401
  dpr: "1",
1402
+ __ccg: "EXCELLENT",
1403
+ __rev: "1049285836",
1404
+ __hsi: "7693193318680449885",
1405
+ __comet_req: "7",
1406
+ jazoest: "26461",
1396
1407
  fb_api_caller_class: "RelayModern",
1397
- fb_api_req_friendly_name: "PolarisProfileStoryHighlightsTrayContentQuery",
1408
+ fb_api_req_friendly_name: "ProfileStoryHighlightsTrayContentQuery_connection",
1398
1409
  server_timestamps: "true",
1399
1410
  variables,
1400
- doc_id: IG_QUERIES.PolarisProfileStoryHighlightsTrayContentQuery
1411
+ doc_id: IG_QUERIES.ProfileStoryHighlightsTrayContentQuery_connection
1401
1412
  });
1402
1413
  if (tokens?.fbDtsg) bodyParams.set("fb_dtsg", tokens.fbDtsg);
1403
1414
  if (tokens?.lsd) bodyParams.set("lsd", tokens.lsd);
@@ -1405,14 +1416,18 @@ var InstagramExtractor = class {
1405
1416
  "Accept": "*/*",
1406
1417
  "Content-Type": "application/x-www-form-urlencoded",
1407
1418
  "X-IG-App-ID": IG_APP_ID,
1408
- "X-FB-Friendly-Name": "PolarisProfileStoryHighlightsTrayContentQuery",
1419
+ "X-IG-Max-Touch-Points": "0",
1420
+ "X-FB-Friendly-Name": "ProfileStoryHighlightsTrayContentQuery_connection",
1409
1421
  "Referer": "https://www.instagram.com/",
1410
1422
  "Origin": "https://www.instagram.com",
1411
- "X-ASBD-ID": "359341"
1423
+ "X-ASBD-ID": "359341",
1424
+ "Sec-Fetch-Dest": "empty",
1425
+ "Sec-Fetch-Mode": "cors",
1426
+ "Sec-Fetch-Site": "same-origin"
1412
1427
  };
1413
1428
  if (tokens?.csrfToken) headers["X-CSRFToken"] = tokens.csrfToken;
1414
1429
  if (tokens?.lsd) headers["X-FB-LSD"] = tokens.lsd;
1415
- const res = await this.httpClient.fetchJson("https://www.instagram.com/graphql/query", {
1430
+ const res = await this.httpClient.fetchJson("https://www.instagram.com/api/graphql", {
1416
1431
  method: "POST",
1417
1432
  body: bodyParams.toString(),
1418
1433
  cookiePath: options.cookiePath,
@@ -1423,7 +1438,7 @@ var InstagramExtractor = class {
1423
1438
  const edges = res?.data?.highlights?.edges;
1424
1439
  if (Array.isArray(edges)) {
1425
1440
  if (res?.errors && Array.isArray(res.errors) && res.errors.length > 0) {
1426
- logExtractorDebug("PolarisProfileStoryHighlightsTrayContentQuery partial field errors", res.errors);
1441
+ logExtractorDebug("ProfileStoryHighlightsTrayContentQuery_connection partial field errors", res.errors);
1427
1442
  }
1428
1443
  return edges.map((e) => e.node).filter(Boolean);
1429
1444
  }
@@ -1434,7 +1449,7 @@ var InstagramExtractor = class {
1434
1449
  }
1435
1450
  }
1436
1451
  } catch (err) {
1437
- logExtractorDebug("PolarisProfileStoryHighlightsTrayContentQuery", err);
1452
+ logExtractorDebug("ProfileStoryHighlightsTrayContentQuery_connection", err);
1438
1453
  const msg = err instanceof Error ? err.message : String(err);
1439
1454
  if (isExplicitAuthError(msg)) {
1440
1455
  authError = new Error("Instagram Highlights require an active authenticated session. Your session cookie is missing or expired. Update your cookies in Settings (^s).");
@@ -1481,7 +1496,13 @@ var InstagramExtractor = class {
1481
1496
  __user: "0",
1482
1497
  __a: "1",
1483
1498
  __req: "0",
1499
+ __hs: "20731.HYP:instagram_web_pkg.2.1...0",
1484
1500
  dpr: "1",
1501
+ __ccg: "EXCELLENT",
1502
+ __rev: "1049285836",
1503
+ __hsi: "7693193318680449885",
1504
+ __comet_req: "7",
1505
+ jazoest: "26461",
1485
1506
  fb_api_caller_class: "RelayModern",
1486
1507
  fb_api_req_friendly_name: "PolarisStoriesV3ReelPageGalleryQuery",
1487
1508
  server_timestamps: "true",
@@ -1494,14 +1515,18 @@ var InstagramExtractor = class {
1494
1515
  "Accept": "*/*",
1495
1516
  "Content-Type": "application/x-www-form-urlencoded",
1496
1517
  "X-IG-App-ID": IG_APP_ID,
1518
+ "X-IG-Max-Touch-Points": "0",
1497
1519
  "X-FB-Friendly-Name": "PolarisStoriesV3ReelPageGalleryQuery",
1498
1520
  "Referer": "https://www.instagram.com/",
1499
1521
  "Origin": "https://www.instagram.com",
1500
- "X-ASBD-ID": "359341"
1522
+ "X-ASBD-ID": "359341",
1523
+ "Sec-Fetch-Dest": "empty",
1524
+ "Sec-Fetch-Mode": "cors",
1525
+ "Sec-Fetch-Site": "same-origin"
1501
1526
  };
1502
1527
  if (tokens?.csrfToken) headers["X-CSRFToken"] = tokens.csrfToken;
1503
1528
  if (tokens?.lsd) headers["X-FB-LSD"] = tokens.lsd;
1504
- const res = await this.httpClient.fetchJson("https://www.instagram.com/graphql/query", {
1529
+ const res = await this.httpClient.fetchJson("https://www.instagram.com/api/graphql", {
1505
1530
  method: "POST",
1506
1531
  body: bodyParams.toString(),
1507
1532
  cookiePath: options.cookiePath,
@@ -1864,14 +1889,6 @@ var InstagramExtractor = class {
1864
1889
  if (!userId || userId === "[object Object]") {
1865
1890
  throw new Error(`Could not resolve user ID for @${username}. Profile may be private or unavailable.`);
1866
1891
  }
1867
- if (user?.latest_reel_media === 0) {
1868
- options.onProgress?.({
1869
- stage: "Stories",
1870
- found: 0,
1871
- message: `Profile @${username} has no active stories in the last 24 hours.`
1872
- });
1873
- return;
1874
- }
1875
1892
  options.onProgress?.({
1876
1893
  stage: "Stories",
1877
1894
  found: 0,
@@ -1901,14 +1918,6 @@ var InstagramExtractor = class {
1901
1918
  if (!userId || userId === "[object Object]") {
1902
1919
  throw new Error(`Could not resolve user ID for @${username}. Profile may be private or unavailable.`);
1903
1920
  }
1904
- if (user?.highlight_reel_count === 0) {
1905
- options.onProgress?.({
1906
- stage: "Highlights",
1907
- found: 0,
1908
- message: `Profile @${username} has no available highlights.`
1909
- });
1910
- return;
1911
- }
1912
1921
  options.onProgress?.({
1913
1922
  stage: "Highlights",
1914
1923
  found: 0,
@@ -3318,6 +3327,15 @@ function parseDateBoundary(input, boundary) {
3318
3327
  return nowSeconds - offsetSeconds;
3319
3328
  }
3320
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
+ }
3321
3339
  if (trimmed.length === 8) {
3322
3340
  const year = parseInt(trimmed.slice(0, 4), 10);
3323
3341
  const month = parseInt(trimmed.slice(4, 6), 10) - 1;
@@ -3333,6 +3351,14 @@ function parseDateBoundary(input, boundary) {
3333
3351
  }
3334
3352
  return num;
3335
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
+ }
3336
3362
  if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
3337
3363
  const timeStr = boundary === "until" ? "T23:59:59.999Z" : "T00:00:00.000Z";
3338
3364
  const parsed2 = /* @__PURE__ */ new Date(`${trimmed}${timeStr}`);
@@ -3346,8 +3372,34 @@ function parseDateBoundary(input, boundary) {
3346
3372
  }
3347
3373
  return void 0;
3348
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
+ }
3349
3397
  function parseLimit(input) {
3350
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
+ }
3351
3403
  const num = typeof input === "number" ? input : parseInt(String(input).trim(), 10);
3352
3404
  if (isNaN(num) || num <= 0) return void 0;
3353
3405
  return Math.floor(num);
@@ -4316,7 +4368,7 @@ async function dispatchDownload(options) {
4316
4368
  }
4317
4369
 
4318
4370
  // src/components/views/picking-view.tsx
4319
- 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";
4320
4372
  function ChoiceIndicator({ isSelected }) {
4321
4373
  const theme = useTheme();
4322
4374
  return /* @__PURE__ */ jsx8(Box6, { marginRight: 1, children: /* @__PURE__ */ jsx8(Text7, { color: theme.primary, children: isSelected ? "\u276F" : " " }) });
@@ -4340,6 +4392,8 @@ function getPickingOptions(platform) {
4340
4392
  });
4341
4393
  return items;
4342
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"];
4343
4397
  function PickingView({
4344
4398
  contentWidth,
4345
4399
  profile,
@@ -4357,14 +4411,17 @@ function PickingView({
4357
4411
  const items = getPickingOptions(profile.platform);
4358
4412
  const [limit, setLimit] = useState4(initialLimit);
4359
4413
  const [dateFilter, setDateFilter] = useState4(() => {
4360
- if (typeof initialSince === "string" && (initialSince === "24h" || initialSince === "7d" || initialSince === "30d")) {
4361
- return initialSince;
4414
+ if (typeof initialSince === "string" && initialSince.trim()) {
4415
+ return initialSince.trim();
4362
4416
  }
4363
4417
  return "all";
4364
4418
  });
4365
4419
  const [writeMetadata, setWriteMetadata] = useState4(initialWriteMetadata);
4366
4420
  const [resume, setResume] = useState4(initialResume);
4367
4421
  const [hasCheckpoint, setHasCheckpoint] = useState4(false);
4422
+ const [editingModal, setEditingModal] = useState4(null);
4423
+ const [customLimitText, setCustomLimitText] = useState4("");
4424
+ const [customDateText, setCustomDateText] = useState4("");
4368
4425
  const targetDir = resolveDefaultDownloadDir(profile.platform, profile.username, outputDir);
4369
4426
  useEffect4(() => {
4370
4427
  let isMounted = true;
@@ -4380,29 +4437,40 @@ function PickingView({
4380
4437
  };
4381
4438
  }, [targetDir]);
4382
4439
  const cycleLimit = useCallback(() => {
4383
- setLimit((prev) => {
4384
- if (!prev) return 10;
4385
- if (prev === 10) return 25;
4386
- if (prev === 25) return 50;
4387
- if (prev === 50) return 100;
4388
- return void 0;
4389
- });
4390
- }, []);
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]);
4391
4450
  const cycleDate = useCallback(() => {
4392
- setDateFilter((prev) => {
4393
- if (prev === "all") return "24h";
4394
- if (prev === "24h") return "7d";
4395
- if (prev === "7d") return "30d";
4396
- return "all";
4397
- });
4398
- }, []);
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]);
4399
4461
  const toggleMetadata = useCallback(() => {
4400
4462
  setWriteMetadata((prev) => !prev);
4401
4463
  }, []);
4402
4464
  const toggleResume = useCallback(() => {
4403
4465
  setResume((prev) => !prev);
4404
4466
  }, []);
4405
- useInput2((input) => {
4467
+ useInput2((input, key) => {
4468
+ if (editingModal) {
4469
+ if (key.escape) {
4470
+ setEditingModal(null);
4471
+ }
4472
+ return;
4473
+ }
4406
4474
  if (input === "l" || input === "L") {
4407
4475
  cycleLimit();
4408
4476
  return;
@@ -4421,10 +4489,13 @@ function PickingView({
4421
4489
  }
4422
4490
  }, { isActive: Boolean(process.stdin.isTTY) });
4423
4491
  const handleItemSelect = (selectedChoice) => {
4492
+ if (editingModal) return;
4493
+ const dateRange = resolveDateFilter(dateFilter);
4424
4494
  onSelect({
4425
4495
  choice: selectedChoice,
4426
4496
  limit,
4427
- since: dateFilter === "all" ? void 0 : dateFilter,
4497
+ since: dateRange.since,
4498
+ until: dateRange.until,
4428
4499
  writeMetadata,
4429
4500
  resume
4430
4501
  });
@@ -4442,12 +4513,11 @@ function PickingView({
4442
4513
  profile.cleanUrl ? ` \xB7 ${profile.cleanUrl}` : ""
4443
4514
  ] }),
4444
4515
  /* @__PURE__ */ jsx8(Gap2, {}),
4445
- /* @__PURE__ */ jsx8(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: "Choose media category to download." }),
4446
- /* @__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." }),
4447
4517
  hasCheckpoint ? /* @__PURE__ */ jsx8(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text7, { color: theme.primary, bold: true, children: "\u26A1 Checkpoint found (cursor saved)" }) }) : null
4448
4518
  ] }),
4449
4519
  /* @__PURE__ */ jsxs6(Panel, { title: "Download", width: 38, children: [
4450
- /* @__PURE__ */ jsx8(
4520
+ !editingModal && /* @__PURE__ */ jsx8(
4451
4521
  SelectInput,
4452
4522
  {
4453
4523
  indicatorComponent: ChoiceIndicator,
@@ -4461,29 +4531,61 @@ function PickingView({
4461
4531
  onHighlight: (item) => onHighlight?.(item.value)
4462
4532
  }
4463
4533
  ),
4464
- /* @__PURE__ */ jsxs6(
4534
+ /* @__PURE__ */ jsx8(
4465
4535
  Box6,
4466
4536
  {
4467
4537
  flexDirection: "column",
4468
4538
  marginTop: 1,
4469
4539
  borderStyle: "round",
4470
- borderColor: theme.gray,
4471
- borderDimColor: true,
4540
+ borderColor: editingModal ? theme.primary : theme.gray,
4541
+ borderDimColor: !editingModal,
4472
4542
  paddingX: 1,
4473
- 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: [
4474
4576
  /* @__PURE__ */ jsxs6(Box6, { justifyContent: "space-between", children: [
4475
4577
  /* @__PURE__ */ jsxs6(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: [
4476
- "[l] Limit: ",
4578
+ "[l] Posts: ",
4477
4579
  /* @__PURE__ */ jsx8(Text7, { color: theme.primary, bold: true, children: limit ? `${limit}` : "ALL" })
4478
4580
  ] }),
4479
4581
  /* @__PURE__ */ jsxs6(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: [
4480
4582
  "[d] Date: ",
4481
- /* @__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() })
4482
4584
  ] })
4483
4585
  ] }),
4484
4586
  /* @__PURE__ */ jsxs6(Box6, { justifyContent: "space-between", children: [
4485
4587
  /* @__PURE__ */ jsxs6(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: [
4486
- "[m] Sidecars: ",
4588
+ "[m] Meta: ",
4487
4589
  /* @__PURE__ */ jsx8(Text7, { color: theme.primary, bold: true, children: writeMetadata ? "ON" : "OFF" })
4488
4590
  ] }),
4489
4591
  /* @__PURE__ */ jsxs6(Text7, { color: theme.gray, dimColor: theme.dimSecondary, children: [
@@ -4491,7 +4593,7 @@ function PickingView({
4491
4593
  /* @__PURE__ */ jsx8(Text7, { color: theme.primary, bold: true, children: resume ? "ON" : "OFF" })
4492
4594
  ] })
4493
4595
  ] })
4494
- ]
4596
+ ] })
4495
4597
  }
4496
4598
  )
4497
4599
  ] })
@@ -4706,7 +4808,7 @@ function DownloadingView({
4706
4808
  // src/components/views/completion-view.tsx
4707
4809
  import os7 from "os";
4708
4810
  import { Box as Box10, Text as Text12 } from "ink";
4709
- 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";
4710
4812
  var DONE_LABEL = "\u21B5 download another";
4711
4813
  function CompletionView({
4712
4814
  profile,
@@ -4718,10 +4820,10 @@ function CompletionView({
4718
4820
  const plural = result.downloadedCount === 1 ? "" : "s";
4719
4821
  const skipText = result.skippedCount > 0 ? ` (${result.skippedCount} archived)` : "";
4720
4822
  return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", alignItems: "center", children: [
4721
- /* @__PURE__ */ jsx13(Text12, { children: isZero ? /* @__PURE__ */ jsxs10(Fragment2, { children: [
4823
+ /* @__PURE__ */ jsx13(Text12, { children: isZero ? /* @__PURE__ */ jsxs10(Fragment3, { children: [
4722
4824
  /* @__PURE__ */ jsx13(Text12, { bold: true, color: theme.primary, children: "\u2713 profile scanned " }),
4723
4825
  /* @__PURE__ */ jsx13(Text12, { color: theme.primary, children: "\u2014 0 media items found" })
4724
- ] }) : /* @__PURE__ */ jsxs10(Fragment2, { children: [
4826
+ ] }) : /* @__PURE__ */ jsxs10(Fragment3, { children: [
4725
4827
  /* @__PURE__ */ jsx13(Text12, { bold: true, color: theme.primary, children: "\u2713 profile downloaded! " }),
4726
4828
  /* @__PURE__ */ jsx13(Text12, { color: theme.primary, children: "saved to:" })
4727
4829
  ] }) }),
@@ -5641,7 +5743,9 @@ async function runBatchQueue(options) {
5641
5743
  }
5642
5744
 
5643
5745
  // src/components/views/batch-view.tsx
5644
- 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"];
5645
5749
  function BatchView({
5646
5750
  onBack,
5647
5751
  outputDir,
@@ -5657,6 +5761,9 @@ function BatchView({
5657
5761
  const [rawText, setRawText] = useState8("");
5658
5762
  const [filePath, setFilePath] = useState8("");
5659
5763
  const [errorMessage, setErrorMessage] = useState8(null);
5764
+ const [focusSection, setFocusSection] = useState8("input");
5765
+ const [customLimitText, setCustomLimitText] = useState8("");
5766
+ const [customDateText, setCustomDateText] = useState8("");
5660
5767
  const [choice, setChoice] = useState8(defaultChoice);
5661
5768
  const [limit, setLimit] = useState8(defaultLimit);
5662
5769
  const [dateFilter, setDateFilter] = useState8("all");
@@ -5676,22 +5783,27 @@ function BatchView({
5676
5783
  });
5677
5784
  }, []);
5678
5785
  const cycleLimit = useCallback4(() => {
5679
- setLimit((prev) => {
5680
- if (!prev) return 10;
5681
- if (prev === 10) return 25;
5682
- if (prev === 25) return 50;
5683
- if (prev === 50) return 100;
5684
- return void 0;
5685
- });
5686
- }, []);
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]);
5687
5796
  const cycleDate = useCallback4(() => {
5688
- setDateFilter((prev) => {
5689
- if (prev === "all") return "24h";
5690
- if (prev === "24h") return "7d";
5691
- if (prev === "7d") return "30d";
5692
- return "all";
5693
- });
5694
- }, []);
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]);
5695
5807
  const toggleMetadata = useCallback4(() => {
5696
5808
  setWriteMetadata((prev) => !prev);
5697
5809
  }, []);
@@ -5722,12 +5834,14 @@ function BatchView({
5722
5834
  abortControllerRef.current = controller;
5723
5835
  setPhase("running");
5724
5836
  try {
5837
+ const dateRange = resolveDateFilter(dateFilter);
5725
5838
  const queueRes = await runBatchQueue({
5726
5839
  filePath: inputField === "file" ? filePath.trim() : void 0,
5727
5840
  lines: inputField === "text" ? lines : void 0,
5728
5841
  choice,
5729
5842
  outputDir,
5730
- since: dateFilter === "all" ? void 0 : dateFilter,
5843
+ since: dateRange.since,
5844
+ until: dateRange.until,
5731
5845
  limit,
5732
5846
  resume,
5733
5847
  writeMetadata,
@@ -5743,7 +5857,7 @@ function BatchView({
5743
5857
  return next;
5744
5858
  });
5745
5859
  },
5746
- onAccountProgress: (item, progress) => {
5860
+ onAccountProgress: (_item, progress) => {
5747
5861
  setCurrentProgressText(progress.statusText);
5748
5862
  },
5749
5863
  onAccountComplete: (item, idx) => {
@@ -5780,6 +5894,14 @@ function BatchView({
5780
5894
  }, [inputField, filePath, rawText, choice, outputDir, dateFilter, limit, resume, writeMetadata]);
5781
5895
  useInput5((input, key) => {
5782
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
+ }
5783
5905
  if (phase === "running") {
5784
5906
  abortControllerRef.current?.abort();
5785
5907
  setPhase("input");
@@ -5801,25 +5923,67 @@ function BatchView({
5801
5923
  return;
5802
5924
  }
5803
5925
  if (phase === "input") {
5804
- if (input === "c" || input === "C") {
5805
- cycleChoice();
5806
- return;
5807
- }
5808
- if (input === "l" || input === "L") {
5809
- cycleLimit();
5926
+ if (focusSection === "custom-limit" || focusSection === "custom-date") {
5810
5927
  return;
5811
5928
  }
5812
- if (input === "d" || input === "D") {
5813
- cycleDate();
5814
- return;
5815
- }
5816
- if (input === "m" || input === "M") {
5817
- toggleMetadata();
5818
- 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
+ }
5819
5952
  }
5820
- if (input === "r" || input === "R") {
5821
- toggleResume();
5822
- 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
+ }
5823
5987
  }
5824
5988
  }
5825
5989
  }, { isActive: Boolean(process.stdin.isTTY) });
@@ -5828,70 +5992,114 @@ function BatchView({
5828
5992
  const totalCount = queueItems.length > 0 ? queueItems.length : 1;
5829
5993
  const percent = Math.min(100, Math.floor(completedCount / totalCount * 100));
5830
5994
  return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", alignItems: "center", children: [
5831
- 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: [
5832
- /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: "Queue multiple creator profiles with automatic anti-ban cooldown between downloads." }),
5833
- /* @__PURE__ */ jsx17(Gap2, {}),
5834
- /* @__PURE__ */ jsxs14(Box14, { marginBottom: 1, children: [
5835
- /* @__PURE__ */ jsx17(Text16, { bold: true, color: inputField === "text" ? theme.primary : theme.gray, children: inputField === "text" ? "\u25CF Paste Links / Usernames" : "\u25CB Paste Links / Usernames" }),
5836
- /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: " (Tab to switch) " }),
5837
- /* @__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)" })
5838
6000
  ] }),
5839
- 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(
5840
6002
  TextInput,
5841
6003
  {
5842
6004
  value: rawText,
5843
6005
  onChange: setRawText,
5844
6006
  onSubmit: startQueue,
5845
- placeholder: "e.g. instagram.com/user1, @user2, tiktok.com/@user3",
5846
- 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",
5847
6010
  onTab: () => setInputField("file"),
5848
- onArrowDown: () => setInputField("file")
6011
+ onArrowDown: () => setFocusSection("options")
5849
6012
  }
5850
- ) }) : /* @__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(
5851
6014
  TextInput,
5852
6015
  {
5853
6016
  value: filePath,
5854
6017
  onChange: setFilePath,
5855
6018
  onSubmit: startQueue,
5856
- placeholder: "e.g. targets.txt or C:\\path\\to\\creators.txt",
5857
- 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",
5858
6022
  onTab: () => setInputField("text"),
5859
- onArrowUp: () => setInputField("text")
6023
+ onArrowDown: () => setFocusSection("options")
5860
6024
  }
5861
6025
  ) }),
5862
6026
  errorMessage ? /* @__PURE__ */ jsx17(Box14, { marginTop: 1, children: /* @__PURE__ */ jsxs14(Text16, { color: "red", children: [
5863
6027
  "\u2717 ",
5864
6028
  errorMessage
5865
6029
  ] }) }) : null,
5866
- /* @__PURE__ */ jsx17(Gap2, {}),
5867
- /* @__PURE__ */ jsx17(Text16, { bold: true, color: theme.primary, children: "Batch Queue Options:" }),
5868
- /* @__PURE__ */ jsxs14(Box14, { justifyContent: "space-between", marginTop: 1, children: [
5869
- /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5870
- "[c] Target: ",
5871
- /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: choice.toUpperCase() })
5872
- ] }),
5873
- /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5874
- "[l] Limit: ",
5875
- /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: limit ? `${limit} posts` : "ALL" })
5876
- ] }),
5877
- /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5878
- "[d] Date: ",
5879
- /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: dateFilter === "all" ? "ALL TIME" : dateFilter.toUpperCase() })
5880
- ] })
5881
- ] }),
5882
- /* @__PURE__ */ jsxs14(Box14, { justifyContent: "space-between", marginTop: 1, children: [
5883
- /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5884
- "[m] Sidecars: ",
5885
- /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: writeMetadata ? "ON" : "OFF" })
5886
- ] }),
5887
- /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5888
- "[r] Resume Checkpoint: ",
5889
- /* @__PURE__ */ jsx17(Text16, { color: theme.primary, bold: true, children: resume ? "ON" : "OFF" })
5890
- ] }),
5891
- /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: "[\u21B5] Start Queue" })
5892
- ] })
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" }) })
5893
6101
  ] }) }),
5894
- 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: [
5895
6103
  /* @__PURE__ */ jsxs14(Box14, { justifyContent: "space-between", marginBottom: 1, children: [
5896
6104
  /* @__PURE__ */ jsxs14(Text16, { color: theme.primary, bold: true, children: [
5897
6105
  "Account ",
@@ -5902,9 +6110,9 @@ function BatchView({
5902
6110
  percent,
5903
6111
  "%)"
5904
6112
  ] }),
5905
- /* @__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" })
5906
6114
  ] }),
5907
- /* @__PURE__ */ jsx17(ProgressBar, { percent, width: 58 }),
6115
+ /* @__PURE__ */ jsx17(ProgressBar, { percent, width: 54 }),
5908
6116
  /* @__PURE__ */ jsx17(Gap2, {}),
5909
6117
  currentItem ? /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", borderStyle: "round", borderColor: theme.primary, paddingX: 1, paddingY: 1, children: [
5910
6118
  /* @__PURE__ */ jsxs14(Box14, { children: [
@@ -5926,7 +6134,7 @@ function BatchView({
5926
6134
  /* @__PURE__ */ jsx17(Gap2, {}),
5927
6135
  /* @__PURE__ */ jsx17(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: "[esc] Stop queue and return" })
5928
6136
  ] }) }),
5929
- 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: [
5930
6138
  /* @__PURE__ */ jsxs14(Text16, { bold: true, color: theme.primary, children: [
5931
6139
  "\u2713 Successfully processed ",
5932
6140
  result.total,
@@ -5946,7 +6154,7 @@ function BatchView({
5946
6154
  result.totalDownloaded
5947
6155
  ] }),
5948
6156
  /* @__PURE__ */ jsxs14(Text16, { color: theme.gray, dimColor: theme.dimSecondary, children: [
5949
- "\u2022 Total items skipped (already archived): ",
6157
+ "\u2022 Total items skipped: ",
5950
6158
  result.totalSkipped
5951
6159
  ] }),
5952
6160
  /* @__PURE__ */ jsx17(Gap2, {}),
@@ -6010,7 +6218,7 @@ function frameRowSpan(row) {
6010
6218
  }
6011
6219
 
6012
6220
  // src/app.tsx
6013
- 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";
6014
6222
  var TAGLINE = "grab any profile. paste. download. done.";
6015
6223
  var DOWNLOAD_BUTTON = "download";
6016
6224
  var Gap = ({ lines = 1 }) => /* @__PURE__ */ jsx18(Box15, { flexDirection: "column", flexShrink: 0, children: Array.from({ length: lines }, (_, i) => /* @__PURE__ */ jsx18(Text17, { children: " " }, i)) });
@@ -6182,6 +6390,7 @@ function InnerApp({
6182
6390
  const targetChoice = typeof choiceOrOptions === "string" ? choiceOrOptions : choiceOrOptions.choice;
6183
6391
  const targetLimit = typeof choiceOrOptions === "string" ? effectiveLimit : choiceOrOptions.limit ?? effectiveLimit;
6184
6392
  const targetSince = typeof choiceOrOptions === "string" ? since : choiceOrOptions.since ?? since;
6393
+ const targetUntil = typeof choiceOrOptions === "string" ? until : choiceOrOptions.until ?? until;
6185
6394
  const targetResume = typeof choiceOrOptions === "string" ? resume : choiceOrOptions.resume ?? resume;
6186
6395
  const targetWriteMetadata = typeof choiceOrOptions === "string" ? effectiveWriteMetadata : choiceOrOptions.writeMetadata ?? effectiveWriteMetadata;
6187
6396
  setPhase({ name: "downloading", profile, choice: targetChoice });
@@ -6192,7 +6401,7 @@ function InnerApp({
6192
6401
  choice: targetChoice,
6193
6402
  outputDir: targetDir,
6194
6403
  since: targetSince,
6195
- until,
6404
+ until: targetUntil,
6196
6405
  limit: targetLimit,
6197
6406
  resume: targetResume,
6198
6407
  writeMetadata: targetWriteMetadata,
@@ -6478,6 +6687,7 @@ function InnerApp({
6478
6687
  outputDir,
6479
6688
  initialLimit: effectiveLimit,
6480
6689
  initialSince: since,
6690
+ initialUntil: until,
6481
6691
  initialWriteMetadata: effectiveWriteMetadata,
6482
6692
  initialResume: resume,
6483
6693
  onSelect: (choiceOrOptions) => handleModeSelect(choiceOrOptions, phase.profile),
@@ -6554,7 +6764,7 @@ function InnerApp({
6554
6764
  defaultWriteMetadata: effectiveWriteMetadata
6555
6765
  }
6556
6766
  ),
6557
- hints.length > 0 ? /* @__PURE__ */ jsxs15(Fragment3, { children: [
6767
+ hints.length > 0 ? /* @__PURE__ */ jsxs15(Fragment5, { children: [
6558
6768
  /* @__PURE__ */ jsx18(Gap, { lines: 2 }),
6559
6769
  /* @__PURE__ */ jsx18(
6560
6770
  Shortcuts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-nami",
3
- "version": "1.2.2",
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": {