From 9a4564b44f66f3557ad28ba8968fdafba6b70aaa Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 01:30:00 +0800 Subject: [PATCH 01/55] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20backend=20=E5=96=AE?= =?UTF-8?q?=E5=85=83=E6=B8=AC=E8=A9=A6=E8=A6=86=E8=93=8B=E6=8E=83=E6=8F=8F?= =?UTF-8?q?=E8=88=87=E9=88=8E=E5=AD=90=E9=82=8F=E8=BC=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test_backend.py | 188 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 test_backend.py diff --git a/test_backend.py b/test_backend.py new file mode 100644 index 0000000..6f2cc6f --- /dev/null +++ b/test_backend.py @@ -0,0 +1,188 @@ +"""Tests for backend module - scanning and downloading logic.""" +import os +import tempfile +import threading +import unittest +from unittest.mock import MagicMock, patch + +from backend import Backend + + +class MockUIManager: + """Mock UI manager for testing backend.""" + + def __init__(self): + self.timeout = (10, 30) + self.USER_AGENT = "TestAgent/1.0" + self.is_scanning = False + self.scanned_urls = 0 + self.total_urls = 0 + self.files_dict = {} + self.folders = {} + self.files_dict_lock = threading.Lock() + self.folders_dict_lock = threading.Lock() + self.scan_pause_event = threading.Event() + self.scan_pause_event.set() + self.should_stop = False + self.pause_event = threading.Event() + self.pause_event.set() + self.log_messages = [] + self.session = MagicMock() + + def log_message(self, message): + self.log_messages.append(message) + + def notify_info(self, title, message): + pass + + def notify_warning(self, title, message): + pass + + def notify_error(self, title, message): + pass + + def on_scan_started(self, url): + self.is_scanning = True + + def on_scan_progress(self, scanned_urls, total_urls): + self.scanned_urls = scanned_urls + self.total_urls = total_urls + + def on_scan_item(self, **payload): + is_directory = payload.get("is_directory") + if is_directory: + with self.folders_dict_lock: + self.folders[payload.get("path")] = payload.get("url") + else: + with self.files_dict_lock: + self.files_dict[payload.get("full_path")] = payload.get("url") + + def on_scan_finished(self, stopped): + self.is_scanning = False + + +class TestBackendScan(unittest.TestCase): + """Tests for website scanning functionality.""" + + def setUp(self): + self.ui = MockUIManager() + self.backend = Backend(self.ui) + self.backend.should_stop = False + + def test_backend_initialization(self): + """Test Backend initializes with correct defaults.""" + self.assertFalse(self.backend.should_stop) + self.assertEqual(self.backend.ui_manager, self.ui) + + def test_log_message(self): + """Test _log method calls ui_manager.log_message.""" + self.backend._log("Test message") + self.assertIn("Test message", self.ui.log_messages) + + def test_log_message_fallback(self): + """Test _log falls back to print when ui_manager has no log_message.""" + self.backend.ui_manager = MagicMock() + del self.backend.ui_manager.log_message + # Should not raise + self.backend._log("Test") + + def test_notify_info(self): + """Test _notify calls correct handler for info.""" + with patch.object(self.ui, 'notify_info') as mock_notify: + self.backend._notify("info", "Title", "Message") + mock_notify.assert_called_once_with("Title", "Message") + + def test_notify_error(self): + """Test _notify calls correct handler for error.""" + with patch.object(self.ui, 'notify_error') as mock_notify: + self.backend._notify("error", "Title", "Message") + mock_notify.assert_called_once_with("Title", "Message") + + def test_notify_warning(self): + """Test _notify calls correct handler for warning.""" + with patch.object(self.ui, 'notify_warning') as mock_notify: + self.backend._notify("warning", "Title", "Message") + mock_notify.assert_called_once_with("Title", "Message") + + def test_call_ui_hook_exists(self): + """Test _call_ui_hook calls existing hook.""" + self.ui.test_hook = MagicMock(return_value=True) + result = self.backend._call_ui_hook("test_hook", foo="bar") + self.assertTrue(result) + self.ui.test_hook.assert_called_once_with(foo="bar") + + def test_call_ui_hook_missing(self): + """Test _call_ui_hook handles missing hook gracefully.""" + result = self.backend._call_ui_hook("nonexistent_hook") + self.assertFalse(result) + + def test_call_ui_hook_exception(self): + """Test _call_ui_hook handles hook exception gracefully.""" + def failing_hook(): + raise Exception("Test error") + + self.ui.failing_hook = failing_hook + result = self.backend._call_ui_hook("failing_hook") + self.assertFalse(result) + + +class TestBackendDownload(unittest.TestCase): + """Tests for file download functionality.""" + + def setUp(self): + self.ui = MockUIManager() + self.backend = Backend(self.ui) + self.temp_dir = tempfile.mkdtemp() + + def tearDown(self): + import shutil + shutil.rmtree(self.temp_dir, ignore_errors=True) + + @unittest.skip("Requires complex network mocking") + def test_download_file_creates_directory(self): + """Test download_file creates target directory if needed.""" + subdir = os.path.join(self.temp_dir, "subdir", "nested") + test_file = os.path.join(subdir, "test.txt") + + # Directory shouldn't exist yet + self.assertFalse(os.path.exists(subdir)) + + # Mock session.get to avoid actual network call + mock_response = MagicMock() + mock_response.headers = {"content-length": "10"} + mock_response.iter_content = lambda size: [b"test data"] + mock_response.raise_for_status = MagicMock() + + self.ui.session.get = MagicMock(return_value=mock_response) + + # Mock the pause event to not block + self.ui.pause_event = threading.Event() + self.ui.pause_event.set() + + result = self.backend.download_file( + "http://example.com/test.txt", + test_file, + "test.txt" + ) + + # File should be created + self.assertTrue(os.path.exists(test_file)) + + +class TestBackendProcessFile(unittest.TestCase): + """Tests for file processing in scan.""" + + def setUp(self): + self.ui = MockUIManager() + self.backend = Backend(self.ui) + + def test_process_file_extracts_filename(self): + """Test _process_file correctly extracts file info from URL.""" + # This tests the parsing logic without network calls + url = "http://example.com/path/to/file.txt" + # The method should handle this without error + # Full test would require mocking session.head + + +if __name__ == "__main__": + unittest.main() From 992188209ee8b9be3d8c4ce08921f5d082a02d49 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 18:56:54 +0800 Subject: [PATCH 02/55] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20CTk=20=E9=81=B7?= =?UTF-8?q?=E7=A7=BB=E8=A8=AD=E8=A8=88=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- docs/plans/2026-03-09-ctk-migration-design.md | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 docs/plans/2026-03-09-ctk-migration-design.md diff --git a/docs/plans/2026-03-09-ctk-migration-design.md b/docs/plans/2026-03-09-ctk-migration-design.md new file mode 100644 index 0000000..7ec469f --- /dev/null +++ b/docs/plans/2026-03-09-ctk-migration-design.md @@ -0,0 +1,121 @@ +# Design: 遷移 UI 至 CustomTkinter + +日期:2026-03-09 +狀態:已核准 + +## 背景 + +目前 UI 混用 ttkbootstrap(`ui_ttkb.py`)和 CustomTkinter(`ui_downloads.py`),造成以下問題: +- 兩套主題系統互相干擾,跨平台外觀不一致 +- `ui_theme.py` 的 design token 是 CTk 格式,卻套用在 ttkbootstrap widget 上 +- 維護成本高,新功能難以保持風格一致 + +## 目標 + +- 統一使用 CustomTkinter 作為唯一 UI 框架 +- 保留所有現有功能,一個都不減 +- 確保多執行緒安全(Queue + `after()` 模式) +- 好看、輕量、跨平台一致 + +## 架構 + +### 檔案變動 + +| 檔案 | 動作 | +|---|---| +| `ui_ctk.py` | **新增**,主 UI 類別,取代 `ui_ttkb.py` | +| `ui_ttkb.py` | 保留不動(歷史參考),入口不再指向它 | +| `ui_downloads.py` | 小幅清理,確保與新主題一致 | +| `ui_theme.py` | 移除 ttkbootstrap 相關函式,保留 CTk token + Treeview 樣式 | +| `index_ripper.py` | 更新入口,改為載入 `ui_ctk.py` | +| `index_ripper_ttkb.py` | 保留,作為 ttkb 備用入口 | + +### Widget 對應 + +| ttkbootstrap | CustomTkinter | 備注 | +|---|---|---| +| `ttkb.Window` | `ctk.CTk()` | | +| `ttkb.Frame` | `ctk.CTkFrame` | | +| `ttkb.Button` | `ctk.CTkButton` | | +| `ttkb.Label` | `ctk.CTkLabel` | | +| `ttkb.Entry` | `ctk.CTkEntry` | | +| `ttkb.Progressbar` | `ctk.CTkProgressBar` | | +| `ttkb.Notebook` | `ctk.CTkTabview` | | +| `ttkb.Combobox` | `ctk.CTkOptionMenu` | Threads 選單 | +| `ttkb.Treeview` | `ttk.Treeview`(保留)| CTk 無此 widget | +| `tk.Text` | `ctk.CTkTextbox` | Logs | +| `tk.Checkbutton` | `ctk.CTkCheckBox` | 檔案類型篩選 | +| Scrollbar | `ctk.CTkScrollableFrame` | Downloads list | + +> `ttk.Treeview` 是唯一例外,透過 `ttk.Style()` 配合 CTk 主題色統一外觀。 + +## 功能清單(必須全部實作) + +### 輸入區 +- [ ] URL 輸入框 +- [ ] Cmd/Ctrl+V paste 處理(含右鍵選單) +- [ ] 全域 paste 偵測(焦點在 URL 欄時自動填入) +- [ ] Status label(Ready / Scanning / Error 等狀態) + +### 掃描控制 +- [ ] Scan 按鈕 +- [ ] Pause Scan / Resume Scan 按鈕(掃描中才啟用) +- [ ] Clear 按鈕 +- [ ] Ctrl+F 聚焦搜尋欄 +- [ ] Ctrl+L 聚焦 Logs + +### 篩選區 +- [ ] 動態產生 Checkbox(依掃描到的副檔名) +- [ ] Select All Types / Deselect All Types 按鈕 +- [ ] 水平可捲動容器(檔案類型多時) + +### 下載控制 +- [ ] Download Selected 按鈕 +- [ ] Pause / Resume 下載按鈕(下載中才啟用) +- [ ] Choose Folder 按鈕 +- [ ] Threads 數量選單(1–10) +- [ ] Hide / Show Panels 切換 + +### Treeview(掃描結果) +- [ ] 欄位:Path / Size / Type(full_path 隱藏) +- [ ] 搜尋即時過濾 +- [ ] 欄位排序(點標題) +- [ ] 行勾選(✔ 標記,`checked` tag) +- [ ] Cmd/Ctrl+A 全選 +- [ ] Space 切換勾選 +- [ ] Enter 確認 +- [ ] 拖曳多選 +- [ ] 右鍵選單(Select All / Deselect All / Expand All / Collapse All) + +### 進度區 +- [ ] 整體進度條(`ctk.CTkProgressBar`) +- [ ] 進度文字 label + +### Tabs 面板 +- [ ] CTkTabview,兩個 tab:Downloads / Logs +- [ ] Downloads tab:每項有名稱、進度條、狀態文字、取消按鈕 +- [ ] Logs tab:`ctk.CTkTextbox`,可捲動,`log_message()` 方法 + +### 執行緒安全 +- [ ] Queue + `window.after()` 輪詢模式完整保留 +- [ ] `scan_item_buffer` 批次 flush(throttle) +- [ ] `files_dict_lock` / `folders_dict_lock` 保留 + +### 其他 +- [ ] `--ui-smoke` 模式(最小化 UI,不啟動完整介面) +- [ ] `INDEX_RIPPER_DEBUG` 環境變數支援 +- [ ] `INDEX_RIPPER_MODAL_DIALOGS` 環境變數支援 +- [ ] `on_closing` 視窗關閉處理 +- [ ] 所有鍵盤快捷鍵綁定 + +## 執行緒安全說明 + +所有背景執行緒(掃描、下載)只能寫入 Queue,不直接操作 widget。 +主執行緒透過 `window.after(N, poll_fn)` 週期性讀取 Queue 並更新 UI。 +CTk 的 `after()` 繼承自 tkinter,行為完全相同,現有模式無需修改。 + +## 測試策略 + +- 現有 `test_ui_ttkb.py` 作為功能對照參考 +- 新增 `test_ui_ctk.py`,覆蓋相同的 smoke / unit 測試 +- `--ui-smoke` 模式用於 CI 驗證視窗可正常建立並關閉 From 88316cb26ff12e151453a8ac071ea074c28ad959 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 19:00:25 +0800 Subject: [PATCH 03/55] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20CTk=20=E9=81=B7?= =?UTF-8?q?=E7=A7=BB=E5=AF=A6=E4=BD=9C=E8=A8=88=E5=8A=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- docs/plans/2026-03-09-ctk-migration.md | 1195 ++++++++++++++++++++++++ 1 file changed, 1195 insertions(+) create mode 100644 docs/plans/2026-03-09-ctk-migration.md diff --git a/docs/plans/2026-03-09-ctk-migration.md b/docs/plans/2026-03-09-ctk-migration.md new file mode 100644 index 0000000..ef3ab84 --- /dev/null +++ b/docs/plans/2026-03-09-ctk-migration.md @@ -0,0 +1,1195 @@ +# CustomTkinter 遷移實作計劃 + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 建立 `ui_ctk.py`,以 CustomTkinter 統一 UI 框架,取代 ttkbootstrap 版本,保留所有現有功能。 + +**Architecture:** 保留 `ui_ttkb.py` 的模組化架構(Queue + after() 執行緒安全模式),以 CTk widget 替換 ttkbootstrap widget。`ui_downloads.py` 幾乎不動(已是 CTk)。`ttk.Treeview` 保留(CTk 無此 widget),以 `ttk.Style()` 配合 CTk 主題色。 + +**Tech Stack:** customtkinter>=5.2.0、tkinter(內建)、ttk.Treeview(內建)、requests、backend.py(不動) + +**References:** +- `ui_ttkb.py`:架構參考(模組化、Queue 模式) +- `index_ripper.py`:CTk widget 用法參考(舊版但已是 CTk) +- `ui_downloads.py`:DownloadsPanel(直接沿用) +- `ui_theme.py`:design token(清理後沿用) +- `docs/plans/2026-03-09-ctk-migration-design.md`:設計文件 + +--- + +### Task 1: 清理 ui_theme.py + +**目的:** 移除 ttkbootstrap bridge,只保留 CTk 相關的 token 與 Treeview style。 + +**Files:** +- Modify: `ui_theme.py` + +**Step 1: 確認現有內容** + +讀 `ui_theme.py` 全文,確認要刪的函式: +- `apply_bootstrap_theme()` — 刪除(ttkb 專用) +- 其餘全部保留 + +**Step 2: 刪除 apply_bootstrap_theme** + +從 `ui_theme.py` 移除 `apply_bootstrap_theme` 函式(第 10–22 行)。 + +修改後確保剩下: +- `apply_app_theme(ctk)` ✓ +- `ui_tokens()` ✓ +- `action_button_style_name(kind)` ✓ +- `configure_action_button_styles(window, ctk, ttk)` ✓ +- `configure_treeview_style(window, ctk, ttk)` ✓ +- `treeview_tag_colors(window)` ✓ + +**Step 3: 確認 index_ripper.py 的 import 仍正常** + +`index_ripper.py` 第 66–73 行 import 了 `apply_bootstrap_theme`,也要同步移除該行。 + +**Step 4: 執行現有測試確認不壞** + +```bash +python -m pytest test_ui_theme.py -v +``` + +Expected: 所有 PASS(若有失敗,修正後再繼續) + +**Step 5: Commit** + +```bash +git add ui_theme.py index_ripper.py +git commit -m "refactor(theme): 移除 ttkbootstrap bridge,統一用 CTk token" +``` + +--- + +### Task 2: ui_ctk.py 骨架 + 視窗建立 + +**目的:** 建立 `ui_ctk.py`,包含 `WebsiteCopierCtk` 類別的骨架、視窗初始化、以及 smoke 模式支援。 + +**Files:** +- Create: `ui_ctk.py` +- Create: `test_ui_ctk.py` + +**Step 1: 先寫失敗測試** + +建立 `test_ui_ctk.py`: + +```python +"""Smoke tests for ui_ctk.WebsiteCopierCtk.""" +import os +import sys +import unittest + +os.environ.setdefault("INDEX_RIPPER_MODAL_DIALOGS", "0") + + +class TestWebsiteCopierCtkSmoke(unittest.TestCase): + def _make(self): + import importlib + mod = importlib.import_module("ui_ctk") + return mod.WebsiteCopierCtk(ui_smoke=True) + + def test_import(self): + import ui_ctk # noqa: F401 + + def test_smoke_init_and_destroy(self): + app = self._make() + app.window.after(0, app.window.destroy) + app.run() + + def test_has_url_var(self): + app = self._make() + app.window.after(0, app.window.destroy) + app.run() + self.assertTrue(hasattr(app, "url_var")) + + def test_has_log_text(self): + app = self._make() + app.window.after(0, app.window.destroy) + app.run() + self.assertTrue(hasattr(app, "log_text")) + + +if __name__ == "__main__": + unittest.main() +``` + +**Step 2: 執行確認失敗** + +```bash +python -m pytest test_ui_ctk.py -v +``` + +Expected: `ModuleNotFoundError: No module named 'ui_ctk'` + +**Step 3: 建立 ui_ctk.py 骨架** + +```python +from __future__ import annotations + +import os +import threading +from concurrent.futures import ThreadPoolExecutor +from queue import Empty, Queue + +import tkinter as tk +from tkinter import filedialog, messagebox, ttk + +import customtkinter as ctk +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +from app_utils import default_download_folder, normalize_extension, safe_join, sanitize_filename +from backend import Backend +from ui_downloads import DownloadsPanel +from ui_theme import ( + apply_app_theme, + configure_action_button_styles, + configure_treeview_style, + treeview_tag_colors, + ui_tokens, +) + + +def should_skip_file_row(existing_entry) -> bool: + return existing_entry is not None + + +class WebsiteCopierCtk: + USER_AGENT = "IndexRipper/2.0" + + def __init__(self, ui_smoke: bool = False): + self._ui_smoke = bool(ui_smoke) + self.debug_enabled = os.environ.get("INDEX_RIPPER_DEBUG", "0") != "0" + self.use_modal_dialogs = os.environ.get("INDEX_RIPPER_MODAL_DIALOGS", "0") == "1" + + apply_app_theme(ctk) + self.window = ctk.CTk() + self.window.title("Index Ripper") + self.window.geometry("1200x900") + self.window.minsize(900, 650) + configure_action_button_styles(self.window, ctk, ttk) + self.ui_tokens = ui_tokens() + + self.backend = Backend(self) + + self.pause_event = threading.Event() + self.pause_event.set() + self.scan_pause_event = threading.Event() + self.scan_pause_event.set() + + self.is_scanning = False + self.scanned_urls = 0 + self.total_urls = 0 + + self.files_dict_lock = threading.Lock() + self.folders_dict_lock = threading.Lock() + self.files_dict: dict[str, dict] = {} + self.folders: dict[str, str] = {} + self.checked_items: set[str] = set() + self.checkbox_checked = "✔ " + + self.dir_queue = Queue() + self.file_queue = Queue() + self.scan_item_buffer = Queue() + self.is_processing_dirs = False + self.is_processing_files = False + self.scan_flush_interval_ms = 16 + self.scan_flush_batch_size = 200 + self.scan_flush_job = None + self._last_logged_queue_size = None + + self.file_types: dict[str, tk.BooleanVar] = {} + self.file_type_counts: dict[str, int] = {} + self.file_type_widgets: dict[str, ctk.CTkCheckBox] = {} + + self.download_path = "" + self.download_queue = Queue() + self.max_workers = 5 + self.executor = ThreadPoolExecutor(max_workers=self.max_workers) + self.active_downloads = [] + + retry_strategy = Retry( + total=3, + backoff_factor=0.1, + status_forcelist=[500, 502, 503, 504], + ) + self.session = requests.Session() + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=10, + pool_maxsize=10, + pool_block=False, + ) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + self.timeout = (10, 20) + + self._build_ui() + + self.window.protocol("WM_DELETE_WINDOW", self.on_closing) + if not self._ui_smoke: + self.window.bind("", self.focus_search) + self.window.bind("", self.focus_logs) + self.window.bind("", self.clear_search) + self.window.bind_all("", self._on_global_url_paste, add="+") + self.window.bind_all("", self._on_global_url_paste, add="+") + self.window.bind_all("", self._on_global_url_paste, add="+") + self.window.bind_all("", self._on_global_url_paste, add="+") + self.window.bind_all("<>", self._on_global_url_paste, add="+") + + def _build_ui(self) -> None: + if self._ui_smoke: + self._build_ui_smoke_only() + return + self._build_full_ui() + + def _build_ui_smoke_only(self) -> None: + self.url_var = tk.StringVar(value="https://example.com/") + self.search_var = tk.StringVar() + self.log_text = ctk.CTkTextbox(self.window, height=80) + self.log_text.pack(fill="both", expand=True, padx=10, pady=10) + self.log_text.insert("end", "UI smoke minimal view initialized.\n") + self.panels_notebook = None + self.logs_tab = None + + def _build_full_ui(self) -> None: + # 後續 Task 中逐步實作 + pass + + def run(self) -> None: + self.window.mainloop() + + def on_closing(self) -> None: + self.executor.shutdown(wait=False) + self.window.destroy() + + def log_message(self, message: str) -> None: + try: + self.log_text.insert("end", message + "\n") + self.log_text.see("end") + except Exception: + pass + + def _debug(self, message: str) -> None: + if not self.debug_enabled: + return + try: + print(f"[DEBUG] {message}") + except Exception: + pass + try: + if hasattr(self, "log_text"): + self.log_text.insert("end", f"[DEBUG] {message}\n") + self.log_text.see("end") + except Exception: + pass + + def notify_info(self, title: str, message: str) -> None: + if not self.use_modal_dialogs: + self.log_message(f"[INFO] {title}: {message}") + return + try: + messagebox.showinfo(title, message) + except Exception: + self.log_message(f"[INFO] {title}: {message}") + + def notify_warning(self, title: str, message: str) -> None: + if not self.use_modal_dialogs: + self.log_message(f"[WARN] {title}: {message}") + return + try: + messagebox.showwarning(title, message) + except Exception: + self.log_message(f"[WARN] {title}: {message}") + + def notify_error(self, title: str, message: str) -> None: + if not self.use_modal_dialogs: + self.log_message(f"[ERROR] {title}: {message}") + return + try: + messagebox.showerror(title, message) + except Exception: + self.log_message(f"[ERROR] {title}: {message}") +``` + +**Step 4: 執行測試確認通過** + +```bash +python -m pytest test_ui_ctk.py -v +``` + +Expected: 4 PASS + +**Step 5: Commit** + +```bash +git add ui_ctk.py test_ui_ctk.py +git commit -m "feat(ui): 新增 WebsiteCopierCtk 骨架與 smoke 測試" +``` + +--- + +### Task 3: Header 列(URL 輸入 + Status + 掃描按鈕) + +**目的:** 實作視窗頂部的 URL 輸入框、狀態標籤、Scan / Pause Scan / Clear 按鈕。 + +**Files:** +- Modify: `ui_ctk.py`(`_build_full_ui` 內容) +- Modify: `test_ui_ctk.py` + +**Step 1: 新增失敗測試** + +在 `test_ui_ctk.py` 加入: + +```python +def test_full_ui_has_scan_btn(self): + import ui_ctk + app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) + app.window.after(0, app.window.destroy) + app.run() + self.assertTrue(hasattr(app, "scan_btn")) + self.assertTrue(hasattr(app, "url_entry")) + self.assertTrue(hasattr(app, "status_label")) +``` + +**Step 2: 執行確認失敗** + +```bash +python -m pytest test_ui_ctk.py::TestWebsiteCopierCtkSmoke::test_full_ui_has_scan_btn -v +``` + +Expected: FAIL(`AttributeError: scan_btn`) + +**Step 3: 實作 _build_full_ui header 部分** + +在 `ui_ctk.py` 的 `_build_full_ui` 中加入: + +```python +def _build_full_ui(self) -> None: + self.window.grid_columnconfigure(0, weight=1) + self.window.grid_rowconfigure(2, weight=1) + + # --- Header --- + header = ctk.CTkFrame(self.window, fg_color="transparent") + header.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 0)) + header.grid_columnconfigure(1, weight=1) + + ctk.CTkLabel(header, text="URL", font=ctk.CTkFont(weight="bold")).grid( + row=0, column=0, sticky="w", padx=(0, 8) + ) + self.url_var = tk.StringVar() + self.url_entry = ctk.CTkEntry(header, textvariable=self.url_var, placeholder_text="https://") + self.url_entry.grid(row=0, column=1, sticky="ew", padx=(0, 8)) + + self.status_label = ctk.CTkLabel(header, text="Ready", text_color="#059669") + self.status_label.grid(row=0, column=2, sticky="e", padx=(0, 8)) + + actions = ctk.CTkFrame(header, fg_color="transparent") + actions.grid(row=0, column=3, sticky="e") + + self.scan_btn = ctk.CTkButton(actions, text="Scan", command=self.start_scan) + self.scan_btn.pack(side="left", padx=3) + + self.scan_pause_btn = ctk.CTkButton( + actions, text="Pause Scan", + fg_color=("gray70", "gray30"), + command=self.toggle_scan_pause, + state="disabled", + ) + self.scan_pause_btn.pack(side="left", padx=3) + + self.clear_scan_btn = ctk.CTkButton( + actions, text="Clear", + fg_color=("gray70", "gray30"), + command=self.clear_scan_results, + ) + self.clear_scan_btn.pack(side="left", padx=3) + + # URL paste bindings + self.url_context_menu = tk.Menu(self.window, tearoff=0) + self.url_context_menu.add_command(label="Paste", command=self._paste_into_url_entry) + self.url_entry.bind("", self._on_url_paste) + self.url_entry.bind("", self._on_url_paste) + self.url_entry.bind("", self._on_url_paste) + self.url_entry.bind("", self._on_url_paste) + self.url_entry.bind("", self._on_url_paste) + self.url_entry.bind("", self._show_url_context_menu) + self.url_entry.bind("", self._show_url_context_menu) + + # 繼續後面的 Task + self._build_filters_row() + self._build_treeview() + self._build_progress_section() + self._build_panels() + self._build_download_controls() + + # 初始化其他狀態 + self.sort_reverse = False + self.full_tree_backup = {} + self.drag_anchor_item = "" + + context_menu = tk.Menu(self.window, tearoff=0) + context_menu.add_command(label="Select All", command=self.select_all) + context_menu.add_command(label="Deselect All", command=self.deselect_all) + context_menu.add_separator() + context_menu.add_command(label="Expand All", command=self.expand_all) + context_menu.add_command(label="Collapse All", command=self.collapse_all) + self.context_menu = context_menu +``` + +並新增 URL paste 輔助方法(從 `ui_ttkb.py` 直接搬): + +```python +def _on_url_paste(self, _event=None): + self._paste_into_url_entry() + return "break" + +def _on_global_url_paste(self, _event=None): + try: + focused = self.window.focus_get() + except tk.TclError: + return None + if focused is self.url_entry._entry: # CTkEntry 內部 widget + self._paste_into_url_entry() + return "break" + return None + +def _paste_into_url_entry(self): + try: + text = self.window.clipboard_get() + except tk.TclError: + return + if not text: + return + self.url_entry.delete(0, "end") + self.url_entry.insert(0, text) + +def _show_url_context_menu(self, event): + try: + self.url_context_menu.tk_popup(event.x_root, event.y_root) + finally: + self.url_context_menu.grab_release() + +def _set_status(self, text: str, color: str = "#059669") -> None: + try: + self.status_label.configure(text=text, text_color=color) + except Exception: + pass +``` + +**Step 4: 執行測試** + +```bash +python -m pytest test_ui_ctk.py -v +``` + +Expected: 5 PASS(新的失敗的測試現在應通過) + +> 注意:`_build_filters_row` 等還未存在,需先加入空的 stub: +```python +def _build_filters_row(self): pass +def _build_treeview(self): pass +def _build_progress_section(self): pass +def _build_panels(self): pass +def _build_download_controls(self): pass +``` + +**Step 5: Commit** + +```bash +git add ui_ctk.py test_ui_ctk.py +git commit -m "feat(ui): 實作 header 列(URL、狀態、掃描按鈕)" +``` + +--- + +### Task 4: 篩選列(檔案類型 Checkboxes) + +**目的:** 動態產生 CTkCheckBox(依副檔名),支援水平捲動,Select All / Deselect All。 + +**Files:** +- Modify: `ui_ctk.py`(`_build_filters_row` + 相關方法) +- Modify: `test_ui_ctk.py` + +**Step 1: 新增失敗測試** + +```python +def test_has_filters_frame(self): + import ui_ctk + app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) + app.window.after(0, app.window.destroy) + app.run() + self.assertTrue(hasattr(app, "filters_container")) +``` + +**Step 2: 執行確認失敗** + +```bash +python -m pytest test_ui_ctk.py::TestWebsiteCopierCtkSmoke::test_has_filters_frame -v +``` + +**Step 3: 實作 _build_filters_row** + +```python +def _build_filters_row(self) -> None: + filters_and_controls = ctk.CTkFrame(self.window, fg_color="transparent") + filters_and_controls.grid(row=1, column=0, sticky="ew", padx=10, pady=(4, 0)) + filters_and_controls.grid_columnconfigure(0, weight=1) + + filters_frame = ctk.CTkFrame(filters_and_controls, fg_color="transparent") + filters_frame.grid(row=0, column=0, sticky="ew") + + # 可水平捲動的容器 + self.filters_container = ctk.CTkScrollableFrame( + filters_frame, height=85, orientation="horizontal", fg_color="transparent" + ) + self.filters_container.pack(fill="x") + + type_actions = ctk.CTkFrame(filters_frame, fg_color="transparent") + type_actions.pack(fill="x", pady=(4, 0)) + ctk.CTkButton( + type_actions, text="Select All Types", + fg_color=("gray80", "gray25"), + text_color=("gray10", "gray90"), + command=self.select_all_types, + width=140, + ).pack(side="left", padx=(0, 6)) + ctk.CTkButton( + type_actions, text="Deselect All Types", + fg_color=("gray80", "gray25"), + text_color=("gray10", "gray90"), + command=self.deselect_all_types, + width=150, + ).pack(side="left") +``` + +並新增 filter 方法 stub(從 `ui_ttkb.py` 搬): + +```python +def select_all_types(self): + for var in self.file_types.values(): + var.set(True) + +def deselect_all_types(self): + for var in self.file_types.values(): + var.set(False) + +def _add_file_type_filter(self, ext: str) -> None: + if ext in self.file_types: + return + var = tk.BooleanVar(value=True) + self.file_types[ext] = var + self.file_type_counts[ext] = 0 + cb = ctk.CTkCheckBox( + self.filters_container, + text=ext or "(no ext)", + variable=var, + onvalue=True, + offvalue=False, + ) + cb.pack(side="left", padx=4) + self.file_type_widgets[ext] = cb +``` + +**Step 4: 執行測試** + +```bash +python -m pytest test_ui_ctk.py -v +``` + +Expected: 全部 PASS + +**Step 5: Commit** + +```bash +git add ui_ctk.py test_ui_ctk.py +git commit -m "feat(ui): 實作檔案類型篩選列(CTkCheckBox + 捲動容器)" +``` + +--- + +### Task 5: 下載控制列 + +**目的:** Download Selected、Pause/Resume、Choose Folder、Threads 選單、Hide/Show Panels。 + +**Files:** +- Modify: `ui_ctk.py`(`_build_download_controls`) + +**Step 1: 新增失敗測試** + +```python +def test_has_download_controls(self): + import ui_ctk + app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) + app.window.after(0, app.window.destroy) + app.run() + self.assertTrue(hasattr(app, "download_btn")) + self.assertTrue(hasattr(app, "pause_btn")) + self.assertTrue(hasattr(app, "threads_var")) +``` + +**Step 2: 實作 _build_download_controls** + +```python +def _build_download_controls(self) -> None: + controls_frame = ctk.CTkFrame(self.window, fg_color="transparent") + # 放在 filters_and_controls 右側(與 Task 4 的 grid 同 row) + # 重新取得 filters_and_controls 參考,或在 Task 4 儲存 self._filters_and_controls + # 建議:在 Task 4 儲存 self._filters_ctrl_frame = filters_and_controls + controls_frame.grid(row=1, column=0, sticky="e", padx=10) + + self.download_btn = ctk.CTkButton( + controls_frame, text="Download Selected", command=self.download_selected + ) + self.download_btn.grid(row=0, column=0, padx=4) + + self.pause_btn = ctk.CTkButton( + controls_frame, text="Pause", + fg_color=("gray70", "gray30"), + command=self.toggle_pause, + state="disabled", + ) + self.pause_btn.grid(row=0, column=1, padx=4) + + self.path_btn = ctk.CTkButton( + controls_frame, text="Choose Folder", + fg_color=("gray70", "gray30"), + command=self.choose_download_path, + ) + self.path_btn.grid(row=0, column=2, padx=4) + + ctk.CTkLabel(controls_frame, text="Threads").grid(row=0, column=3, padx=(12, 4)) + self.threads_var = tk.StringVar(value="5") + self.threads_combo = ctk.CTkOptionMenu( + controls_frame, + values=[str(i) for i in range(1, 11)], + variable=self.threads_var, + command=self.update_thread_count, + width=70, + ) + self.threads_combo.grid(row=0, column=4) + + self.panels_visible = True + self.toggle_panels_btn = ctk.CTkButton( + controls_frame, text="Hide Panels", + fg_color=("gray80", "gray25"), + text_color=("gray10", "gray90"), + command=self.toggle_panels, + width=110, + ) + self.toggle_panels_btn.grid(row=0, column=5, padx=(10, 0)) +``` + +> **重要:** `_build_download_controls` 需要嵌入 `filters_and_controls` 這個 frame(同一個 grid row)。 +> 建議在 Task 4 修改,讓 `_build_filters_row` 存 `self._ctrl_row_frame = filters_and_controls`, +> 然後在 `_build_download_controls` 直接用 `self._ctrl_row_frame`。 + +新增 stub 方法: + +```python +def choose_download_path(self) -> None: + path = filedialog.askdirectory(title="Choose Download Location") + if path: + self.download_path = path + +def update_thread_count(self, value=None) -> None: + try: + n = int(self.threads_var.get()) + except (ValueError, AttributeError): + return + self.max_workers = max(1, min(10, n)) + +def toggle_panels(self) -> None: + if self.panels_visible: + if hasattr(self, "_panels_widget"): + self._panels_widget.grid_remove() + self.panels_visible = False + self.toggle_panels_btn.configure(text="Show Panels") + else: + if hasattr(self, "_panels_widget"): + self._panels_widget.grid() + self.panels_visible = True + self.toggle_panels_btn.configure(text="Hide Panels") +``` + +**Step 3: 執行測試** + +```bash +python -m pytest test_ui_ctk.py -v +``` + +**Step 4: Commit** + +```bash +git add ui_ctk.py test_ui_ctk.py +git commit -m "feat(ui): 實作下載控制列" +``` + +--- + +### Task 6: Treeview(掃描結果列表) + +**目的:** 建立 `ttk.Treeview`,含搜尋欄、欄位排序、行勾選、鍵盤與滑鼠互動、右鍵選單。 + +**Files:** +- Modify: `ui_ctk.py`(`_build_treeview` + 互動方法) +- Modify: `test_ui_ctk.py` + +**Step 1: 新增失敗測試** + +```python +def test_has_treeview(self): + import ui_ctk + app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) + app.window.after(0, app.window.destroy) + app.run() + self.assertTrue(hasattr(app, "tree")) + self.assertTrue(hasattr(app, "search_var")) +``` + +**Step 2: 實作 _build_treeview** + +```python +def _build_treeview(self) -> None: + tree_frame = ctk.CTkFrame(self.window, fg_color="transparent") + tree_frame.grid(row=2, column=0, sticky="nsew", padx=10, pady=(4, 0)) + tree_frame.grid_columnconfigure(0, weight=1) + tree_frame.grid_rowconfigure(1, weight=1) + + # 搜尋欄 + search_bar = ctk.CTkFrame(tree_frame, fg_color="transparent") + search_bar.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 4)) + search_bar.grid_columnconfigure(1, weight=1) + ctk.CTkLabel(search_bar, text="Search").grid(row=0, column=0, padx=(0, 6)) + self.search_var = tk.StringVar() + self.search_entry = ctk.CTkEntry(search_bar, textvariable=self.search_var) + self.search_entry.grid(row=0, column=1, sticky="ew") + self.search_var.trace_add("write", self.on_search_filter_changed) + + # Treeview + configure_treeview_style(self.window, ctk, ttk) + self.tree = ttk.Treeview( + tree_frame, + columns=("size", "type", "full_path"), + show="tree headings", + selectmode="extended", + ) + self.tree.heading("#0", text="Path", command=lambda: self.sort_tree("#0")) + self.tree.heading("size", text="Size", command=lambda: self.sort_tree("size")) + self.tree.heading("type", text="Type", command=lambda: self.sort_tree("type")) + self.tree.column("#0", width=600, stretch=True) + self.tree.column("size", width=120, stretch=False, anchor="e") + self.tree.column("type", width=240, stretch=False) + self.tree.column("full_path", width=0, stretch=False) + + tag_colors = treeview_tag_colors(self.window) + self.tree.tag_configure("checked", foreground=tag_colors["checked"]) + + style = ttk.Style() + style.configure("Treeview", font=("SF Pro Text", 14), rowheight=34) + style.configure("Treeview.Heading", font=("SF Pro Text", 13, "bold")) + + self.tree.grid(row=1, column=0, sticky="nsew") + + tree_scroll = tk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview) + self.tree.configure(yscrollcommand=tree_scroll.set) + tree_scroll.grid(row=1, column=1, sticky="ns") + + # 鍵盤與滑鼠綁定 + self.tree.bind("", self._on_tree_select_all) + self.tree.bind("", self._on_tree_select_all) + self.tree.bind("", self.on_tree_click) + self.tree.bind("", self.show_context_menu) + self.tree.bind("", self.show_context_menu) + self.tree.bind("", self.show_context_menu) + self.tree.bind("", self.on_tree_drag_select) + self.tree.bind("", self.on_tree_space) + self.tree.bind("", self.on_tree_enter) +``` + +**Step 3: 搬移 Treeview 互動方法(從 ui_ttkb.py)** + +以下方法直接從 `ui_ttkb.py` 搬移,邏輯不變: + +- `on_tree_click(event)` — 點擊切換 ✔ 勾選 +- `on_tree_drag_select(event)` — 拖曳多選 +- `on_tree_space(event)` — Space 切換勾選 +- `on_tree_enter(event)` — Enter 確認 +- `_on_tree_select_all(event)` — Cmd/Ctrl+A +- `show_context_menu(event)` — 右鍵選單 +- `select_all()` / `deselect_all()` — 全選/取消全選 +- `expand_all()` / `collapse_all()` — 展開/收合 +- `sort_tree(col)` — 欄位排序 + +在 `ui_ttkb.py` 中找到這些方法並直接複製(它們純粹操作 ttk.Treeview,與框架無關)。 + +**Step 4: 執行測試** + +```bash +python -m pytest test_ui_ctk.py -v +``` + +**Step 5: Commit** + +```bash +git add ui_ctk.py test_ui_ctk.py +git commit -m "feat(ui): 實作 Treeview 與互動邏輯" +``` + +--- + +### Task 7: 進度條區塊 + +**目的:** 整體下載進度條 + 進度文字。 + +**Files:** +- Modify: `ui_ctk.py`(`_build_progress_section`) + +**Step 1: 新增失敗測試** + +```python +def test_has_progress_bar(self): + import ui_ctk + app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) + app.window.after(0, app.window.destroy) + app.run() + self.assertTrue(hasattr(app, "progress_bar")) + self.assertTrue(hasattr(app, "progress_label")) +``` + +**Step 2: 實作** + +```python +def _build_progress_section(self) -> None: + progress_frame = ctk.CTkFrame(self.window, fg_color="transparent") + progress_frame.grid(row=3, column=0, sticky="ew", padx=10, pady=(4, 0)) + progress_frame.grid_columnconfigure(0, weight=1) + + self.progress_var = tk.DoubleVar(value=0) + self.progress_bar = ctk.CTkProgressBar(progress_frame) + self.progress_bar.set(0) + self.progress_bar.grid(row=0, column=0, sticky="ew") + + self.progress_label = ctk.CTkLabel(progress_frame, text="") + self.progress_label.grid(row=1, column=0, sticky="w") +``` + +**Step 3: 執行測試** + +```bash +python -m pytest test_ui_ctk.py -v +``` + +**Step 4: Commit** + +```bash +git add ui_ctk.py test_ui_ctk.py +git commit -m "feat(ui): 實作進度條區塊" +``` + +--- + +### Task 8: Tabs 面板(Downloads + Logs) + +**目的:** `ctk.CTkTabview` 包含 Downloads tab(DownloadsPanel)和 Logs tab(CTkTextbox)。 + +**Files:** +- Modify: `ui_ctk.py`(`_build_panels`) +- Modify: `test_ui_ctk.py` + +**Step 1: 新增失敗測試** + +```python +def test_has_panels_notebook(self): + import ui_ctk + app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) + app.window.after(0, app.window.destroy) + app.run() + self.assertTrue(hasattr(app, "panels_notebook")) + self.assertTrue(hasattr(app, "log_text")) + self.assertTrue(hasattr(app, "downloads_panel")) +``` + +**Step 2: 實作 _build_panels** + +```python +def _build_panels(self) -> None: + self.panels_notebook = ctk.CTkTabview(self.window, height=180) + self.panels_notebook.grid(row=4, column=0, sticky="ew", padx=10, pady=(4, 10)) + self._panels_widget = self.panels_notebook # toggle_panels 用 + + downloads_tab = self.panels_notebook.add("Downloads") + logs_tab = self.panels_notebook.add("Logs") + self.panels_notebook.set("Logs") # 預設顯示 Logs + + # Downloads tab + downloads_scroll = ctk.CTkScrollableFrame(downloads_tab, height=120) + downloads_scroll.pack(fill="both", expand=True) + self.downloads_panel = DownloadsPanel( + parent_frame=downloads_scroll, + ctk=ctk, + tk=tk, + tokens=self.ui_tokens, + ) + + # Logs tab + self.log_text = ctk.CTkTextbox(logs_tab, height=120, wrap="word") + self.log_text.pack(fill="both", expand=True) +``` + +**Step 3: 更新 log_message 方法** + +`ctk.CTkTextbox` 的 API 與 `tk.Text` 略不同: + +```python +def log_message(self, message: str) -> None: + try: + self.log_text.configure(state="normal") + self.log_text.insert("end", message + "\n") + self.log_text.see("end") + except Exception: + pass +``` + +**Step 4: 執行測試** + +```bash +python -m pytest test_ui_ctk.py -v +``` + +**Step 5: Commit** + +```bash +git add ui_ctk.py test_ui_ctk.py +git commit -m "feat(ui): 實作 CTkTabview(Downloads + Logs)" +``` + +--- + +### Task 9: Queue 輪詢 + 執行緒安全 + +**目的:** 確保 `after()` 輪詢 Queue 的機制完整移植,包含 scan buffer throttle。 + +**Files:** +- Modify: `ui_ctk.py` + +**Step 1: 從 ui_ttkb.py 搬移以下方法(邏輯不變)** + +在 `WebsiteCopierCtk.__init__` 末尾啟動輪詢: + +```python +# 在 _build_ui() 之後加入 +if not self._ui_smoke: + self.window.after(100, self._poll_scan_queue) + self.window.after(100, self._poll_file_queue) +``` + +從 `ui_ttkb.py` 直接搬移以下方法: +- `_poll_scan_queue()` +- `_poll_file_queue()` +- `_flush_scan_buffer()` +- `_schedule_flush()` + +這些方法只用到 `self.scan_item_buffer`、`self.dir_queue`、`self.file_queue` 與 Treeview 操作,與框架無關,可直接複製。 + +**Step 2: 新增測試** + +```python +def test_poll_methods_exist(self): + import ui_ctk + app = ui_ctk.WebsiteCopierCtk(ui_smoke=True) + self.assertTrue(hasattr(app, "_poll_scan_queue")) + self.assertTrue(hasattr(app, "_flush_scan_buffer")) + app.window.after(0, app.window.destroy) + app.run() +``` + +**Step 3: 執行測試** + +```bash +python -m pytest test_ui_ctk.py -v +``` + +**Step 4: Commit** + +```bash +git add ui_ctk.py test_ui_ctk.py +git commit -m "feat(ui): 移植 Queue 輪詢與 scan buffer throttle" +``` + +--- + +### Task 10: Backend 橋接方法(掃描、下載、樹狀填充) + +**目的:** 移植所有與 backend 互動的方法,確保掃描與下載功能正常。 + +**Files:** +- Modify: `ui_ctk.py` + +**Step 1: 從 ui_ttkb.py 搬移以下方法(邏輯不變)** + +掃描相關: +- `start_scan()` +- `toggle_scan_pause()` +- `clear_scan_results()` +- `add_folder(folder_url, parent_id)` +- `add_file(file_url, size, file_type, parent_id)` +- `update_progress(scanned, total)` + +下載相關: +- `download_selected()` +- `toggle_pause()` +- `_download_file(url, file_path, cancel_event)` +- `_update_download_progress(file_path, progress)` +- `_set_download_status(file_path, text)` + +搜尋相關: +- `on_search_filter_changed(*args)` +- `_apply_search_filter(query)` +- `focus_search(event=None)` +- `focus_logs(event=None)` +- `clear_search(event=None)` + +**Step 2: 調整 CTkProgressBar 差異** + +`ctk.CTkProgressBar` 的 API 與 ttkb.Progressbar 不同: + +```python +# ttkb 版本(舊) +self.progress_var.set(value) + +# CTk 版本(新) +self.progress_bar.set(value / 100) # CTkProgressBar 接受 0.0–1.0 +``` + +搜尋 `ui_ttkb.py` 中所有 `progress_var.set` 的地方,改為 `self.progress_bar.set(value / 100)`。 + +**Step 3: 新增測試** + +```python +def test_scan_methods_exist(self): + import ui_ctk + for method in ("start_scan", "toggle_scan_pause", "clear_scan_results", + "download_selected", "toggle_pause"): + self.assertTrue(hasattr(ui_ctk.WebsiteCopierCtk, method), f"Missing: {method}") +``` + +**Step 4: 執行測試** + +```bash +python -m pytest test_ui_ctk.py -v +``` + +**Step 5: Commit** + +```bash +git add ui_ctk.py test_ui_ctk.py +git commit -m "feat(ui): 移植 backend 橋接方法(掃描、下載、搜尋)" +``` + +--- + +### Task 11: 更新入口 index_ripper.py + +**目的:** 讓 `index_ripper.py` 預設啟動 CTk 新版本(`ui_ctk.py`)。 + +**Files:** +- Modify: `index_ripper.py`(只改主入口的 import,舊 `WebsiteCopier` 類別保留) + +**Step 1: 確認現有入口結構** + +讀 `index_ripper.py` 最後的 `main()` / `if __name__ == "__main__"` 部分。 + +**Step 2: 修改入口** + +在 `index_ripper.py` 底部找到啟動 `WebsiteCopier` 的地方,改為: + +```python +def main() -> int: + if "--ui-smoke" in sys.argv: + from ui_ctk import WebsiteCopierCtk + app = WebsiteCopierCtk(ui_smoke=True) + app.run() + print("UI_SMOKE_OK") + return 0 + + from ui_ctk import WebsiteCopierCtk + app = WebsiteCopierCtk() + app.run() + return 0 +``` + +**Step 3: 執行 smoke 測試** + +```bash +python index_ripper.py --smoke +``` + +Expected: `SMOKE_OK` + +**Step 4: Commit** + +```bash +git add index_ripper.py +git commit -m "feat: 更新入口,預設啟動 CTk 版 UI" +``` + +--- + +### Task 12: 整合驗證與清理 + +**目的:** 完整執行所有測試,確認功能正常,移除不必要的 ttkbootstrap import。 + +**Files:** +- Modify: `ui_ctk.py`(若有殘留的 ttkb import) +- Modify: `requirements.txt`(可選:將 ttkbootstrap 移至 optional) + +**Step 1: 執行完整測試套件** + +```bash +python -m pytest test_ui_ctk.py test_ui_theme.py test_ui_downloads.py test_backend.py -v +``` + +Expected: 全部 PASS + +**Step 2: 手動 UI smoke** + +```bash +python index_ripper.py --ui-smoke +``` + +Expected: 視窗出現並正常關閉,輸出 `UI_SMOKE_OK` + +**Step 3: 確認 ttkbootstrap 仍在 requirements.txt(保留舊入口用)** + +`index_ripper_ttkb.py` 仍可用 ttkb,不移除 dependency,只確認不影響新版。 + +**Step 4: 最終 commit** + +```bash +git add . +git commit -m "test: 整合驗證通過,CTk 遷移完成" +``` + +--- + +## 完整功能對照檢查 + +實作完畢後,逐一對照設計文件的功能清單: + +``` +docs/plans/2026-03-09-ctk-migration-design.md +``` + +每個 `[ ]` 項目都應手動驗證可運作。 + +--- + +## 已知限制 + +- `ctk.CTkEntry` 的 `_entry` 屬性是內部 API,`_on_global_url_paste` 中需改用 `focused is self.url_entry` 的比較方式(CTk 5.x 的 entry focus 行為與 tk.Entry 略有不同,需測試後調整) +- `ctk.CTkOptionMenu` 不支援 `readonly` state,threads 輸入值需在 command callback 中做範圍驗證 +- `ttk.Treeview` 的 style 在每次 `configure_treeview_style()` 呼叫時會重設 `theme_use("default")`,確保只在 `_build_treeview()` 呼叫一次 From 25abd3319d18f0902bf92a6c3a8985e8a49c5aae Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 19:03:46 +0800 Subject: [PATCH 04/55] =?UTF-8?q?chore:=20=E5=B0=87=20.worktrees/=20?= =?UTF-8?q?=E5=8A=A0=E5=85=A5=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 35e0c25..0f7bf01 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ __pycache__/ *.egg-info/ .sisyphus/ +.worktrees/ From 42b54630ab901ab4c45f60ca73166c9556545e76 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 19:07:37 +0800 Subject: [PATCH 05/55] =?UTF-8?q?refactor(theme):=20=E7=A7=BB=E9=99=A4=20t?= =?UTF-8?q?tkbootstrap=20bridge=EF=BC=8C=E7=B5=B1=E4=B8=80=E7=94=A8=20CTk?= =?UTF-8?q?=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test_ui_theme.py | 32 +++++++++++++ ui_theme.py | 120 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 test_ui_theme.py diff --git a/test_ui_theme.py b/test_ui_theme.py new file mode 100644 index 0000000..e5adf75 --- /dev/null +++ b/test_ui_theme.py @@ -0,0 +1,32 @@ +import unittest + +import ui_theme + + +class TestUITheme(unittest.TestCase): + def test_action_button_style_name(self): + self.assertEqual(ui_theme.action_button_style_name("primary"), "Primary.TButton") + self.assertEqual( + ui_theme.action_button_style_name("secondary"), "Secondary.TButton" + ) + self.assertEqual(ui_theme.action_button_style_name("danger"), "Danger.TButton") + self.assertEqual( + ui_theme.action_button_style_name("success"), "Success.TButton" + ) + self.assertEqual(ui_theme.action_button_style_name("unknown"), "Secondary.TButton") + + def test_ui_tokens_contains_modern_widget_styles(self): + tokens = ui_theme.ui_tokens() + self.assertIn("option_menu", tokens) + self.assertIn("tabview", tokens) + self.assertIn("checkbox", tokens) + self.assertIn("fg_color", tokens["option_menu"]) + self.assertIn("segmented_button_fg_color", tokens["tabview"]) + self.assertIn("border_color", tokens["checkbox"]) + + def test_apply_bootstrap_theme_not_present(self): + self.assertFalse(hasattr(ui_theme, "apply_bootstrap_theme")) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui_theme.py b/ui_theme.py index 11cc825..87ad1e5 100644 --- a/ui_theme.py +++ b/ui_theme.py @@ -6,6 +6,109 @@ def apply_app_theme(ctk): ctk.set_default_color_theme("green") +def ui_tokens(): + return { + "option_menu": { + "height": 34, + "fg_color": ("#E2E8F0", "#334155"), + "button_color": ("#CBD5E1", "#475569"), + "button_hover_color": ("#94A3B8", "#64748B"), + "text_color": ("#0F172A", "#F8FAFC"), + "dropdown_fg_color": ("#FFFFFF", "#1E293B"), + "dropdown_text_color": ("#0F172A", "#F1F5F9"), + "dropdown_hover_color": ("#E2E8F0", "#334155"), + }, + "tabview": { + "segmented_button_fg_color": ("#E2E8F0", "#334155"), + "segmented_button_selected_color": ("#2563EB", "#3B82F6"), + "segmented_button_selected_hover_color": ("#1D4ED8", "#2563EB"), + "segmented_button_unselected_color": ("#CBD5E1", "#475569"), + "segmented_button_unselected_hover_color": ("#94A3B8", "#64748B"), + "text_color": ("#0F172A", "#F8FAFC"), + }, + "checkbox": { + "checkbox_width": 20, + "checkbox_height": 20, + "corner_radius": 6, + "border_width": 2, + "border_color": ("#94A3B8", "#64748B"), + "fg_color": ("#2563EB", "#3B82F6"), + "hover_color": ("#1D4ED8", "#2563EB"), + "checkmark_color": ("#FFFFFF", "#FFFFFF"), + "text_color": ("#0F172A", "#E2E8F0"), + }, + "logs": { + "height": 100, + "font": ("SF Mono", 12), + "text_color": ("#0F172A", "#E2E8F0"), + "fg_color": ("#F8FAFC", "#111827"), + }, + "downloads": { + "row_fg_color": ("#FFFFFF", "#1F2937"), + "row_border_width": 1, + "row_corner_radius": 10, + "name_text_color": ("#0F172A", "#E2E8F0"), + "status_colors": { + "queued": ("#475569", "#94A3B8"), + "active": ("#1D4ED8", "#60A5FA"), + "warning": ("#B45309", "#F59E0B"), + "success": ("#047857", "#34D399"), + "error": ("#B91C1C", "#F87171"), + }, + "cancel_button": { + "width": 88, + "height": 30, + "fg_color": "#DC2626", + "hover_color": "#B91C1C", + }, + }, + } + + +def action_button_style_name(kind: str) -> str: + mapping = { + "primary": "Primary.TButton", + "secondary": "Secondary.TButton", + "danger": "Danger.TButton", + "success": "Success.TButton", + } + return mapping.get(kind, "Secondary.TButton") + + +def configure_action_button_styles(window, ctk, ttk): + style = ttk.Style() + text_light = "#F8FAFC" + fg = { + "primary": window._apply_appearance_mode(("#2563EB", "#3B82F6")), + "secondary": window._apply_appearance_mode(("#334155", "#475569")), + "danger": window._apply_appearance_mode(("#DC2626", "#EF4444")), + "success": window._apply_appearance_mode(("#059669", "#10B981")), + } + active = { + "primary": window._apply_appearance_mode(("#1D4ED8", "#2563EB")), + "secondary": window._apply_appearance_mode(("#1E293B", "#334155")), + "danger": window._apply_appearance_mode(("#B91C1C", "#DC2626")), + "success": window._apply_appearance_mode(("#047857", "#059669")), + } + for kind in ("primary", "secondary", "danger", "success"): + style_name = action_button_style_name(kind) + style.configure( + style_name, + padding=(12, 7), + foreground=text_light, + background=fg[kind], + borderwidth=0, + focusthickness=1, + focuscolor=active[kind], + font=("SF Pro Text", 11, "bold"), + ) + style.map( + style_name, + background=[("active", active[kind]), ("disabled", "#6B7280")], + foreground=[("disabled", "#E5E7EB")], + ) + + def configure_treeview_style(window, ctk, ttk): style = ttk.Style() bg_color = window._apply_appearance_mode(ctk.ThemeManager.theme["CTkFrame"]["fg_color"]) @@ -23,9 +126,14 @@ def configure_treeview_style(window, ctk, ttk): foreground=text_color, fieldbackground=bg_color, borderwidth=0, - rowheight=22, + rowheight=28, + font=("SF Pro Text", 12), + ) + style.map( + "Treeview", + background=[("selected", selected_color)], + foreground=[("selected", "#FFFFFF")], ) - style.map("Treeview", background=[("selected", selected_color)]) style.configure( "Treeview.Heading", background=window._apply_appearance_mode( @@ -33,7 +141,7 @@ def configure_treeview_style(window, ctk, ttk): ), foreground=text_color, relief="flat", - font=("TkDefaultFont", 10, "bold"), + font=("SF Pro Text", 11, "bold"), ) style.map( "Treeview.Heading", @@ -50,7 +158,7 @@ def configure_treeview_style(window, ctk, ttk): def treeview_tag_colors(window): return { - "oddrow": window._apply_appearance_mode(("#F8FAFC", "#2B2B2B")), - "evenrow": window._apply_appearance_mode(("#F1F5F9", "#2E2E2E")), - "checked": window._apply_appearance_mode(("#166534", "#50C878")), + "oddrow": window._apply_appearance_mode(("#FFFFFF", "#2A2D35")), + "evenrow": window._apply_appearance_mode(("#F8FAFC", "#242831")), + "checked": window._apply_appearance_mode(("#0F766E", "#2DD4BF")), } From 8c7ec039edfb845a4633da9f076dd9de8c4e299f Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 19:18:47 +0800 Subject: [PATCH 06/55] =?UTF-8?q?feat(ui):=20=E6=96=B0=E5=A2=9E=20WebsiteC?= =?UTF-8?q?opierCtk=20=E9=AA=A8=E6=9E=B6=E8=88=87=20smoke=20=E6=B8=AC?= =?UTF-8?q?=E8=A9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 建立 ui_ctk.py 骨架(__init__ 狀態初始化、smoke/full UI 分支、run/on_closing/log_message/notify_* 方法、stub 方法), 並新增 test_ui_ctk.py smoke 測試(含 TCL_LIBRARY 自動修正)共 4 個測試全部通過。 Co-Authored-By: Claude Sonnet 4.6 --- test_ui_ctk.py | 47 ++++++++++++ ui_ctk.py | 201 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 test_ui_ctk.py create mode 100644 ui_ctk.py diff --git a/test_ui_ctk.py b/test_ui_ctk.py new file mode 100644 index 0000000..90bc115 --- /dev/null +++ b/test_ui_ctk.py @@ -0,0 +1,47 @@ +"""Smoke tests for ui_ctk.WebsiteCopierCtk.""" +import os +import sys +import unittest + +os.environ.setdefault("INDEX_RIPPER_MODAL_DIALOGS", "0") +# 無頭環境 +os.environ.setdefault("DISPLAY", ":99") + +# Ensure Tcl/Tk can find its library when running under uv's isolated Python. +# TCL_LIBRARY must be set before _tkinter is first imported. +import sys as _sys +_tcl_dir = os.path.join(_sys.base_prefix, "lib", "tcl8.6") +if os.path.isdir(_tcl_dir) and "TCL_LIBRARY" not in os.environ: + os.environ["TCL_LIBRARY"] = _tcl_dir + + +class TestWebsiteCopierCtkSmoke(unittest.TestCase): + def _make_smoke(self): + import importlib + mod = importlib.import_module("ui_ctk") + importlib.reload(mod) + return mod.WebsiteCopierCtk(ui_smoke=True) + + def test_import(self): + import ui_ctk # noqa: F401 + + def test_smoke_init_and_destroy(self): + app = self._make_smoke() + app.window.after(0, app.window.destroy) + app.run() + + def test_has_url_var(self): + app = self._make_smoke() + self.assertTrue(hasattr(app, "url_var")) + app.window.after(0, app.window.destroy) + app.run() + + def test_has_log_text(self): + app = self._make_smoke() + self.assertTrue(hasattr(app, "log_text")) + app.window.after(0, app.window.destroy) + app.run() + + +if __name__ == "__main__": + unittest.main() diff --git a/ui_ctk.py b/ui_ctk.py new file mode 100644 index 0000000..c6bcbee --- /dev/null +++ b/ui_ctk.py @@ -0,0 +1,201 @@ +from __future__ import annotations + +import os +import threading +from concurrent.futures import ThreadPoolExecutor +from queue import Empty, Queue + +import tkinter as tk +from tkinter import filedialog, messagebox, ttk + +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +import customtkinter as ctk + +from app_utils import default_download_folder, normalize_extension, safe_join, sanitize_filename +from backend import Backend +from ui_downloads import DownloadsPanel +from ui_theme import ( + apply_app_theme, + configure_action_button_styles, + ui_tokens, +) + + +def should_skip_file_row(existing_entry) -> bool: + """Return True when a file row is already fully registered.""" + return existing_entry is not None + + +class WebsiteCopierCtk: + USER_AGENT = "IndexRipper/2.0" + + def __init__(self, ui_smoke: bool = False): + self._ui_smoke = bool(ui_smoke) + self.debug_enabled = os.environ.get("INDEX_RIPPER_DEBUG", "0") != "0" + self.use_modal_dialogs = os.environ.get("INDEX_RIPPER_MODAL_DIALOGS", "0") == "1" + + apply_app_theme(ctk) + + self.window = ctk.CTk() + self.window.title("Index Ripper") + self.window.geometry("1200x900") + self.window.minsize(900, 650) + + configure_action_button_styles(self.window, ctk, ttk) + + self.ui_tokens = ui_tokens() + + self.backend = Backend(self) + + self.pause_event = threading.Event() + self.pause_event.set() + self.scan_pause_event = threading.Event() + self.scan_pause_event.set() + + self.is_scanning = False + self.scanned_urls = 0 + self.total_urls = 0 + + self.files_dict_lock = threading.Lock() + self.folders_dict_lock = threading.Lock() + self.files_dict: dict[str, dict] = {} + self.folders: dict[str, str] = {} + self.checked_items: set[str] = set() + self.checkbox_checked = "✔ " + + self.dir_queue = Queue() + self.file_queue = Queue() + self.scan_item_buffer = Queue() + self.is_processing_dirs = False + self.is_processing_files = False + self.scan_flush_interval_ms = 16 + self.scan_flush_batch_size = 200 + self.scan_flush_job = None + self._last_logged_queue_size = None + + self.file_types: dict[str, tk.BooleanVar] = {} + self.file_type_counts: dict[str, int] = {} + self.file_type_widgets: dict = {} + + self.download_path = "" + self.download_queue = Queue() + self.max_workers = 5 + self.executor = ThreadPoolExecutor(max_workers=self.max_workers) + self.active_downloads = [] + + retry_strategy = Retry( + total=3, + backoff_factor=0.1, + status_forcelist=[500, 502, 503, 504], + ) + self.session = requests.Session() + adapter = HTTPAdapter(max_retries=retry_strategy) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) + self.timeout = (10, 20) + + self._build_ui() + + self.window.protocol("WM_DELETE_WINDOW", self.on_closing) + if not self._ui_smoke: + self.window.bind("", self.focus_search) + self.window.bind("", self.focus_logs) + self.window.bind("", self.clear_search) + self.window.bind_all("", self._on_global_url_paste, add="+") + self.window.bind_all("", self._on_global_url_paste, add="+") + self.window.bind_all("", self._on_global_url_paste, add="+") + self.window.bind_all("", self._on_global_url_paste, add="+") + self.window.bind_all("<>", self._on_global_url_paste, add="+") + + def _build_ui(self) -> None: + if self._ui_smoke: + self._build_ui_smoke_only() + else: + self._build_full_ui() + + def _build_ui_smoke_only(self) -> None: + self.url_var = tk.StringVar(value="https://example.com/") + self.search_var = tk.StringVar() + self.log_text = ctk.CTkTextbox(self.window, height=80) + self.log_text.pack(fill="both", expand=True, padx=10, pady=10) + self.log_text.insert("end", "UI smoke minimal view initialized.\n") + self.panels_notebook = None + self.logs_tab = None + + def _build_full_ui(self) -> None: + pass + + def run(self) -> None: + self.window.mainloop() + + def on_closing(self) -> None: + self.executor.shutdown(wait=False) + self.window.destroy() + + def log_message(self, message: str) -> None: + try: + self.log_text.configure(state="normal") + self.log_text.insert("end", message + "\n") + self.log_text.see("end") + except Exception: + pass + + def _debug(self, message: str) -> None: + if self.debug_enabled: + print(f"[DEBUG] {message}") + try: + self.log_text.configure(state="normal") + self.log_text.insert("end", f"[DEBUG] {message}\n") + self.log_text.see("end") + except Exception: + pass + + def notify_info(self, title: str, message: str) -> None: + if self.use_modal_dialogs: + messagebox.showinfo(title, message) + else: + self.log_message(f"[INFO] {title}: {message}") + + def notify_warning(self, title: str, message: str) -> None: + if self.use_modal_dialogs: + messagebox.showwarning(title, message) + else: + self.log_message(f"[WARNING] {title}: {message}") + + def notify_error(self, title: str, message: str) -> None: + if self.use_modal_dialogs: + messagebox.showerror(title, message) + else: + self.log_message(f"[ERROR] {title}: {message}") + + # --- Stub methods (implemented in later Tasks) --- + + def _build_filters_row(self) -> None: + pass + + def _build_treeview(self) -> None: + pass + + def _build_progress_section(self) -> None: + pass + + def _build_panels(self) -> None: + pass + + def _build_download_controls(self) -> None: + pass + + def focus_search(self, event=None) -> None: + pass + + def focus_logs(self, event=None) -> None: + pass + + def clear_search(self, event=None) -> None: + pass + + def _on_global_url_paste(self, event=None) -> None: + pass From 0735eec4c693d2304de9d5955f83662fc077f366 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 19:21:26 +0800 Subject: [PATCH 07/55] =?UTF-8?q?fix(ui):=20=E4=BF=AE=E6=AD=A3=20log=5Fmes?= =?UTF-8?q?sage=20state=E3=80=81test=20=E9=A0=86=E5=BA=8F=E3=80=81?= =?UTF-8?q?=E8=A3=9C=E9=BD=8A=20ui=5Ftheme=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test_ui_ctk.py | 4 ++-- ui_ctk.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test_ui_ctk.py b/test_ui_ctk.py index 90bc115..7abd15e 100644 --- a/test_ui_ctk.py +++ b/test_ui_ctk.py @@ -32,15 +32,15 @@ def test_smoke_init_and_destroy(self): def test_has_url_var(self): app = self._make_smoke() - self.assertTrue(hasattr(app, "url_var")) app.window.after(0, app.window.destroy) app.run() + self.assertTrue(hasattr(app, "url_var")) def test_has_log_text(self): app = self._make_smoke() - self.assertTrue(hasattr(app, "log_text")) app.window.after(0, app.window.destroy) app.run() + self.assertTrue(hasattr(app, "log_text")) if __name__ == "__main__": diff --git a/ui_ctk.py b/ui_ctk.py index c6bcbee..8f09983 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -20,6 +20,8 @@ from ui_theme import ( apply_app_theme, configure_action_button_styles, + configure_treeview_style, + treeview_tag_colors, ui_tokens, ) From ec68adb8198cda2e7c3401ed88d42681ab590d51 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 19:32:57 +0800 Subject: [PATCH 08/55] =?UTF-8?q?feat(ui):=20=E5=AF=A6=E4=BD=9C=20header?= =?UTF-8?q?=20=E5=88=97=EF=BC=88URL=E3=80=81=E7=8B=80=E6=85=8B=E3=80=81?= =?UTF-8?q?=E6=8E=83=E6=8F=8F=E6=8C=89=E9=88=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test_ui_ctk.py | 10 ++++ ui_ctk.py | 121 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 128 insertions(+), 3 deletions(-) diff --git a/test_ui_ctk.py b/test_ui_ctk.py index 7abd15e..5bbbf22 100644 --- a/test_ui_ctk.py +++ b/test_ui_ctk.py @@ -42,6 +42,16 @@ def test_has_log_text(self): app.run() self.assertTrue(hasattr(app, "log_text")) + def test_full_ui_has_scan_btn(self): + import ui_ctk, importlib + importlib.reload(ui_ctk) + app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) + app.window.after(0, app.window.destroy) + app.run() + self.assertTrue(hasattr(app, "scan_btn")) + self.assertTrue(hasattr(app, "url_entry")) + self.assertTrue(hasattr(app, "status_label")) + if __name__ == "__main__": unittest.main() diff --git a/ui_ctk.py b/ui_ctk.py index 8f09983..c9ceb37 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -128,7 +128,77 @@ def _build_ui_smoke_only(self) -> None: self.logs_tab = None def _build_full_ui(self) -> None: - pass + self.window.grid_columnconfigure(0, weight=1) + self.window.grid_rowconfigure(2, weight=1) + + self._build_header() + self._build_filters_row() + self._build_treeview() + self._build_progress_section() + self._build_panels() + self._build_download_controls() + + self.sort_reverse = False + self.full_tree_backup = {} + self.drag_anchor_item = "" + + self.context_menu = tk.Menu(self.window, tearoff=0) + self.context_menu.add_command(label="Select All", command=self.select_all) + self.context_menu.add_command(label="Deselect All", command=self.deselect_all) + self.context_menu.add_separator() + self.context_menu.add_command(label="Expand All", command=self.expand_all) + self.context_menu.add_command(label="Collapse All", command=self.collapse_all) + + def _build_header(self) -> None: + header = ctk.CTkFrame(self.window, fg_color="transparent") + header.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 0)) + header.grid_columnconfigure(1, weight=1) + + ctk.CTkLabel(header, text="URL", font=ctk.CTkFont(weight="bold")).grid( + row=0, column=0, sticky="w", padx=(0, 8) + ) + + self.url_var = tk.StringVar() + self.url_entry = ctk.CTkEntry(header, textvariable=self.url_var, placeholder_text="https://") + self.url_entry.grid(row=0, column=1, sticky="ew", padx=(0, 8)) + + self.status_label = ctk.CTkLabel(header, text="Ready", text_color="#059669") + self.status_label.grid(row=0, column=2, sticky="e", padx=(0, 8)) + + actions = ctk.CTkFrame(header, fg_color="transparent") + actions.grid(row=0, column=3, sticky="e") + + self.scan_btn = ctk.CTkButton(actions, text="Scan", command=self.start_scan) + self.scan_btn.pack(side="left", padx=3) + + self.scan_pause_btn = ctk.CTkButton( + actions, text="Pause Scan", + fg_color=("gray70", "gray30"), + hover_color=("gray60", "gray40"), + command=self.toggle_scan_pause, + state="disabled", + ) + self.scan_pause_btn.pack(side="left", padx=3) + + self.clear_scan_btn = ctk.CTkButton( + actions, text="Clear", + fg_color=("gray70", "gray30"), + hover_color=("gray60", "gray40"), + command=self.clear_scan_results, + ) + self.clear_scan_btn.pack(side="left", padx=3) + + # URL context menu + self.url_context_menu = tk.Menu(self.window, tearoff=0) + self.url_context_menu.add_command(label="Paste", command=self._paste_into_url_entry) + + self.url_entry.bind("", self._on_url_paste) + self.url_entry.bind("", self._on_url_paste) + self.url_entry.bind("", self._on_url_paste) + self.url_entry.bind("", self._on_url_paste) + self.url_entry.bind("", self._on_url_paste) + self.url_entry.bind("", self._show_url_context_menu) + self.url_entry.bind("", self._show_url_context_menu) def run(self) -> None: self.window.mainloop() @@ -199,5 +269,50 @@ def focus_logs(self, event=None) -> None: def clear_search(self, event=None) -> None: pass - def _on_global_url_paste(self, event=None) -> None: - pass + def _on_url_paste(self, _event=None): + self._paste_into_url_entry() + return "break" + + def _on_global_url_paste(self, _event=None): + try: + focused = self.window.focus_get() + except tk.TclError: + return None + # CTkEntry 的實際 tk.Entry widget 在 self.url_entry._entry + try: + if focused is self.url_entry._entry: + self._paste_into_url_entry() + return "break" + except AttributeError: + pass + return None + + def _paste_into_url_entry(self): + try: + text = self.window.clipboard_get() + except tk.TclError: + return + if not text: + return + self.url_entry.delete(0, "end") + self.url_entry.insert(0, text) + + def _show_url_context_menu(self, event): + try: + self.url_context_menu.tk_popup(event.x_root, event.y_root) + finally: + self.url_context_menu.grab_release() + + def _set_status(self, text: str, color: str = "#059669") -> None: + try: + self.status_label.configure(text=text, text_color=color) + except Exception: + pass + + def select_all(self): pass + def deselect_all(self): pass + def expand_all(self): pass + def collapse_all(self): pass + def start_scan(self): pass + def toggle_scan_pause(self): pass + def clear_scan_results(self): pass From 67a9eb50fc50d1e19d2ec3bbaffb959c4fb5a035 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 19:34:33 +0800 Subject: [PATCH 09/55] =?UTF-8?q?fix(ui):=20=5Fon=5Fglobal=5Furl=5Fpaste?= =?UTF-8?q?=20=E5=8A=A0=E5=85=A5=20hasattr=20=E5=89=8D=E7=BD=AE=E9=98=B2?= =?UTF-8?q?=E8=AD=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui_ctk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui_ctk.py b/ui_ctk.py index c9ceb37..1eed3b0 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -274,11 +274,12 @@ def _on_url_paste(self, _event=None): return "break" def _on_global_url_paste(self, _event=None): + if not hasattr(self, "url_entry"): + return None try: focused = self.window.focus_get() except tk.TclError: return None - # CTkEntry 的實際 tk.Entry widget 在 self.url_entry._entry try: if focused is self.url_entry._entry: self._paste_into_url_entry() From c207989777b2cd2a4ba7d27f08ebda628c0d1ba3 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 19:40:58 +0800 Subject: [PATCH 10/55] =?UTF-8?q?feat(ui):=20=E5=AF=A6=E4=BD=9C=E6=AA=94?= =?UTF-8?q?=E6=A1=88=E9=A1=9E=E5=9E=8B=E7=AF=A9=E9=81=B8=E5=88=97=EF=BC=88?= =?UTF-8?q?CTkScrollableFrame=20+=20CTkCheckBox=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test_ui_ctk.py | 19 +++++++++++++++ ui_ctk.py | 64 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/test_ui_ctk.py b/test_ui_ctk.py index 5bbbf22..b21fa06 100644 --- a/test_ui_ctk.py +++ b/test_ui_ctk.py @@ -53,5 +53,24 @@ def test_full_ui_has_scan_btn(self): self.assertTrue(hasattr(app, "status_label")) + def test_has_filters_container(self): + import ui_ctk, importlib + importlib.reload(ui_ctk) + app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) + app.window.after(0, app.window.destroy) + app.run() + self.assertTrue(hasattr(app, "filters_container")) + + def test_add_file_type_filter(self): + import ui_ctk, importlib + importlib.reload(ui_ctk) + app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) + app._add_file_type_filter(".mp4") + app.window.after(0, app.window.destroy) + app.run() + self.assertIn(".mp4", app.file_types) + self.assertIn(".mp4", app.file_type_widgets) + + if __name__ == "__main__": unittest.main() diff --git a/ui_ctk.py b/ui_ctk.py index 1eed3b0..bc88851 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -246,7 +246,43 @@ def notify_error(self, title: str, message: str) -> None: # --- Stub methods (implemented in later Tasks) --- def _build_filters_row(self) -> None: - pass + self._ctrl_row_frame = ctk.CTkFrame(self.window, fg_color="transparent") + self._ctrl_row_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=(4, 0)) + self._ctrl_row_frame.grid_columnconfigure(0, weight=1) + + filters_frame = ctk.CTkFrame(self._ctrl_row_frame, fg_color="transparent") + filters_frame.grid(row=0, column=0, sticky="ew") + + self.filters_container = ctk.CTkScrollableFrame( + filters_frame, + height=70, + orientation="horizontal", + fg_color="transparent", + ) + self.filters_container.pack(fill="x") + + type_actions = ctk.CTkFrame(filters_frame, fg_color="transparent") + type_actions.pack(fill="x", pady=(4, 0)) + + ctk.CTkButton( + type_actions, + text="Select All Types", + fg_color=("gray80", "gray25"), + text_color=("gray10", "gray90"), + hover_color=("gray70", "gray35"), + command=self.select_all_types, + width=140, + ).pack(side="left", padx=(0, 6)) + + ctk.CTkButton( + type_actions, + text="Deselect All Types", + fg_color=("gray80", "gray25"), + text_color=("gray10", "gray90"), + hover_color=("gray70", "gray35"), + command=self.deselect_all_types, + width=150, + ).pack(side="left") def _build_treeview(self) -> None: pass @@ -317,3 +353,29 @@ def collapse_all(self): pass def start_scan(self): pass def toggle_scan_pause(self): pass def clear_scan_results(self): pass + + def select_all_types(self) -> None: + for var in self.file_types.values(): + var.set(True) + + def deselect_all_types(self) -> None: + for var in self.file_types.values(): + var.set(False) + + def _add_file_type_filter(self, ext: str) -> None: + if not hasattr(self, "filters_container"): + return + if ext in self.file_types: + return + var = tk.BooleanVar(value=True) + self.file_types[ext] = var + self.file_type_counts[ext] = 0 + cb = ctk.CTkCheckBox( + self.filters_container, + text=ext if ext else "(no ext)", + variable=var, + onvalue=True, + offvalue=False, + ) + cb.pack(side="left", padx=4, pady=4) + self.file_type_widgets[ext] = cb From e14401e64668e3ad17b9f0fe48a5d5685360620d Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 19:43:01 +0800 Subject: [PATCH 11/55] =?UTF-8?q?feat(ui):=20=E5=AF=A6=E4=BD=9C=E4=B8=8B?= =?UTF-8?q?=E8=BC=89=E6=8E=A7=E5=88=B6=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 _build_download_controls、choose_download_path、update_thread_count、toggle_panels 方法, 並補充 download_selected / toggle_pause stub,使 test_has_download_controls 等 8 項測試全數通過。 Co-Authored-By: Claude Sonnet 4.6 --- test_ui_ctk.py | 12 ++++++++ ui_ctk.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/test_ui_ctk.py b/test_ui_ctk.py index b21fa06..f2fe5bb 100644 --- a/test_ui_ctk.py +++ b/test_ui_ctk.py @@ -72,5 +72,17 @@ def test_add_file_type_filter(self): self.assertIn(".mp4", app.file_type_widgets) + def test_has_download_controls(self): + import ui_ctk, importlib + importlib.reload(ui_ctk) + app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) + app.window.after(0, app.window.destroy) + app.run() + self.assertTrue(hasattr(app, "download_btn")) + self.assertTrue(hasattr(app, "pause_btn")) + self.assertTrue(hasattr(app, "threads_var")) + self.assertTrue(hasattr(app, "toggle_panels_btn")) + + if __name__ == "__main__": unittest.main() diff --git a/ui_ctk.py b/ui_ctk.py index bc88851..eac5e44 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -294,7 +294,53 @@ def _build_panels(self) -> None: pass def _build_download_controls(self) -> None: - pass + controls = ctk.CTkFrame(self._ctrl_row_frame, fg_color="transparent") + controls.grid(row=0, column=1, sticky="e", padx=(10, 0)) + + self.download_btn = ctk.CTkButton( + controls, text="Download Selected", command=self.download_selected + ) + self.download_btn.grid(row=0, column=0, padx=4) + + self.pause_btn = ctk.CTkButton( + controls, text="Pause", + fg_color=("gray70", "gray30"), + hover_color=("gray60", "gray40"), + command=self.toggle_pause, + state="disabled", + ) + self.pause_btn.grid(row=0, column=1, padx=4) + + self.path_btn = ctk.CTkButton( + controls, text="Choose Folder", + fg_color=("gray70", "gray30"), + hover_color=("gray60", "gray40"), + command=self.choose_download_path, + ) + self.path_btn.grid(row=0, column=2, padx=4) + + ctk.CTkLabel(controls, text="Threads").grid(row=0, column=3, padx=(12, 4)) + + self.threads_var = tk.StringVar(value="5") + self.threads_combo = ctk.CTkOptionMenu( + controls, + values=[str(i) for i in range(1, 11)], + variable=self.threads_var, + command=self.update_thread_count, + width=70, + ) + self.threads_combo.grid(row=0, column=4) + + self.panels_visible = True + self.toggle_panels_btn = ctk.CTkButton( + controls, text="Hide Panels", + fg_color=("gray80", "gray25"), + text_color=("gray10", "gray90"), + hover_color=("gray70", "gray35"), + command=self.toggle_panels, + width=110, + ) + self.toggle_panels_btn.grid(row=0, column=5, padx=(10, 0)) def focus_search(self, event=None) -> None: pass @@ -353,6 +399,32 @@ def collapse_all(self): pass def start_scan(self): pass def toggle_scan_pause(self): pass def clear_scan_results(self): pass + def download_selected(self): pass + def toggle_pause(self): pass + + def choose_download_path(self) -> None: + path = filedialog.askdirectory(title="Choose Download Location") + if path: + self.download_path = path + + def update_thread_count(self, value=None) -> None: + try: + n = int(self.threads_var.get()) + except (ValueError, AttributeError): + return + self.max_workers = max(1, min(10, n)) + + def toggle_panels(self) -> None: + if self.panels_visible: + if hasattr(self, "_panels_widget"): + self._panels_widget.grid_remove() + self.panels_visible = False + self.toggle_panels_btn.configure(text="Show Panels") + else: + if hasattr(self, "_panels_widget"): + self._panels_widget.grid() + self.panels_visible = True + self.toggle_panels_btn.configure(text="Hide Panels") def select_all_types(self) -> None: for var in self.file_types.values(): From 009742a98f436e4e73c40ebcb5002e39f2da91d2 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 19:51:18 +0800 Subject: [PATCH 12/55] =?UTF-8?q?feat(ui):=20=E5=AF=A6=E4=BD=9C=20Treeview?= =?UTF-8?q?=EF=BC=88=E6=90=9C=E5=B0=8B=E6=AC=84=E3=80=81=E6=8E=92=E5=BA=8F?= =?UTF-8?q?=E3=80=81=E5=8B=BE=E9=81=B8=E3=80=81=E4=BA=92=E5=8B=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test_ui_ctk.py | 18 ++++ ui_ctk.py | 231 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 244 insertions(+), 5 deletions(-) diff --git a/test_ui_ctk.py b/test_ui_ctk.py index f2fe5bb..4f006b5 100644 --- a/test_ui_ctk.py +++ b/test_ui_ctk.py @@ -83,6 +83,24 @@ def test_has_download_controls(self): self.assertTrue(hasattr(app, "threads_var")) self.assertTrue(hasattr(app, "toggle_panels_btn")) + def test_has_treeview(self): + import ui_ctk, importlib + importlib.reload(ui_ctk) + app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) + app.window.after(0, app.window.destroy) + app.run() + self.assertTrue(hasattr(app, "tree")) + self.assertTrue(hasattr(app, "search_var")) + + def test_search_var_traces(self): + import ui_ctk, importlib + importlib.reload(ui_ctk) + app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) + app.window.after(0, app.window.destroy) + app.run() + # search_var should have trace (on_search_filter_changed) + self.assertTrue(len(app.search_var.trace_info()) > 0) + if __name__ == "__main__": unittest.main() diff --git a/ui_ctk.py b/ui_ctk.py index eac5e44..e5e901a 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -285,7 +285,60 @@ def _build_filters_row(self) -> None: ).pack(side="left") def _build_treeview(self) -> None: - pass + tree_frame = ctk.CTkFrame(self.window, fg_color="transparent") + tree_frame.grid(row=2, column=0, sticky="nsew", padx=10, pady=(4, 0)) + tree_frame.grid_columnconfigure(0, weight=1) + tree_frame.grid_rowconfigure(1, weight=1) + + # 搜尋欄 + search_bar = ctk.CTkFrame(tree_frame, fg_color="transparent") + search_bar.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 4)) + search_bar.grid_columnconfigure(1, weight=1) + ctk.CTkLabel(search_bar, text="Search").grid(row=0, column=0, padx=(0, 6)) + self.search_var = tk.StringVar() + self.search_entry = ctk.CTkEntry(search_bar, textvariable=self.search_var) + self.search_entry.grid(row=0, column=1, sticky="ew") + self.search_var.trace_add("write", self.on_search_filter_changed) + + # Treeview + configure_treeview_style(self.window, ctk, ttk) + self.tree = ttk.Treeview( + tree_frame, + columns=("size", "type", "full_path"), + show="tree headings", + selectmode="extended", + ) + self.tree.heading("#0", text="Path", command=lambda: self.sort_tree("#0")) + self.tree.heading("size", text="Size", command=lambda: self.sort_tree("size")) + self.tree.heading("type", text="Type", command=lambda: self.sort_tree("type")) + self.tree.column("#0", width=600, stretch=True) + self.tree.column("size", width=120, stretch=False, anchor="e") + self.tree.column("type", width=240, stretch=False) + self.tree.column("full_path", width=0, stretch=False) + + tag_colors = treeview_tag_colors(self.window) + self.tree.tag_configure("checked", foreground=tag_colors["checked"]) + + style = ttk.Style() + style.configure("Treeview", font=("SF Pro Text", 14), rowheight=34) + style.configure("Treeview.Heading", font=("SF Pro Text", 13, "bold")) + + self.tree.grid(row=1, column=0, sticky="nsew") + + tree_scroll = tk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview) + self.tree.configure(yscrollcommand=tree_scroll.set) + tree_scroll.grid(row=1, column=1, sticky="ns") + + # 鍵盤與滑鼠綁定 + self.tree.bind("", self._on_tree_select_all) + self.tree.bind("", self._on_tree_select_all) + self.tree.bind("", self.on_tree_click) + self.tree.bind("", self.show_context_menu) + self.tree.bind("", self.show_context_menu) + self.tree.bind("", self.show_context_menu) + self.tree.bind("", self.on_tree_drag_select) + self.tree.bind("", self.on_tree_space) + self.tree.bind("", self.on_tree_enter) def _build_progress_section(self) -> None: pass @@ -392,10 +445,178 @@ def _set_status(self, text: str, color: str = "#059669") -> None: except Exception: pass - def select_all(self): pass - def deselect_all(self): pass - def expand_all(self): pass - def collapse_all(self): pass + # --- Treeview interaction helpers --- + + def _all_tree_items(self): + out = [] + stack = list(self.tree.get_children("")) + while stack: + item = stack.pop() + out.append(item) + stack.extend(self.tree.get_children(item)) + return out + + def _focused_tree_item(self): + try: + item = self.tree.focus() + if item and self.tree.exists(item): + return item + selected = self.tree.selection() + if selected: + return selected[0] + except tk.TclError: + return "" + return "" + + def _strip_checkmark(self, text: str) -> str: + if text.startswith(self.checkbox_checked): + return text[len(self.checkbox_checked):] + return text + + def _set_item_checked_visual(self, item: str, checked: bool) -> None: + try: + text = self.tree.item(item, "text") + except tk.TclError: + return + raw = self._strip_checkmark(text or "") + display = f"{self.checkbox_checked}{raw}" if checked else raw + try: + self.tree.item(item, text=display) + except tk.TclError: + return + + def toggle_check(self, item, force_check=None): + try: + if not self.tree.exists(item): + return + tags = list(self.tree.item(item, "tags")) + except tk.TclError: + return + + is_checked = "checked" in tags + new_checked = (not is_checked) if force_check is None else bool(force_check) + + if "file" in tags: + full_path = self.tree.set(item, "full_path") + if full_path: + if new_checked: + self.checked_items.add(full_path) + else: + self.checked_items.discard(full_path) + + if new_checked and "checked" not in tags: + tags.append("checked") + if (not new_checked) and "checked" in tags: + tags.remove("checked") + self.tree.item(item, tags=tuple(tags)) + self._set_item_checked_visual(item, new_checked) + + if "folder" in tags: + for child in self.tree.get_children(item): + self.toggle_check(child, force_check=new_checked) + + def on_tree_click(self, event): + try: + item = self.tree.identify("item", event.x, event.y) + if not item: + return + self.drag_anchor_item = item + self.tree.focus(item) + element = self.tree.identify_element(event.x, event.y) + if "indicator" in element: + return + modifier_mask = 0x0001 | 0x0004 | 0x0008 | 0x0010 | 0x0080 + if event.state & modifier_mask: + return + if self.tree.identify_column(event.x) == "#0": + self.toggle_check(item) + except tk.TclError: + return + + def on_tree_drag_select(self, event): + try: + if not self.drag_anchor_item: + return + target = self.tree.identify_row(event.y) + if not target: + return + items = self._all_tree_items() + if self.drag_anchor_item not in items or target not in items: + return + a = items.index(self.drag_anchor_item) + b = items.index(target) + lo, hi = (a, b) if a <= b else (b, a) + self.tree.selection_set(items[lo: hi + 1]) + except tk.TclError: + return + + def on_tree_space(self, _event=None): + item = self._focused_tree_item() + if item: + self.toggle_check(item) + return "break" + return None + + def on_tree_enter(self, _event=None): + item = self._focused_tree_item() + if not item: + return None + try: + tags = self.tree.item(item, "tags") + if "folder" in tags: + self.tree.item(item, open=not bool(self.tree.item(item, "open"))) + else: + self.toggle_check(item) + except tk.TclError: + return None + return "break" + + def _on_tree_select_all(self, _event=None): + self.select_all() + return "break" + + def show_context_menu(self, event): + try: + item = self.tree.identify_row(event.y) + if item: + self.tree.focus(item) + self.tree.selection_set(item) + self.context_menu.tk_popup(event.x_root, event.y_root) + finally: + self.context_menu.grab_release() + return "break" + + def select_all(self): + for item in self._all_tree_items(): + self.toggle_check(item, force_check=True) + + def deselect_all(self): + for item in self._all_tree_items(): + self.toggle_check(item, force_check=False) + + def expand_all(self, parent=""): + for item in self.tree.get_children(parent): + self.tree.item(item, open=True) + self.expand_all(item) + + def collapse_all(self, parent=""): + for item in self.tree.get_children(parent): + self.tree.item(item, open=False) + self.collapse_all(item) + + def sort_tree(self, col): + try: + items = [(self.tree.set(i, col), i) for i in self.tree.get_children("")] + items.sort(reverse=self.sort_reverse) + for index, (_, i) in enumerate(items): + self.tree.move(i, "", index) + self.sort_reverse = not self.sort_reverse + except tk.TclError: + pass + + def on_search_filter_changed(self, *args): + pass + def start_scan(self): pass def toggle_scan_pause(self): pass def clear_scan_results(self): pass From bf12f18b09ee6ccdf03668d4afab5691b2a3ef91 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 19:53:50 +0800 Subject: [PATCH 13/55] =?UTF-8?q?fix(ui):=20sort=5Ftree=20=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20#0=20=E6=AC=84=E5=8F=96=E5=80=BC=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=E8=88=87=E9=81=9E=E8=BF=B4=E6=8E=92=E5=BA=8F=E5=AD=90=E7=AF=80?= =?UTF-8?q?=E9=BB=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- ui_ctk.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/ui_ctk.py b/ui_ctk.py index e5e901a..0d2f3c3 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -604,15 +604,24 @@ def collapse_all(self, parent=""): self.tree.item(item, open=False) self.collapse_all(item) - def sort_tree(self, col): - try: - items = [(self.tree.set(i, col), i) for i in self.tree.get_children("")] - items.sort(reverse=self.sort_reverse) - for index, (_, i) in enumerate(items): - self.tree.move(i, "", index) - self.sort_reverse = not self.sort_reverse - except tk.TclError: - pass + def sort_tree(self, col: str) -> None: + def get_key(item_id: str) -> str: + if col == "#0": + return self._strip_checkmark(self.tree.item(item_id, "text")).lower() + return self.tree.set(item_id, col).lower() + + def sort_children(parent: str) -> None: + children = list(self.tree.get_children(parent)) + if not children: + return + children.sort(key=get_key, reverse=self.sort_reverse) + for idx, item in enumerate(children): + self.tree.move(item, parent, idx) + for item in children: + sort_children(item) + + sort_children("") + self.sort_reverse = not self.sort_reverse def on_search_filter_changed(self, *args): pass From 7d6f4f11a24307b4fd20536f363757a2df89132c Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 19:55:23 +0800 Subject: [PATCH 14/55] =?UTF-8?q?feat(ui):=20=E5=AF=A6=E4=BD=9C=E9=80=B2?= =?UTF-8?q?=E5=BA=A6=E6=A2=9D=E8=88=87=20CTkTabview=EF=BC=88Downloads=20+?= =?UTF-8?q?=20Logs=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test_ui_ctk.py | 20 ++++++++++++++++++++ ui_ctk.py | 33 +++++++++++++++++++++++++++++++-- ui_downloads.py | 2 +- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/test_ui_ctk.py b/test_ui_ctk.py index 4f006b5..ca5b13b 100644 --- a/test_ui_ctk.py +++ b/test_ui_ctk.py @@ -101,6 +101,26 @@ def test_search_var_traces(self): # search_var should have trace (on_search_filter_changed) self.assertTrue(len(app.search_var.trace_info()) > 0) + def test_has_progress_bar(self): + import ui_ctk, importlib + importlib.reload(ui_ctk) + app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) + app.window.after(0, app.window.destroy) + app.run() + self.assertTrue(hasattr(app, "progress_bar")) + self.assertTrue(hasattr(app, "progress_label")) + + def test_has_panels_notebook(self): + import ui_ctk, importlib + importlib.reload(ui_ctk) + app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) + app.window.after(0, app.window.destroy) + app.run() + self.assertTrue(hasattr(app, "panels_notebook")) + self.assertIsNotNone(app.panels_notebook) + self.assertTrue(hasattr(app, "log_text")) + self.assertTrue(hasattr(app, "downloads_panel")) + if __name__ == "__main__": unittest.main() diff --git a/ui_ctk.py b/ui_ctk.py index 0d2f3c3..7ac54c7 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -341,10 +341,39 @@ def _build_treeview(self) -> None: self.tree.bind("", self.on_tree_enter) def _build_progress_section(self) -> None: - pass + progress_frame = ctk.CTkFrame(self.window, fg_color="transparent") + progress_frame.grid(row=3, column=0, sticky="ew", padx=10, pady=(4, 0)) + progress_frame.grid_columnconfigure(0, weight=1) + + self.progress_bar = ctk.CTkProgressBar(progress_frame) + self.progress_bar.set(0) + self.progress_bar.grid(row=0, column=0, sticky="ew") + + self.progress_label = ctk.CTkLabel(progress_frame, text="") + self.progress_label.grid(row=1, column=0, sticky="w") def _build_panels(self) -> None: - pass + self.panels_notebook = ctk.CTkTabview(self.window, height=180) + self.panels_notebook.grid(row=4, column=0, sticky="ew", padx=10, pady=(4, 10)) + self._panels_widget = self.panels_notebook # toggle_panels 用 + + downloads_tab = self.panels_notebook.add("Downloads") + logs_tab = self.panels_notebook.add("Logs") + self.panels_notebook.set("Logs") # 預設顯示 Logs + + # Downloads tab:可捲動容器 + DownloadsPanel + downloads_scroll = ctk.CTkScrollableFrame(downloads_tab, height=120) + downloads_scroll.pack(fill="both", expand=True) + self.downloads_panel = DownloadsPanel( + parent_frame=downloads_scroll, + ctk=ctk, + tk=tk, + tokens=self.ui_tokens, + ) + + # Logs tab:CTkTextbox + self.log_text = ctk.CTkTextbox(logs_tab, height=120, wrap="word") + self.log_text.pack(fill="both", expand=True) def _build_download_controls(self) -> None: controls = ctk.CTkFrame(self._ctrl_row_frame, fg_color="transparent") diff --git a/ui_downloads.py b/ui_downloads.py index 103c989..0f809c3 100644 --- a/ui_downloads.py +++ b/ui_downloads.py @@ -2,7 +2,7 @@ class DownloadsPanel: - def __init__(self, parent_frame, ctk, tk, threading_module=None): + def __init__(self, parent_frame, ctk, tk, threading_module=None, tokens=None): self.parent_frame = parent_frame self.ctk = ctk self.tk = tk From 4d26575d49019099aae9f95ea0fff80fdab0edf4 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 20:00:53 +0800 Subject: [PATCH 15/55] =?UTF-8?q?feat(ui):=20=E7=A7=BB=E6=A4=8D=20Queue=20?= =?UTF-8?q?=E8=BC=AA=E8=A9=A2=E8=88=87=20scan=20buffer=20throttle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test_ui_ctk.py | 11 ++++++ ui_ctk.py | 103 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/test_ui_ctk.py b/test_ui_ctk.py index ca5b13b..782f625 100644 --- a/test_ui_ctk.py +++ b/test_ui_ctk.py @@ -122,5 +122,16 @@ def test_has_panels_notebook(self): self.assertTrue(hasattr(app, "downloads_panel")) + def test_poll_methods_exist(self): + import ui_ctk, importlib + importlib.reload(ui_ctk) + app = ui_ctk.WebsiteCopierCtk(ui_smoke=True) + for method in ("_poll_scan_queue", "_poll_file_queue", + "_flush_scan_buffer", "_schedule_flush"): + self.assertTrue(hasattr(app, method), f"Missing: {method}") + app.window.after(0, app.window.destroy) + app.run() + + if __name__ == "__main__": unittest.main() diff --git a/ui_ctk.py b/ui_ctk.py index 7ac54c7..b80f805 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -101,6 +101,10 @@ def __init__(self, ui_smoke: bool = False): self._build_ui() + if not self._ui_smoke: + self.window.after(100, self._poll_scan_queue) + self.window.after(100, self._poll_file_queue) + self.window.protocol("WM_DELETE_WINDOW", self.on_closing) if not self._ui_smoke: self.window.bind("", self.focus_search) @@ -243,6 +247,96 @@ def notify_error(self, title: str, message: str) -> None: else: self.log_message(f"[ERROR] {title}: {message}") + # --- Queue polling (thread-safe) --- + + def _schedule_flush(self) -> None: + """Schedule a deferred flush of scan_item_buffer if not already pending.""" + if self.scan_flush_job is not None: + return + if not self.is_scanning and self.scan_item_buffer.empty(): + return + self.scan_flush_job = self.window.after( + self.scan_flush_interval_ms, self._flush_scan_buffer + ) + + def _flush_scan_buffer(self, max_items: int | None = None, reschedule: bool = True) -> None: + """Drain scan_item_buffer in batches and dispatch to dir/file queues.""" + self.scan_flush_job = None + if max_items is None: + max_items = self.scan_flush_batch_size + + processed = 0 + added_dir = False + added_file = False + + while processed < max_items: + try: + ( + is_directory, + path, + url, + file_name, + size, + file_type, + full_path, + ) = self.scan_item_buffer.get_nowait() + except Empty: + break + + if is_directory: + with self.folders_dict_lock: + self.dir_queue.put((path, url)) + added_dir = True + else: + self.file_queue.put((path, url, file_name, size, file_type, full_path)) + added_file = True + processed += 1 + + if added_dir and not self.is_processing_dirs: + self.window.after(0, self._poll_scan_queue) + if added_file and not self.is_processing_files: + self.window.after(0, self._poll_file_queue) + + if reschedule and not self.scan_item_buffer.empty(): + self._schedule_flush() + + def _poll_scan_queue(self) -> None: + """Process one item from dir_queue and reschedule until empty.""" + try: + self.is_processing_dirs = True + if not self.dir_queue.empty(): + dir_path, url = self.dir_queue.get() + self.add_folder(dir_path, url) + self.window.after(10, self._poll_scan_queue) + else: + self.is_processing_dirs = False + except tk.TclError: + self.is_processing_dirs = False + + def _poll_file_queue(self) -> None: + """Process one item from file_queue and reschedule until empty.""" + try: + self.is_processing_files = True + qsize = self.file_queue.qsize() + if qsize != self._last_logged_queue_size and (qsize <= 5 or qsize % 100 == 0): + self._debug(f"_poll_file_queue queue_size={qsize}") + self._last_logged_queue_size = qsize + if not self.file_queue.empty(): + ( + dir_path, + url, + file_name, + size, + file_type, + full_path, + ) = self.file_queue.get() + self.add_file(dir_path, url, file_name, size, file_type, full_path) + self.window.after(5, self._poll_file_queue) + else: + self.is_processing_files = False + except tk.TclError: + self.is_processing_files = False + # --- Stub methods (implemented in later Tasks) --- def _build_filters_row(self) -> None: @@ -661,6 +755,15 @@ def clear_scan_results(self): pass def download_selected(self): pass def toggle_pause(self): pass + # Stubs for Task 10 (backend bridge) + def add_folder(self, dir_path: str, url: str) -> str: + """Stub: create folder node in treeview (Task 10).""" + return "" + + def add_file(self, dir_path: str, url: str, file_name: str, size, file_type: str, full_path: str) -> None: + """Stub: add file row to treeview (Task 10).""" + pass + def choose_download_path(self) -> None: path = filedialog.askdirectory(title="Choose Download Location") if path: From 585c764b454a3367fe4c3a61f62526b3a9a28bfd Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 20:04:25 +0800 Subject: [PATCH 16/55] =?UTF-8?q?fix(ui):=20=E7=A7=BB=E9=99=A4=20=5Fflush?= =?UTF-8?q?=5Fscan=5Fbuffer=20=E4=B8=AD=E4=B8=8D=E5=BF=85=E8=A6=81?= =?UTF-8?q?=E7=9A=84=20lock=20=E5=8C=85=E8=A3=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui_ctk.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui_ctk.py b/ui_ctk.py index b80f805..842f689 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -284,8 +284,7 @@ def _flush_scan_buffer(self, max_items: int | None = None, reschedule: bool = Tr break if is_directory: - with self.folders_dict_lock: - self.dir_queue.put((path, url)) + self.dir_queue.put((path, url)) added_dir = True else: self.file_queue.put((path, url, file_name, size, file_type, full_path)) From 4e6367053fa3fe6ad3e8df547a7754536e0ab21a Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 20:07:06 +0800 Subject: [PATCH 17/55] =?UTF-8?q?feat(ui):=20=E7=A7=BB=E6=A4=8D=20backend?= =?UTF-8?q?=20=E6=A9=8B=E6=8E=A5=E6=96=B9=E6=B3=95=EF=BC=88=E6=8E=83?= =?UTF-8?q?=E6=8F=8F=E3=80=81=E4=B8=8B=E8=BC=89=E3=80=81=E6=90=9C=E5=B0=8B?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- test_ui_ctk.py | 9 ++ ui_ctk.py | 347 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 341 insertions(+), 15 deletions(-) diff --git a/test_ui_ctk.py b/test_ui_ctk.py index 782f625..722005c 100644 --- a/test_ui_ctk.py +++ b/test_ui_ctk.py @@ -132,6 +132,15 @@ def test_poll_methods_exist(self): app.window.after(0, app.window.destroy) app.run() + def test_scan_methods_exist(self): + import ui_ctk + for method in ("start_scan", "toggle_scan_pause", "clear_scan_results", + "add_folder", "add_file", "update_progress", + "download_selected", "toggle_pause", + "on_search_filter_changed", "_apply_search_filter", + "focus_search", "focus_logs", "clear_search"): + self.assertTrue(hasattr(ui_ctk.WebsiteCopierCtk, method), f"Missing: {method}") + if __name__ == "__main__": unittest.main() diff --git a/ui_ctk.py b/ui_ctk.py index 842f689..56124ba 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -518,13 +518,22 @@ def _build_download_controls(self) -> None: self.toggle_panels_btn.grid(row=0, column=5, padx=(10, 0)) def focus_search(self, event=None) -> None: - pass + try: + self.search_entry.focus_set() + except tk.TclError: + pass def focus_logs(self, event=None) -> None: - pass + try: + if hasattr(self, "panels_notebook") and self.panels_notebook is not None: + self.panels_notebook.set("Logs") + self.log_text.focus_set() + except (AttributeError, tk.TclError): + pass def clear_search(self, event=None) -> None: - pass + if hasattr(self, "search_var"): + self.search_var.set("") def _on_url_paste(self, _event=None): self._paste_into_url_entry() @@ -745,23 +754,331 @@ def sort_children(parent: str) -> None: sort_children("") self.sort_reverse = not self.sort_reverse - def on_search_filter_changed(self, *args): - pass + # --- Status color mapping --- + + _STATUS_COLORS = { + "success": "#059669", + "danger": "#DC2626", + "warning": "#B45309", + "secondary": "#64748B", + "info": "#0284C7", + } + + # --- Backend bridge: scan --- + + def _run_on_ui_thread(self, callback, *args, wait: bool = False): + if not wait: + self.window.after(0, lambda: callback(*args)) + return + done = threading.Event() + error_box = {} + + def wrapped(): + try: + callback(*args) + except Exception as ex: + error_box["error"] = ex + finally: + done.set() + + self.window.after(0, wrapped) + done.wait() + if "error" in error_box: + raise error_box["error"] + + def start_scan(self) -> None: + url = self.url_var.get().strip() + if self.is_scanning: + self.backend.should_stop = True + self._set_status("Stopping", "#B45309") + return + if not url: + self.notify_error("Error", "Please enter a URL") + return + + self.backend.should_stop = False + self.search_var.set("") + self.full_tree_backup.clear() + try: + self.download_path = default_download_folder(url, os.getcwd()) + except Exception: + self.download_path = os.path.join(os.getcwd(), "downloads") - def start_scan(self): pass - def toggle_scan_pause(self): pass - def clear_scan_results(self): pass - def download_selected(self): pass - def toggle_pause(self): pass + t = threading.Thread(target=self.backend.scan_website, args=(url,), daemon=True) + t.start() + + def toggle_scan_pause(self) -> None: + if self.scan_pause_event.is_set(): + self.scan_pause_event.clear() + self.scan_pause_btn.configure(text="Resume Scan", state="normal") + self._set_status("Scan Paused", "#B45309") + else: + self.scan_pause_event.set() + self.scan_pause_btn.configure(text="Pause Scan", state="normal") + self._set_status("Scanning", "#0284C7") + + def clear_scan_results(self) -> None: + if self.is_scanning: + return + for item in self.tree.get_children(""): + self.tree.delete(item) + with self.files_dict_lock: + self.files_dict.clear() + with self.folders_dict_lock: + self.folders.clear() + if hasattr(self, "filters_container"): + for widget in self.filters_container.winfo_children(): + widget.destroy() + self.file_types.clear() + self.file_type_counts.clear() + self.file_type_widgets.clear() + self.checked_items.clear() + self.progress_bar.set(0) + self.progress_label.configure(text="") + self._set_status("Ready", "#059669") + + def update_progress(self, scanned: int, total: int) -> None: + """Update the scan progress bar (0–100%).""" + if total <= 0: + self.progress_bar.set(0) + return + pct = (scanned / total) * 100 + self.progress_bar.set(pct / 100) + self.progress_label.configure(text=f"Scan: {scanned}/{total}") - # Stubs for Task 10 (backend bridge) def add_folder(self, dir_path: str, url: str) -> str: - """Stub: create folder node in treeview (Task 10).""" - return "" + """Create folder nodes in the treeview for the given path.""" + if not dir_path: + dir_path = "/" + parts = [p for p in dir_path.split("/") if p] + + current = "" + parent_id = "" + for part in parts: + current = current + "/" + part + with self.folders_dict_lock: + existing = self.folders.get(current) + if existing: + parent_id = existing + continue + + node_id = self.tree.insert( + parent_id, + "end", + text=f"📁 {part}", + values=("", "dir", ""), + tags=("folder",), + open=True, + ) + with self.folders_dict_lock: + self.folders[current] = node_id + parent_id = node_id + + return parent_id def add_file(self, dir_path: str, url: str, file_name: str, size, file_type: str, full_path: str) -> None: - """Stub: add file row to treeview (Task 10).""" - pass + """Add a file row to the treeview.""" + if not file_name: + return + is_html_dir_like = ( + isinstance(file_type, str) + and "text/html" in file_type.lower() + and "." not in (file_name or "") + ) + parent_id = self.add_folder(dir_path, url) + with self.files_dict_lock: + existing_entry = self.files_dict.get(full_path) + if should_skip_file_row(existing_entry): + return + if is_html_dir_like: + self.files_dict.pop(full_path, None) + else: + self.files_dict[full_path] = { + "url": url, + "file_name": file_name, + "size": size, + "file_type": file_type, + "path": dir_path, + } + + if is_html_dir_like: + folder_path = f"{dir_path.rstrip('/')}/{file_name}".replace("//", "/") + self.add_folder(folder_path, url) + return + + ext = normalize_extension(file_name) + self._add_file_type_filter(ext) + var = self.file_types.get(ext) + if var is not None and not var.get(): + return + icon, group = self._file_icon_and_group(file_name, file_type) + display_type = group if not file_type else f"{group} | {file_type}" + + node_id = self.tree.insert( + parent_id, + "end", + text=f"{icon} {file_name}", + values=(size or "", display_type, full_path or ""), + tags=("file", ext), + ) + if full_path and full_path in self.checked_items: + self._set_item_checked_visual(node_id, True) + tags = list(self.tree.item(node_id, "tags")) + if "checked" not in tags: + tags.append("checked") + self.tree.item(node_id, tags=tuple(tags)) + + def _file_icon_and_group(self, file_name: str, file_type: str | None): + ext = normalize_extension(file_name) + mime = (file_type or "").lower() + if ext in (".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp", ".ico"): + return "🖼", "image" + if ext in (".md", ".txt", ".pdf", ".doc", ".docx", ".rtf"): + return "📄", "document" + if ext in (".zip", ".rar", ".7z", ".tar", ".gz", ".bz2", ".xz"): + return "🗜", "archive" + if ext in ( + ".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs", ".java", ".c", ".cpp", + ".h", ".hpp", ".json", ".yaml", ".yml", ".toml", ".xml", ".html", ".css", ".sh", + ): + return "💻", "code" + if "text/" in mime: + return "📄", "text" + if "image/" in mime: + return "🖼", "image" + if "audio/" in mime: + return "🎵", "audio" + if "video/" in mime: + return "🎬", "video" + return "📦", "binary" + + # --- Backend bridge: downloads --- + + def download_selected(self) -> None: + if not self.download_path: + url = self.url_var.get().strip() + if url: + try: + self.download_path = default_download_folder(url, os.getcwd()) + except Exception: + self.download_path = os.path.join(os.getcwd(), "downloads") + else: + self.download_path = os.path.join(os.getcwd(), "downloads") + + os.makedirs(self.download_path, exist_ok=True) + + selected = [] + with self.files_dict_lock: + for full_path in sorted(self.checked_items): + info = self.files_dict.get(full_path) + if info: + selected.append((full_path, info)) + if not selected: + selected_item_ids = list(self.tree.selection()) + for item_id in selected_item_ids: + try: + tags = self.tree.item(item_id, "tags") + except tk.TclError: + continue + if "file" not in tags: + continue + full_path = self.tree.set(item_id, "full_path") + if not full_path: + continue + info = self.files_dict.get(full_path) + if info: + selected.append((full_path, info)) + + if not selected: + self.notify_info("Info", "No selected files to download") + return + + self.pause_btn.configure(state="normal") + futures = [] + for full_path, info in selected: + url = info.get("url") + file_name = info.get("file_name") + if not url or not file_name: + continue + + safe_name = sanitize_filename(file_name) + file_path = safe_join(self.download_path, [safe_name]) + + cancel_event = self.downloads_panel.ensure(file_path, safe_name) + futures.append(self.executor.submit(self.backend.download_file, url, file_path, safe_name, cancel_event)) + + monitor = threading.Thread(target=self.backend.monitor_downloads, args=(futures,), daemon=True) + monitor.start() + + def toggle_pause(self) -> None: + if self.pause_event.is_set(): + self.pause_event.clear() + self.pause_btn.configure(text="Resume") + self.progress_label.configure(text="Downloads paused") + else: + self.pause_event.set() + self.pause_btn.configure(text="Pause") + + def _update_download_progress(self, file_path: str, progress: float) -> None: + self.downloads_panel.set_progress(file_path, progress) + + def _set_download_status(self, file_path: str, text: str) -> None: + self.downloads_panel.set_status(file_path, text) + + # --- Backend bridge: search --- + + def on_search_filter_changed(self, *args): + query = self.search_var.get() if hasattr(self, "search_var") else "" + self._apply_search_filter(query) + + def _apply_search_filter(self, query: str) -> None: + term = (query or "").strip().lower() + if not term: + if self.full_tree_backup: + self._restore_full_tree() + return + if not self.full_tree_backup: + self._backup_full_tree() + self._filter_tree_by_term(term) + + def _backup_full_tree(self) -> None: + self.full_tree_backup.clear() + for item in self._all_tree_items(): + self.full_tree_backup[item] = { + "parent": self.tree.parent(item), + "index": self.tree.index(item), + "text": self.tree.item(item, "text"), + "values": self.tree.item(item, "values"), + "tags": self.tree.item(item, "tags"), + "open": self.tree.item(item, "open"), + } + + def _restore_full_tree(self) -> None: + backup = dict(self.full_tree_backup) + self.full_tree_backup.clear() + for item in self.tree.get_children(""): + self.tree.delete(item) + with self.folders_dict_lock: + self.folders.clear() + + items_sorted = sorted(backup.items(), key=lambda kv: (kv[1]["parent"], kv[1]["index"])) + id_map = {} + for old_id, data in items_sorted: + parent_old = data["parent"] + parent_new = id_map.get(parent_old, "") if parent_old else "" + new_id = self.tree.insert(parent_new, "end", text=data["text"], values=data["values"], tags=data["tags"]) + self.tree.item(new_id, open=data["open"]) + id_map[old_id] = new_id + + def _filter_tree_by_term(self, term: str) -> None: + for item in self._all_tree_items(): + text = (self.tree.item(item, "text") or "").lower() + if term in text: + continue + try: + self.tree.detach(item) + except tk.TclError: + pass def choose_download_path(self) -> None: path = filedialog.askdirectory(title="Choose Download Location") From e5f880ea9527f4a8eef0a0413bf72bf6cc1fcbbb Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 20:09:59 +0800 Subject: [PATCH 18/55] =?UTF-8?q?fix(ui):=20=E4=BF=AE=E6=AD=A3=20=5Frestor?= =?UTF-8?q?e=5Ffull=5Ftree=20folders=20=E9=87=8D=E5=BB=BA=E3=80=81download?= =?UTF-8?q?=5Fselected=20lock=20=E7=AF=84=E5=9C=8D=E3=80=81toggle=5Fpause?= =?UTF-8?q?=20=E6=96=87=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- ui_ctk.py | 69 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/ui_ctk.py b/ui_ctk.py index 56124ba..6eef760 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -967,32 +967,51 @@ def download_selected(self) -> None: os.makedirs(self.download_path, exist_ok=True) - selected = [] + # Step 1: Collect candidate full_paths from the treeview without holding + # files_dict_lock, since treeview operations must happen on the UI thread + # and should not be interleaved with background lock-holding work. + checked_paths: list[str] = [] + for item_id in self._all_tree_items(): + try: + tags = self.tree.item(item_id, "tags") + except tk.TclError: + continue + if "checked" in tags and "file" in tags: + full_path = self.tree.set(item_id, "full_path") + if full_path: + checked_paths.append(full_path) + + # Fall back to treeview selection when nothing is checked. + if not checked_paths: + for item_id in list(self.tree.selection()): + try: + tags = self.tree.item(item_id, "tags") + except tk.TclError: + continue + if "file" not in tags: + continue + full_path = self.tree.set(item_id, "full_path") + if full_path: + checked_paths.append(full_path) + + if not checked_paths: + self.notify_info("Info", "No selected files to download") + return + + # Step 2: Hold lock only for the brief dict look-ups; copy info so the + # lock is released before any further work. + selected: list[tuple[str, dict]] = [] with self.files_dict_lock: - for full_path in sorted(self.checked_items): + for full_path in checked_paths: info = self.files_dict.get(full_path) if info: - selected.append((full_path, info)) - if not selected: - selected_item_ids = list(self.tree.selection()) - for item_id in selected_item_ids: - try: - tags = self.tree.item(item_id, "tags") - except tk.TclError: - continue - if "file" not in tags: - continue - full_path = self.tree.set(item_id, "full_path") - if not full_path: - continue - info = self.files_dict.get(full_path) - if info: - selected.append((full_path, info)) + selected.append((full_path, dict(info))) if not selected: self.notify_info("Info", "No selected files to download") return + # Step 3: Submit download tasks without holding the lock. self.pause_btn.configure(state="normal") futures = [] for full_path, info in selected: @@ -1018,6 +1037,7 @@ def toggle_pause(self) -> None: else: self.pause_event.set() self.pause_btn.configure(text="Pause") + self.progress_label.configure(text="") def _update_download_progress(self, file_path: str, progress: float) -> None: self.downloads_panel.set_progress(file_path, progress) @@ -1052,10 +1072,16 @@ def _backup_full_tree(self) -> None: "tags": self.tree.item(item, "tags"), "open": self.tree.item(item, "open"), } + # Snapshot folders so _restore_full_tree can rebuild the mapping. + with self.folders_dict_lock: + self._folders_backup = dict(self.folders) def _restore_full_tree(self) -> None: backup = dict(self.full_tree_backup) self.full_tree_backup.clear() + folders_snapshot = getattr(self, "_folders_backup", {}) + self._folders_backup = {} + for item in self.tree.get_children(""): self.tree.delete(item) with self.folders_dict_lock: @@ -1070,6 +1096,13 @@ def _restore_full_tree(self) -> None: self.tree.item(new_id, open=data["open"]) id_map[old_id] = new_id + # Rebuild self.folders by remapping old item IDs → new item IDs. + with self.folders_dict_lock: + for folder_path, old_id in folders_snapshot.items(): + new_id = id_map.get(old_id) + if new_id: + self.folders[folder_path] = new_id + def _filter_tree_by_term(self, term: str) -> None: for item in self._all_tree_items(): text = (self.tree.item(item, "text") or "").lower() From 001edf79329ee6521f71e7a85ee714d6d6edc0d2 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 23:47:44 +0800 Subject: [PATCH 19/55] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=8C=E9=A0=90=E8=A8=AD=E5=95=9F=E5=8B=95=20CTk=20?= =?UTF-8?q?=E7=89=88=20UI=20(WebsiteCopierCtk)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index_ripper.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/index_ripper.py b/index_ripper.py index e673672..1661207 100644 --- a/index_ripper.py +++ b/index_ripper.py @@ -1301,19 +1301,13 @@ def sort_tree(self, col): if __name__ == "__main__": if "--ui-smoke" in sys.argv: - app = WebsiteCopier() - try: - for _ in range(3): - app.window.update_idletasks() - app.window.update() - app.window.destroy() - except Exception: - try: - app.window.destroy() - except Exception: - pass + from ui_ctk import WebsiteCopierCtk + app = WebsiteCopierCtk(ui_smoke=True) + app.window.after(0, app.window.destroy) + app.run() print("UI_SMOKE_OK") raise SystemExit(0) - app = WebsiteCopier() + from ui_ctk import WebsiteCopierCtk + app = WebsiteCopierCtk() app.run() From 6f3cada74de52ad8414d2fb54c184bf7165e8742 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 23:52:00 +0800 Subject: [PATCH 20/55] =?UTF-8?q?fix:=20=E8=A3=9C=E5=85=85=20=5Fconfigure?= =?UTF-8?q?=5Ftk=5Flibraries()=20=E7=A2=BA=E4=BF=9D=20uv=20Python=20?= =?UTF-8?q?=E5=8F=AF=E6=89=BE=E5=88=B0=20Tcl/Tk=20=E8=B7=AF=E5=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index_ripper.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/index_ripper.py b/index_ripper.py index 1661207..6e3671c 100644 --- a/index_ripper.py +++ b/index_ripper.py @@ -23,6 +23,25 @@ ) raise SystemExit(0) + +def _configure_tk_libraries() -> None: + """Set Tcl/Tk library env vars for uv-managed Python when missing.""" + if os.environ.get("TCL_LIBRARY") and os.environ.get("TK_LIBRARY"): + return + for prefix in (sys.base_prefix, sys.prefix): + lib_root = os.path.join(prefix, "lib") + tcl_library = os.path.join(lib_root, "tcl8.6") + tk_library = os.path.join(lib_root, "tk8.6") + if os.path.isfile(os.path.join(tcl_library, "init.tcl")) and os.path.isfile( + os.path.join(tk_library, "tk.tcl") + ): + os.environ.setdefault("TCL_LIBRARY", tcl_library) + os.environ.setdefault("TK_LIBRARY", tk_library) + return + + +_configure_tk_libraries() + import tkinter as tk from tkinter import filedialog, messagebox, ttk From e78fa14094f7b98623447377f08fc238d9bee4e9 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 23:56:11 +0800 Subject: [PATCH 21/55] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=20stash=20pop?= =?UTF-8?q?=20=E5=B8=B6=E5=9B=9E=E7=9A=84=20apply=5Fbootstrap=5Ftheme=20?= =?UTF-8?q?=E6=AE=98=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend.py | 4 +- index_ripper.py | 191 ++++++++++++--- index_ripper_ttkb.py | 20 ++ ui_downloads.py | 54 ++++- ui_theme.py | 105 +++++++- ui_ttkb.py | 564 +++++++++++++++++++++++++++++++++++++++---- 6 files changed, 849 insertions(+), 89 deletions(-) diff --git a/backend.py b/backend.py index 8918a7b..fab72f2 100644 --- a/backend.py +++ b/backend.py @@ -184,6 +184,7 @@ def _get_all_urls(self, url, scanned_urls=None, base_url=None): href = link.get("href") if not href or href in [".", "..", "/"] or href.startswith("?"): continue + link_text = link.get_text(strip=True) full_url = urljoin(url, href) if not full_url or not is_url_in_scope(base_url, full_url): @@ -196,7 +197,8 @@ def _get_all_urls(self, url, scanned_urls=None, base_url=None): final_url = f"{parsed_full.scheme}://{parsed_full.netloc}{path}" - if href.endswith("/"): + is_directory_hint = href.endswith("/") or link_text.endswith("/") + if is_directory_hint: if not final_url.endswith("/"): final_url += "/" else: diff --git a/index_ripper.py b/index_ripper.py index 6e3671c..a299f22 100644 --- a/index_ripper.py +++ b/index_ripper.py @@ -28,6 +28,7 @@ def _configure_tk_libraries() -> None: """Set Tcl/Tk library env vars for uv-managed Python when missing.""" if os.environ.get("TCL_LIBRARY") and os.environ.get("TK_LIBRARY"): return + for prefix in (sys.base_prefix, sys.prefix): lib_root = os.path.join(prefix, "lib") tcl_library = os.path.join(lib_root, "tcl8.6") @@ -61,7 +62,15 @@ def _configure_tk_libraries() -> None: shorten_path, ) from settings_store import load_settings, save_settings -from ui_theme import apply_app_theme, configure_treeview_style, treeview_tag_colors +from ui_theme import ( + action_button_style_name, + apply_app_theme, + apply_bootstrap_theme, + configure_action_button_styles, + configure_treeview_style, + treeview_tag_colors, + ui_tokens, +) class WebsiteCopier: @@ -81,6 +90,9 @@ def __init__(self): self.window.title("Index Ripper") self.window.geometry("1200x900") self.window.minsize(900, 650) + apply_bootstrap_theme("flatly") + configure_action_button_styles(self.window, ctk, ttk) + self.ui_tokens = ui_tokens() self.backend = Backend(self) @@ -112,25 +124,30 @@ def __init__(self): self.window.grid_rowconfigure(1, weight=1) # --- URL and filter area --- - self.top_frame = ctk.CTkFrame(self.window) + self.top_frame = ctk.CTkFrame(self.window, corner_radius=14, border_width=1) self.top_frame.grid(row=0, column=0, padx=10, pady=(10, 5), sticky="ew") self.top_frame.grid_columnconfigure(0, weight=1) + ctk.CTkLabel( + self.top_frame, + text="Index Ripper Studio", + font=ctk.CTkFont(size=20, weight="bold"), + ).grid(row=0, column=0, padx=12, pady=(10, 2), sticky="w") # URL input url_input_frame = ctk.CTkFrame(self.top_frame, fg_color="transparent") - url_input_frame.grid(row=0, column=0, padx=10, pady=5, sticky="ew") + url_input_frame.grid(row=1, column=0, padx=10, pady=5, sticky="ew") url_input_frame.grid_columnconfigure(1, weight=1) ctk.CTkLabel( url_input_frame, text="URL:", font=ctk.CTkFont(weight="bold") ).grid(row=0, column=0, padx=(0, 5)) self.url_entry = ctk.CTkEntry( - url_input_frame, placeholder_text="https://example.com" + url_input_frame, placeholder_text="https://example.com", height=36 ) self.url_entry.grid(row=0, column=1, sticky="ew") # Scan buttons scan_buttons_frame = ctk.CTkFrame(self.top_frame, fg_color="transparent") - scan_buttons_frame.grid(row=0, column=1, padx=10, pady=5) + scan_buttons_frame.grid(row=1, column=1, padx=10, pady=5) self.status_chip = ctk.CTkLabel( self.top_frame, @@ -139,31 +156,38 @@ def __init__(self): text_color=("#14532D", "#DCFCE7"), corner_radius=12, padx=10, - pady=2, + pady=4, ) - self.status_chip.grid(row=0, column=2, padx=(0, 10), pady=5, sticky="e") + self.status_chip.grid(row=0, column=2, padx=(0, 10), pady=8, sticky="e") - self.scan_btn = ctk.CTkButton( - scan_buttons_frame, text="Scan", command=self.start_scan + self.scan_btn = ttk.Button( + scan_buttons_frame, + text="Scan", + command=self.start_scan, + style=action_button_style_name("primary"), ) self.scan_btn.pack(side="left", padx=5) - self.scan_pause_btn = ctk.CTkButton( + self.scan_pause_btn = ttk.Button( scan_buttons_frame, text="Pause Scan", command=self.toggle_scan_pause, state="disabled", + style=action_button_style_name("secondary"), ) self.scan_pause_btn.pack(side="left", padx=5) - self.clear_scan_btn = ctk.CTkButton( - scan_buttons_frame, text="Clear", command=self.clear_scan_results + self.clear_scan_btn = ttk.Button( + scan_buttons_frame, + text="Clear", + command=self.clear_scan_results, + style=action_button_style_name("danger"), ) self.clear_scan_btn.pack(side="left", padx=5) # --- File type filter area --- - filter_frame = ctk.CTkFrame(self.top_frame) - filter_frame.grid(row=1, column=0, columnspan=2, padx=10, pady=5, sticky="ew") + filter_frame = ctk.CTkFrame(self.top_frame, corner_radius=10) + filter_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky="ew") filter_frame.grid_columnconfigure(0, weight=1) ctk.CTkLabel( @@ -191,7 +215,7 @@ def __init__(self): ).pack(side="left", padx=2) # --- File list area --- - self.tree_frame = ctk.CTkFrame(self.window) + self.tree_frame = ctk.CTkFrame(self.window, corner_radius=14, border_width=1) self.tree_frame.grid(row=1, column=0, padx=10, pady=5, sticky="nsew") self.tree_frame.grid_rowconfigure(1, weight=1) self.tree_frame.grid_columnconfigure(0, weight=1) @@ -205,7 +229,11 @@ def __init__(self): self.search_var = ctk.StringVar() self.search_var.trace_add("write", self.on_search_filter_changed) self.search_entry = ctk.CTkEntry( - search_frame, textvariable=self.search_var, state="disabled" + search_frame, + textvariable=self.search_var, + state="disabled", + placeholder_text="Filter by file or folder name...", + height=32, ) self.search_entry.grid(row=0, column=1, sticky="ew") self.full_tree_backup = {} @@ -214,7 +242,7 @@ def __init__(self): self.tree = ttk.Treeview( self.tree_frame, - selectmode="none", + selectmode="browse", show=("tree", "headings"), style="Treeview", ) @@ -230,7 +258,7 @@ def __init__(self): self.tree.heading("size", text="Size", command=lambda: self.sort_tree("size")) self.tree.heading("type", text="Type", command=lambda: self.sort_tree("type")) - self.tree.column("#0", minwidth=400, width=600) + self.tree.column("#0", minwidth=300, width=600, stretch=True) self.tree.column("size", width=120, anchor="center") self.tree.column("type", width=200, anchor="center") @@ -256,36 +284,49 @@ def __init__(self): self.folders = {} self.scrollbar = ctk.CTkScrollbar(self.tree_frame, command=self.tree.yview) - self.tree.configure(yscrollcommand=self.scrollbar.set) + self.h_scrollbar = ctk.CTkScrollbar( + self.tree_frame, orientation="horizontal", command=self.tree.xview + ) + self.tree.configure( + yscrollcommand=self.scrollbar.set, xscrollcommand=self.h_scrollbar.set + ) self.tree.grid(row=1, column=0, sticky="nsew") self.scrollbar.grid(row=1, column=1, sticky="ns") + self.h_scrollbar.grid(row=2, column=0, sticky="ew") self.tree.bind("", self.on_tree_click) + self.tree.bind("", self.on_tree_space) + self.tree.bind("", self.on_tree_enter) + self.tree.bind("", self.on_tree_select_all) + self.tree.bind("", self.on_tree_select_all) # --- Download control area --- - control_frame = ctk.CTkFrame(self.window) + control_frame = ctk.CTkFrame(self.window, corner_radius=14, border_width=1) control_frame.grid(row=2, column=0, padx=10, pady=5, sticky="ew") - self.download_btn = ctk.CTkButton( + self.download_btn = ttk.Button( control_frame, text="Download Selected Files", command=self.download_selected, + style=action_button_style_name("success"), ) self.download_btn.pack(side="left", padx=5, pady=5) - self.pause_btn = ctk.CTkButton( + self.pause_btn = ttk.Button( control_frame, text="Pause Download", command=self.toggle_pause, state="disabled", + style=action_button_style_name("secondary"), ) self.pause_btn.pack(side="left", padx=5, pady=5) - self.path_btn = ctk.CTkButton( + self.path_btn = ttk.Button( control_frame, text="Choose Download Location", command=self.choose_download_path, + style=action_button_style_name("secondary"), ) self.path_btn.pack(side="left", padx=5, pady=5) @@ -299,21 +340,22 @@ def __init__(self): values=[str(i) for i in range(1, 11)], variable=self.threads_var, command=self.update_thread_count, + **self.ui_tokens["option_menu"], ) threads_option_menu.pack(side="left", padx=5) # Toggle button to show/hide bottom panels (Downloads/Logs) self.panels_visible = True - self.toggle_panels_btn = ctk.CTkButton( + self.toggle_panels_btn = ttk.Button( control_frame, text="Hide Panels", - width=110, command=self.toggle_panels, + style=action_button_style_name("secondary"), ) self.toggle_panels_btn.pack(side="right", padx=5) # --- Progress bar --- - self.progress_frame = ctk.CTkFrame(self.window) + self.progress_frame = ctk.CTkFrame(self.window, corner_radius=14, border_width=1) self.progress_frame.grid(row=3, column=0, padx=10, pady=(5, 10), sticky="ew") self.progress_frame.grid_columnconfigure(0, weight=1) @@ -328,7 +370,9 @@ def __init__(self): self.progress_label.grid(row=1, column=0, padx=5, pady=2, sticky="w") # --- Panels (Downloads + Logs) inside a Tabview --- - self.panels_tabview = ctk.CTkTabview(self.window) + self.panels_tabview = ctk.CTkTabview( + self.window, corner_radius=14, **self.ui_tokens["tabview"] + ) self.panels_tabview.grid(row=4, column=0, padx=10, pady=(0, 10), sticky="ew") self.panels_tabview.add("Downloads") @@ -339,7 +383,13 @@ def __init__(self): self.downloads_frame.pack(fill="x", expand=False) logs_tab = self.panels_tabview.tab("Logs") - self.log_text = ctk.CTkTextbox(logs_tab, height=80) + self.log_text = ctk.CTkTextbox( + logs_tab, + height=self.ui_tokens["logs"]["height"], + font=self.ui_tokens["logs"]["font"], + text_color=self.ui_tokens["logs"]["text_color"], + fg_color=self.ui_tokens["logs"]["fg_color"], + ) self.log_text.pack(fill="x", expand=False) self.log_text.configure(state="disabled") @@ -349,7 +399,9 @@ def __init__(self): pass # Track per-file download UI elements and cancel events - self.downloads_panel = DownloadsPanel(self.downloads_frame, ctk, tk, threading) + self.downloads_panel = DownloadsPanel( + self.downloads_frame, ctk, tk, threading, tokens=self.ui_tokens + ) # Load and apply saved settings (panels visibility and active tab) self._settings_path = os.path.join( @@ -426,6 +478,34 @@ def notify_error(self, title: str, message: str) -> None: except Exception: self.log_message(f"[ERROR] {title}: {message}") + def _set_status(self, text: str, state: str = "ready") -> None: + """Set status chip text and style based on scan state.""" + status_styles = { + "ready": { + "fg_color": ("#DCFCE7", "#14532D"), + "text_color": ("#14532D", "#DCFCE7"), + }, + "scanning": { + "fg_color": ("#DBEAFE", "#1E3A8A"), + "text_color": ("#1E3A8A", "#DBEAFE"), + }, + "paused": { + "fg_color": ("#FEF3C7", "#78350F"), + "text_color": ("#78350F", "#FEF3C7"), + }, + "stopping": { + "fg_color": ("#FEE2E2", "#7F1D1D"), + "text_color": ("#7F1D1D", "#FEE2E2"), + }, + } + + style = status_styles.get(state, status_styles["ready"]) + self.status_chip.configure( + text=text, + fg_color=style["fg_color"], + text_color=style["text_color"], + ) + def toggle_panels(self): """Show or hide the bottom panels (Downloads/Logs).""" if self.panels_visible: @@ -474,11 +554,11 @@ def toggle_scan_pause(self): self.scan_pause_event.clear() self.scan_pause_btn.configure(text="Resume Scan") self.progress_label.configure(text="Scan paused") - self.status_chip.configure(text="Scan Paused") + self._set_status("Scan Paused", "paused") else: self.scan_pause_event.set() self.scan_pause_btn.configure(text="Pause Scan") - self.status_chip.configure(text="Scanning") + self._set_status("Scanning", "scanning") def filter_file_type(self, file_name): """Check if file should be displayed""" @@ -563,6 +643,9 @@ def on_tree_click(self, event): if not item: return + self.tree.focus(item) + self.tree.selection_set(item) + # Identify the specific element clicked element = self.tree.identify_element(event.x, event.y) @@ -576,6 +659,43 @@ def on_tree_click(self, event): if column == "#0": # Only toggle check if name column is clicked self.toggle_check(item) + def _focused_tree_item(self): + item = self.tree.focus() + if item and self.tree.exists(item): + return item + selected = self.tree.selection() + if selected: + candidate = selected[0] + if self.tree.exists(candidate): + return candidate + return "" + + def on_tree_space(self, _event=None): + """Toggle checked state for the focused tree item.""" + item = self._focused_tree_item() + if item: + self.toggle_check(item) + return "break" + return None + + def on_tree_enter(self, _event=None): + """Expand/collapse folders or toggle files for the focused tree item.""" + item = self._focused_tree_item() + if not item: + return None + + tags = self.tree.item(item, "tags") + if "folder" in tags: + self.tree.item(item, open=not bool(self.tree.item(item, "open"))) + else: + self.toggle_check(item) + return "break" + + def on_tree_select_all(self, _event=None): + """Select all files in tree when tree widget has focus.""" + self.select_all() + return "break" + def toggle_check(self, item, force_check=None): """Toggle item checked status""" if not self.tree.exists(item): @@ -646,7 +766,7 @@ def _on_scan_started_ui(self, _url): self.scan_pause_btn.configure(state="normal", text="Pause Scan") self.progress_label.configure(text="Scanning website...") self.scan_btn.configure(state="normal", text="Stop Scan") - self.status_chip.configure(text="Scanning") + self._set_status("Scanning", "scanning") self._cancel_scan_item_flush() while not self.scan_item_buffer.empty(): @@ -938,6 +1058,7 @@ def redraw_file_type_filters(self): text=f"{file_type} ({count})", variable=var, command=self.apply_filters, + **self.ui_tokens["checkbox"], ) chk.grid(row=i // 4, column=i % 4, padx=5, pady=2, sticky="w") @@ -1207,7 +1328,7 @@ def start_scan(self): self.backend.should_stop = True self.scan_btn.configure(text="Stopping...") self.scan_btn.configure(state="disabled") - self.status_chip.configure(text="Stopping") + self._set_status("Stopping", "stopping") return self.backend.should_stop = False @@ -1218,7 +1339,7 @@ def start_scan(self): self.download_path = os.path.join(os.getcwd(), "downloads") self.progress_label.configure(text="Preparing to scan...") - self.status_chip.configure(text="Scanning") + self._set_status("Scanning", "scanning") self.scan_btn.configure(text="Stop Scan") self.search_entry.configure(state="disabled") self.search_var.set("") @@ -1235,7 +1356,7 @@ def scan_completed(self): "text": self.tree.item(item, "text"), "tags": self.tree.item(item, "tags"), } - self.status_chip.configure(text="Ready") + self._set_status("Ready", "ready") def focus_search(self, _event=None): if self.search_entry.cget("state") != "disabled": diff --git a/index_ripper_ttkb.py b/index_ripper_ttkb.py index 82e6c5e..ca78d2d 100644 --- a/index_ripper_ttkb.py +++ b/index_ripper_ttkb.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os import sys @@ -17,6 +18,25 @@ raise SystemExit(0) +def _configure_tk_libraries() -> None: + if os.environ.get("TCL_LIBRARY") and os.environ.get("TK_LIBRARY"): + return + + for prefix in (sys.base_prefix, sys.prefix): + lib_root = os.path.join(prefix, "lib") + tcl_library = os.path.join(lib_root, "tcl8.6") + tk_library = os.path.join(lib_root, "tk8.6") + if os.path.isfile(os.path.join(tcl_library, "init.tcl")) and os.path.isfile( + os.path.join(tk_library, "tk.tcl") + ): + os.environ.setdefault("TCL_LIBRARY", tcl_library) + os.environ.setdefault("TK_LIBRARY", tk_library) + return + + +_configure_tk_libraries() + + def main() -> int: if "--ui-smoke" in sys.argv: from ui_ttkb import WebsiteCopierTtkb diff --git a/ui_downloads.py b/ui_downloads.py index 0f809c3..78a0806 100644 --- a/ui_downloads.py +++ b/ui_downloads.py @@ -1,6 +1,19 @@ import threading +def download_status_state(text: str) -> str: + lower = (text or "").lower() + if "fail" in lower: + return "error" + if "complete" in lower: + return "success" + if "cancel" in lower: + return "warning" + if "download" in lower: + return "active" + return "queued" + + class DownloadsPanel: def __init__(self, parent_frame, ctk, tk, threading_module=None, tokens=None): self.parent_frame = parent_frame @@ -8,6 +21,17 @@ def __init__(self, parent_frame, ctk, tk, threading_module=None, tokens=None): self.tk = tk self.threading = threading_module or threading self._items = {} + self.tokens = tokens or {} + downloads_tokens = self.tokens.get("downloads", {}) + self.status_colors = downloads_tokens.get("status_colors", {}) + + def _set_status_text(self, item, text: str) -> None: + state = download_status_state(text) + status_kwargs = {"text": text} + color = self.status_colors.get(state) + if color is not None: + status_kwargs["text_color"] = color + item["status"].configure(**status_kwargs) def ensure(self, file_path: str, display_name: str): try: @@ -15,10 +39,20 @@ def ensure(self, file_path: str, display_name: str): if item: return item["cancel_event"] - row = self.ctk.CTkFrame(self.parent_frame) + downloads_tokens = self.tokens.get("downloads", {}) + row = self.ctk.CTkFrame( + self.parent_frame, + fg_color=downloads_tokens.get("row_fg_color"), + corner_radius=downloads_tokens.get("row_corner_radius", 8), + border_width=downloads_tokens.get("row_border_width", 0), + ) row.pack(fill="x", padx=5, pady=4) - name_label = self.ctk.CTkLabel(row, text=display_name) + name_label = self.ctk.CTkLabel( + row, + text=display_name, + text_color=downloads_tokens.get("name_text_color"), + ) name_label.pack(side="left", padx=5) progress_bar_widget = self.ctk.CTkProgressBar(row) @@ -33,12 +67,19 @@ def ensure(self, file_path: str, display_name: str): def do_cancel(): cancel_event.set() try: - status.configure(text="Canceling...") + self._set_status_text(self._items[file_path], "Canceling...") except self.tk.TclError: pass + cancel_btn_tokens = downloads_tokens.get("cancel_button", {}) cancel_btn = self.ctk.CTkButton( - row, text="Cancel", width=80, command=do_cancel + row, + text="Cancel", + command=do_cancel, + width=cancel_btn_tokens.get("width", 80), + height=cancel_btn_tokens.get("height", 28), + fg_color=cancel_btn_tokens.get("fg_color"), + hover_color=cancel_btn_tokens.get("hover_color"), ) cancel_btn.pack(side="right", padx=5) @@ -49,6 +90,7 @@ def do_cancel(): "status": status, "cancel_event": cancel_event, } + self._set_status_text(self._items[file_path], "Queued") return cancel_event except self.tk.TclError: return self.threading.Event() @@ -59,7 +101,7 @@ def set_progress(self, file_path: str, progress: float) -> None: if not item: return item["bar"].set(progress / 100) - item["status"].configure(text=f"Downloading {progress:.1f}%") + self._set_status_text(item, f"Downloading {progress:.1f}%") except self.tk.TclError: pass @@ -68,6 +110,6 @@ def set_status(self, file_path: str, text: str) -> None: item = self._items.get(file_path) if not item: return - item["status"].configure(text=text) + self._set_status_text(item, text) except self.tk.TclError: pass diff --git a/ui_theme.py b/ui_theme.py index 87ad1e5..4279cb1 100644 --- a/ui_theme.py +++ b/ui_theme.py @@ -3,7 +3,110 @@ def apply_app_theme(ctk): ctk.set_appearance_mode("system") - ctk.set_default_color_theme("green") + ctk.set_default_color_theme("blue") + + +def ui_tokens(): + return { + "option_menu": { + "height": 34, + "fg_color": ("#E2E8F0", "#334155"), + "button_color": ("#CBD5E1", "#475569"), + "button_hover_color": ("#94A3B8", "#64748B"), + "text_color": ("#0F172A", "#F8FAFC"), + "dropdown_fg_color": ("#FFFFFF", "#1E293B"), + "dropdown_text_color": ("#0F172A", "#F1F5F9"), + "dropdown_hover_color": ("#E2E8F0", "#334155"), + }, + "tabview": { + "segmented_button_fg_color": ("#E2E8F0", "#334155"), + "segmented_button_selected_color": ("#2563EB", "#3B82F6"), + "segmented_button_selected_hover_color": ("#1D4ED8", "#2563EB"), + "segmented_button_unselected_color": ("#CBD5E1", "#475569"), + "segmented_button_unselected_hover_color": ("#94A3B8", "#64748B"), + "text_color": ("#0F172A", "#F8FAFC"), + }, + "checkbox": { + "checkbox_width": 20, + "checkbox_height": 20, + "corner_radius": 6, + "border_width": 2, + "border_color": ("#94A3B8", "#64748B"), + "fg_color": ("#2563EB", "#3B82F6"), + "hover_color": ("#1D4ED8", "#2563EB"), + "checkmark_color": ("#FFFFFF", "#FFFFFF"), + "text_color": ("#0F172A", "#E2E8F0"), + }, + "logs": { + "height": 100, + "font": ("SF Mono", 12), + "text_color": ("#0F172A", "#E2E8F0"), + "fg_color": ("#F8FAFC", "#111827"), + }, + "downloads": { + "row_fg_color": ("#FFFFFF", "#1F2937"), + "row_border_width": 1, + "row_corner_radius": 10, + "name_text_color": ("#0F172A", "#E2E8F0"), + "status_colors": { + "queued": ("#475569", "#94A3B8"), + "active": ("#1D4ED8", "#60A5FA"), + "warning": ("#B45309", "#F59E0B"), + "success": ("#047857", "#34D399"), + "error": ("#B91C1C", "#F87171"), + }, + "cancel_button": { + "width": 88, + "height": 30, + "fg_color": "#DC2626", + "hover_color": "#B91C1C", + }, + }, + } + + +def action_button_style_name(kind: str) -> str: + mapping = { + "primary": "Primary.TButton", + "secondary": "Secondary.TButton", + "danger": "Danger.TButton", + "success": "Success.TButton", + } + return mapping.get(kind, "Secondary.TButton") + + +def configure_action_button_styles(window, ctk, ttk): + style = ttk.Style() + text_light = "#F8FAFC" + fg = { + "primary": window._apply_appearance_mode(("#2563EB", "#3B82F6")), + "secondary": window._apply_appearance_mode(("#334155", "#475569")), + "danger": window._apply_appearance_mode(("#DC2626", "#EF4444")), + "success": window._apply_appearance_mode(("#059669", "#10B981")), + } + active = { + "primary": window._apply_appearance_mode(("#1D4ED8", "#2563EB")), + "secondary": window._apply_appearance_mode(("#1E293B", "#334155")), + "danger": window._apply_appearance_mode(("#B91C1C", "#DC2626")), + "success": window._apply_appearance_mode(("#047857", "#059669")), + } + for kind in ("primary", "secondary", "danger", "success"): + style_name = action_button_style_name(kind) + style.configure( + style_name, + padding=(12, 7), + foreground=text_light, + background=fg[kind], + borderwidth=0, + focusthickness=1, + focuscolor=active[kind], + font=("SF Pro Text", 11, "bold"), + ) + style.map( + style_name, + background=[("active", active[kind]), ("disabled", "#6B7280")], + foreground=[("disabled", "#E5E7EB")], + ) def ui_tokens(): diff --git a/ui_ttkb.py b/ui_ttkb.py index 11b55e7..bbf3d8d 100644 --- a/ui_ttkb.py +++ b/ui_ttkb.py @@ -19,11 +19,18 @@ from backend import Backend +def should_skip_file_row(existing_entry) -> bool: + """Return True when a file row is already fully registered.""" + return existing_entry is not None + + class WebsiteCopierTtkb: USER_AGENT = "IndexRipper/2.0" def __init__(self, ui_smoke: bool = False): self._ui_smoke = bool(ui_smoke) + self.debug_enabled = os.environ.get("INDEX_RIPPER_DEBUG", "0") != "0" + self.use_modal_dialogs = os.environ.get("INDEX_RIPPER_MODAL_DIALOGS", "0") == "1" self.window = ttkb.Window(title="Index Ripper", themename="flatly") self.window.geometry("1200x900") @@ -45,6 +52,7 @@ def __init__(self, ui_smoke: bool = False): self.files_dict: dict[str, dict] = {} self.folders: dict[str, str] = {} self.checked_items: set[str] = set() + self.checkbox_checked = "✔ " self.dir_queue = Queue() self.file_queue = Queue() @@ -54,9 +62,11 @@ def __init__(self, ui_smoke: bool = False): self.scan_flush_interval_ms = 16 self.scan_flush_batch_size = 200 self.scan_flush_job = None + self._last_logged_queue_size = None self.file_types: dict[str, tk.BooleanVar] = {} self.file_type_counts: dict[str, int] = {} + self.file_type_widgets: dict[str, tk.Checkbutton] = {} self.download_path = "" self.download_queue = Queue() @@ -87,6 +97,12 @@ def __init__(self, ui_smoke: bool = False): self.window.bind("", self.focus_search) self.window.bind("", self.focus_logs) self.window.bind("", self.clear_search) + self.window.bind_all("", self._on_global_url_paste, add="+") + self.window.bind_all("", self._on_global_url_paste, add="+") + self.window.bind_all("", self._on_global_url_paste, add="+") + self.window.bind_all("", self._on_global_url_paste, add="+") + self.window.bind_all("<>", self._on_global_url_paste, add="+") + self._debug("WebsiteCopierTtkb initialized") def _build_ui(self) -> None: if self._ui_smoke: @@ -102,8 +118,18 @@ def _build_ui(self) -> None: ttkb.Label(header, text="URL").grid(row=0, column=0, sticky="w") self.url_var = tk.StringVar() - self.url_entry = ttkb.Entry(header, textvariable=self.url_var) + self.url_entry = tk.Entry(header, textvariable=self.url_var, relief="flat") self.url_entry.grid(row=0, column=1, sticky="ew", padx=(8, 8)) + self.url_entry.bind("", self._on_url_paste) + self.url_entry.bind("", self._on_url_paste) + self.url_entry.bind("", self._on_url_paste) + self.url_entry.bind("", self._on_url_paste) + self.url_entry.bind("", self._on_url_paste) + self.url_entry.bind("", self._show_url_context_menu) + self.url_entry.bind("", self._show_url_context_menu) + + self.url_context_menu = tk.Menu(self.window, tearoff=0) + self.url_context_menu.add_command(label="Paste", command=self._paste_into_url_entry) self.status_label = ttkb.Label(header, text="Ready", bootstyle="success") self.status_label.grid(row=0, column=2, sticky="e", padx=(0, 8)) @@ -135,6 +161,20 @@ def _build_ui(self) -> None: self.filters_frame, height=85, ) + type_actions = ttkb.Frame(self.filters_frame) + type_actions.pack(fill=X, pady=(4, 0)) + ttkb.Button( + type_actions, + text="Select All Types", + bootstyle="secondary-outline", + command=self.select_all_types, + ).pack(side=LEFT, padx=(0, 6)) + ttkb.Button( + type_actions, + text="Deselect All Types", + bootstyle="secondary-outline", + command=self.deselect_all_types, + ).pack(side=LEFT) controls = ttkb.Frame(filters_and_controls) controls.grid(row=0, column=1, sticky="e", padx=(10, 0)) @@ -160,18 +200,40 @@ def _build_ui(self) -> None: command=self.choose_download_path, ) self.path_btn.grid(row=0, column=2, padx=4) + self.panels_visible = True + self.toggle_panels_btn = ttkb.Button( + controls, + text="Hide Panels", + bootstyle="secondary-outline", + command=self.toggle_panels, + ) + self.toggle_panels_btn.grid(row=0, column=5, padx=(10, 0)) ttkb.Label(controls, text="Threads").grid(row=0, column=3, padx=(12, 4)) self.threads_var = tk.StringVar(value="5") - self.threads_combo = ttkb.Combobox( - controls, - values=[str(i) for i in range(1, 11)], - textvariable=self.threads_var, - width=4, - state="readonly", - ) + try: + self.threads_combo = ttkb.Combobox( + controls, + values=[str(i) for i in range(1, 11)], + textvariable=self.threads_var, + width=4, + state="readonly", + ) + self.threads_combo.bind("<>", self.update_thread_count) + except (tk.TclError, TypeError): + # Fallback when ttkbootstrap arrow assets fail to initialize. + self.threads_combo = tk.Spinbox( + controls, + from_=1, + to=10, + textvariable=self.threads_var, + width=4, + command=self.update_thread_count, + relief="flat", + ) + self.threads_combo.bind("", self.update_thread_count) + self.threads_combo.bind("", self.update_thread_count) self.threads_combo.grid(row=0, column=4) - self.threads_combo.bind("<>", self.update_thread_count) self.tree_frame = ttkb.Frame(self.window, padding=(10, 0, 10, 8)) self.tree_frame.grid(row=2, column=0, sticky="nsew") @@ -185,13 +247,14 @@ def _build_ui(self) -> None: self.search_var = tk.StringVar() self.search_entry = ttkb.Entry(search_bar, textvariable=self.search_var) self.search_entry.grid(row=0, column=1, sticky="ew") - self.search_var.trace_add("write", lambda *_: self.apply_search_filter()) + self.search_var.trace_add("write", self.on_search_filter_changed) self.tree = ttkb.Treeview( self.tree_frame, - columns=("size", "type"), + columns=("size", "type", "full_path"), show="tree headings", bootstyle="info", + selectmode="extended", ) self.tree.heading("#0", text="Path", command=lambda: self.sort_tree("#0")) self.tree.heading("size", text="Size", command=lambda: self.sort_tree("size")) @@ -199,7 +262,21 @@ def _build_ui(self) -> None: self.tree.column("#0", width=600, stretch=True) self.tree.column("size", width=120, stretch=False, anchor="e") self.tree.column("type", width=240, stretch=False) + self.tree.column("full_path", width=0, stretch=False, anchor="w") self.tree.grid(row=1, column=0, sticky="nsew") + style = ttkb.Style() + style.configure("Treeview", font=("SF Pro Text", 14), rowheight=34) + style.configure("Treeview.Heading", font=("SF Pro Text", 13, "bold")) + self.tree.tag_configure("checked", foreground="#0F766E") + self.tree.bind("", self._on_tree_select_all) + self.tree.bind("", self._on_tree_select_all) + self.tree.bind("", self.on_tree_click) + self.tree.bind("", self.show_context_menu) + self.tree.bind("", self.show_context_menu) + self.tree.bind("", self.show_context_menu) + self.tree.bind("", self.on_tree_drag_select) + self.tree.bind("", self.on_tree_space) + self.tree.bind("", self.on_tree_enter) self.tree_scroll = tk.Scrollbar(self.tree_frame, orient="vertical", command=self.tree.yview) self.tree.configure(yscrollcommand=self.tree_scroll.set) @@ -246,6 +323,13 @@ def _build_ui(self) -> None: self.download_items: dict[str, dict] = {} self.sort_reverse = False self.full_tree_backup = {} + self.drag_anchor_item = "" + self.context_menu = tk.Menu(self.window, tearoff=0) + self.context_menu.add_command(label="Select All", command=self.select_all) + self.context_menu.add_command(label="Deselect All", command=self.deselect_all) + self.context_menu.add_separator() + self.context_menu.add_command(label="Expand All", command=self.expand_all) + self.context_menu.add_command(label="Collapse All", command=self.collapse_all) def _build_ui_smoke_only(self) -> None: self.window.columnconfigure(0, weight=1) @@ -275,18 +359,27 @@ def _build_ui_smoke_only(self) -> None: self.logs_tab = None def notify_info(self, title: str, message: str) -> None: + if not self.use_modal_dialogs: + self.log_message(f"[INFO] {title}: {message}") + return try: messagebox.showinfo(title, message) except Exception: self.log_message(f"[INFO] {title}: {message}") def notify_warning(self, title: str, message: str) -> None: + if not self.use_modal_dialogs: + self.log_message(f"[WARN] {title}: {message}") + return try: messagebox.showwarning(title, message) except Exception: self.log_message(f"[WARN] {title}: {message}") def notify_error(self, title: str, message: str) -> None: + if not self.use_modal_dialogs: + self.log_message(f"[ERROR] {title}: {message}") + return try: messagebox.showerror(title, message) except Exception: @@ -299,6 +392,21 @@ def log_message(self, message: str) -> None: except tk.TclError: pass + def _debug(self, message: str) -> None: + if not self.debug_enabled: + return + line = f"[DEBUG] {message}" + try: + print(line) + except Exception: + pass + try: + if hasattr(self, "log_text"): + self.log_text.insert(END, line + "\n") + self.log_text.see(END) + except Exception: + pass + def _set_status(self, text: str, style: str = "secondary") -> None: try: self.status_label.configure(text=text, bootstyle=style) @@ -310,11 +418,52 @@ def choose_download_path(self) -> None: if path: self.download_path = path + def _on_url_paste(self, _event=None): + """Paste clipboard into URL field exactly once.""" + self._debug("URL paste handler invoked") + self._paste_into_url_entry() + return "break" + + def _on_global_url_paste(self, _event=None): + try: + focused = self.window.focus_get() + except tk.TclError: + return None + if focused is self.url_entry: + self._debug("Global paste routed to URL entry") + self._paste_into_url_entry() + return "break" + return None + + def _paste_into_url_entry(self): + try: + text = self.window.clipboard_get() + except tk.TclError: + return + if not text: + return + try: + if self.url_entry.selection_present(): + self.url_entry.delete("sel.first", "sel.last") + except tk.TclError: + pass + self.url_entry.insert("insert", text) + self._debug(f"Pasted URL text length={len(text)}") + + def _show_url_context_menu(self, event): + try: + self.url_entry.focus_set() + self.url_context_menu.tk_popup(event.x_root, event.y_root) + finally: + self.url_context_menu.grab_release() + return "break" + def update_thread_count(self, _event=None) -> None: try: - self.max_workers = int(self.threads_var.get()) + self.max_workers = max(1, min(10, int(self.threads_var.get()))) except ValueError: self.max_workers = 5 + self.threads_var.set(str(self.max_workers)) self.executor.shutdown(wait=False, cancel_futures=True) self.executor = ThreadPoolExecutor(max_workers=self.max_workers) @@ -428,8 +577,6 @@ def on_scan_item(self, **payload): self._run_on_ui_thread(self._on_scan_item_ui, payload) def _on_scan_item_ui(self, payload: dict): - if not self.is_scanning: - return self.scan_item_buffer.put( ( bool(payload.get("is_directory")), @@ -446,7 +593,7 @@ def _on_scan_item_ui(self, payload: dict): def _schedule_scan_item_flush(self): if self.scan_flush_job is not None: return - if not self.is_scanning: + if not self.is_scanning and self.scan_item_buffer.empty(): return self.scan_flush_job = self.window.after( self.scan_flush_interval_ms, self._flush_scan_item_buffer @@ -499,7 +646,7 @@ def _flush_scan_item_buffer(self, max_items=None, reschedule=True): if added_file and not self.is_processing_files: self.window.after(0, self.process_file_queue) - if reschedule and self.is_scanning and not self.scan_item_buffer.empty(): + if reschedule and not self.scan_item_buffer.empty(): self._schedule_scan_item_flush() def on_scan_finished(self, stopped): @@ -510,6 +657,10 @@ def _on_scan_finished_ui(self, stopped): self._cancel_scan_item_flush() while not self.scan_item_buffer.empty(): self._flush_scan_item_buffer(max_items=self.scan_flush_batch_size, reschedule=False) + while not self.dir_queue.empty(): + self.process_dir_queue() + while not self.file_queue.empty(): + self.process_file_queue() self.scan_btn.configure(text="Scan") self.scan_pause_btn.configure(state="disabled", text="Pause Scan") @@ -523,6 +674,13 @@ def _on_scan_finished_ui(self, stopped): self.progress_label.configure(text="Scan finished.") self._set_status("Ready", "success") self.log_message("[Scan] Finished.") + self.scan_completed() + tree_items = len(self._all_tree_items()) + with self.files_dict_lock: + file_entries = len(self.files_dict) + self._debug( + f"scan_finished stopped={stopped} files_dict={file_entries} tree_items={tree_items}" + ) def update_scan_progress(self): if self.total_urls <= 0: @@ -532,6 +690,10 @@ def update_scan_progress(self): self.progress_var.set(pct) self.progress_label.configure(text=f"Scan: {self.scanned_urls}/{self.total_urls}") + def scan_completed(self): + self.full_tree_backup.clear() + self._backup_full_tree() + def create_folder_structure(self, dir_path: str, url: str) -> str: if not dir_path: dir_path = "/" @@ -547,7 +709,14 @@ def create_folder_structure(self, dir_path: str, url: str) -> str: parent_id = existing continue - node_id = self.tree.insert(parent_id, "end", text=part, values=("", "dir"), tags=("folder",)) + node_id = self.tree.insert( + parent_id, + "end", + text=f"📁 {part}", + values=("", "dir", ""), + tags=("folder",), + open=True, + ) with self.folders_dict_lock: self.folders[current] = node_id parent_id = node_id @@ -569,6 +738,10 @@ def process_dir_queue(self): def process_file_queue(self): try: self.is_processing_files = True + qsize = self.file_queue.qsize() + if qsize != self._last_logged_queue_size and (qsize <= 5 or qsize % 100 == 0): + self._debug(f"process_file_queue queue_size={qsize}") + self._last_logged_queue_size = qsize if not self.file_queue.empty(): ( dir_path, @@ -586,34 +759,61 @@ def process_file_queue(self): except tk.TclError: self.is_processing_files = False - def _add_file_to_tree(self, dir_path, url, file_name, size, file_type, full_path): + def _add_file_to_tree(self, dir_path, url, file_name, size, file_type, full_path, from_cache=False): if not file_name: return + is_html_dir_like = ( + isinstance(file_type, str) + and "text/html" in file_type.lower() + and "." not in (file_name or "") + ) parent_id = self.create_folder_structure(dir_path, url) - with self.files_dict_lock: - if full_path in self.files_dict: - return - self.files_dict[full_path] = { - "url": url, - "file_name": file_name, - "size": size, - "file_type": file_type, - "path": dir_path, - } + if not from_cache: + with self.files_dict_lock: + existing_entry = self.files_dict.get(full_path) + if should_skip_file_row(existing_entry): + return + if is_html_dir_like: + self.files_dict.pop(full_path, None) + else: + self.files_dict[full_path] = { + "url": url, + "file_name": file_name, + "size": size, + "file_type": file_type, + "path": dir_path, + } + + if is_html_dir_like: + folder_path = f"{dir_path.rstrip('/')}/{file_name}".replace("//", "/") + self.create_folder_structure(folder_path, url) + self._debug(f"add_folder_like path={folder_path} from_url={url}") + return ext = normalize_extension(file_name) if not self.filter_file_type(file_name): return + icon, group = self._file_icon_and_group(file_name, file_type) + display_type = group if not file_type else f"{group} | {file_type}" node_id = self.tree.insert( parent_id, "end", - text=file_name, - values=(size or "", file_type or ""), + text=f"{icon} {file_name}", + values=(size or "", display_type, full_path or ""), tags=("file", ext), ) if full_path: - self.tree.set(node_id, "#0", file_name) + self.tree.set(node_id, "#0", f"{icon} {file_name}") + if full_path and full_path in self.checked_items: + self._set_item_checked_visual(node_id, True) + tags = list(self.tree.item(node_id, "tags")) + if "checked" not in tags: + tags.append("checked") + self.tree.item(node_id, tags=tuple(tags)) + self._debug( + f"add_file name={file_name} full_path={full_path} parent={parent_id or '/'} from_cache={from_cache}" + ) def filter_file_type(self, file_name: str) -> bool: ext = normalize_extension(file_name) @@ -625,21 +825,35 @@ def filter_file_type(self, file_name: str) -> bool: def update_file_types(self, file_name: str) -> None: ext = normalize_extension(file_name) self.file_type_counts[ext] = self.file_type_counts.get(ext, 0) + 1 - if ext in self.file_types: + if ext not in self.file_types: + self.file_types[ext] = tk.BooleanVar(value=True) + self.redraw_file_type_filters() return + widget = self.file_type_widgets.get(ext) + if widget is not None: + widget.configure(text=f"{ext} ({self.file_type_counts.get(ext, 0)})") - var = tk.BooleanVar(value=True) - self.file_types[ext] = var - cb = ttkb.Checkbutton( - self.filters_container, - text=f"{ext} ({self.file_type_counts[ext]})", - variable=var, - bootstyle="round-toggle", - command=self.apply_filters, - ) - cb.pack(side=LEFT, padx=6) + def redraw_file_type_filters(self): + for widget in self.filters_container.winfo_children(): + widget.destroy() + self.file_type_widgets.clear() + for ext in sorted(self.file_types.keys()): + cb = tk.Checkbutton( + self.filters_container, + text=f"{ext} ({self.file_type_counts.get(ext, 0)})", + variable=self.file_types[ext], + command=self.apply_filters, + anchor="w", + relief="flat", + highlightthickness=0, + padx=6, + pady=2, + ) + cb.pack(side=LEFT, padx=6) + self.file_type_widgets[ext] = cb def apply_filters(self) -> None: + before = len(self._all_tree_items()) for item in self.tree.get_children(""): self.tree.delete(item) with self.folders_dict_lock: @@ -653,7 +867,19 @@ def apply_filters(self) -> None: continue if not self.filter_file_type(file_name): continue - self._add_file_to_tree(info.get("path") or "", info.get("url") or "", file_name, info.get("size"), info.get("file_type"), full_path) + self._add_file_to_tree( + info.get("path") or "", + info.get("url") or "", + file_name, + info.get("size"), + info.get("file_type"), + full_path, + from_cache=True, + ) + after = len(self._all_tree_items()) + self._debug( + f"apply_filters files_dict={len(items)} visible_before={before} visible_after={after}" + ) def clear_scan_results(self) -> None: if self.is_scanning: @@ -668,6 +894,8 @@ def clear_scan_results(self) -> None: widget.destroy() self.file_types.clear() self.file_type_counts.clear() + self.file_type_widgets.clear() + self.checked_items.clear() self.progress_var.set(0) self.progress_label.configure(text="") self._set_status("Ready", "success") @@ -685,6 +913,8 @@ def start_scan(self) -> None: self.backend.should_stop = False self.backend.should_stop = False + self.search_var.set("") + self.full_tree_backup.clear() try: self.download_path = default_download_folder(url, os.getcwd()) except Exception: @@ -779,11 +1009,28 @@ def download_selected(self) -> None: selected = [] with self.files_dict_lock: - for full_path, info in self.files_dict.items(): - selected.append((full_path, info)) + for full_path in sorted(self.checked_items): + info = self.files_dict.get(full_path) + if info: + selected.append((full_path, info)) + if not selected: + selected_item_ids = list(self.tree.selection()) + for item_id in selected_item_ids: + try: + tags = self.tree.item(item_id, "tags") + except tk.TclError: + continue + if "file" not in tags: + continue + full_path = self.tree.set(item_id, "full_path") + if not full_path: + continue + info = self.files_dict.get(full_path) + if info: + selected.append((full_path, info)) if not selected: - self.notify_info("Info", "No files to download") + self.notify_info("Info", "No selected files to download") return self.pause_btn.configure(state="normal") @@ -814,6 +1061,9 @@ def apply_search_filter(self) -> None: self._backup_full_tree() self._filter_tree_by_term(term) + def on_search_filter_changed(self, *_): + self.apply_search_filter() + def _backup_full_tree(self) -> None: self.full_tree_backup.clear() for item in self._all_tree_items(): @@ -862,6 +1112,228 @@ def _all_tree_items(self): stack.extend(self.tree.get_children(item)) return out + def get_all_tree_items(self, parent=""): + if parent == "": + return self._all_tree_items() + out = [] + for child in self.tree.get_children(parent): + out.append(child) + out.extend(self.get_all_tree_items(child)) + return out + + def _get_ancestors(self, item): + ancestors = [] + try: + parent = self.tree.parent(item) + while parent: + ancestors.append(parent) + parent = self.tree.parent(parent) + except tk.TclError: + return [] + return ancestors + + def _strip_checkmark(self, text: str) -> str: + if text.startswith(self.checkbox_checked): + return text[len(self.checkbox_checked) :] + return text + + def _file_icon_and_group(self, file_name: str, file_type: str | None): + ext = normalize_extension(file_name) + mime = (file_type or "").lower() + if ext in (".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp", ".ico"): + return "🖼", "image" + if ext in (".md", ".txt", ".pdf", ".doc", ".docx", ".rtf"): + return "📄", "document" + if ext in (".zip", ".rar", ".7z", ".tar", ".gz", ".bz2", ".xz"): + return "🗜", "archive" + if ext in ( + ".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs", ".java", ".c", ".cpp", + ".h", ".hpp", ".json", ".yaml", ".yml", ".toml", ".xml", ".html", ".css", ".sh", + ): + return "💻", "code" + if "text/" in mime: + return "📄", "text" + if "image/" in mime: + return "🖼", "image" + if "audio/" in mime: + return "🎵", "audio" + if "video/" in mime: + return "🎬", "video" + return "📦", "binary" + + def _set_item_checked_visual(self, item: str, checked: bool) -> None: + try: + text = self.tree.item(item, "text") + except tk.TclError: + return + raw = self._strip_checkmark(text or "") + display = f"{self.checkbox_checked}{raw}" if checked else raw + try: + self.tree.item(item, text=display) + except tk.TclError: + return + + def _focused_tree_item(self): + try: + item = self.tree.focus() + if item and self.tree.exists(item): + return item + selected = self.tree.selection() + if selected: + return selected[0] + except tk.TclError: + return "" + return "" + + def toggle_check(self, item, force_check=None): + try: + if not self.tree.exists(item): + return + tags = list(self.tree.item(item, "tags")) + except tk.TclError: + return + + is_checked = "checked" in tags + new_checked = (not is_checked) if force_check is None else bool(force_check) + + if "file" in tags: + full_path = self.tree.set(item, "full_path") + if full_path: + if new_checked: + self.checked_items.add(full_path) + else: + self.checked_items.discard(full_path) + + if new_checked and "checked" not in tags: + tags.append("checked") + if (not new_checked) and "checked" in tags: + tags.remove("checked") + self.tree.item(item, tags=tuple(tags)) + self._set_item_checked_visual(item, new_checked) + + if "folder" in tags: + for child in self.tree.get_children(item): + self.toggle_check(child, force_check=new_checked) + + def on_tree_click(self, event): + try: + item = self.tree.identify("item", event.x, event.y) + if not item: + return + self.drag_anchor_item = item + # Let Treeview default bindings handle selection semantics (shift/cmd drag). + self.tree.focus(item) + element = self.tree.identify_element(event.x, event.y) + if "indicator" in element: + return + # Do not toggle check state when modifier-assisted multi-selection is active. + modifier_mask = 0x0001 | 0x0004 | 0x0008 | 0x0010 | 0x0080 + if event.state & modifier_mask: + return + if self.tree.identify_column(event.x) == "#0": + self.toggle_check(item) + except tk.TclError: + return + + def on_tree_drag_select(self, event): + try: + if not self.drag_anchor_item: + return + target = self.tree.identify_row(event.y) + if not target: + return + items = self._all_tree_items() + if self.drag_anchor_item not in items or target not in items: + return + a = items.index(self.drag_anchor_item) + b = items.index(target) + lo, hi = (a, b) if a <= b else (b, a) + self.tree.selection_set(items[lo : hi + 1]) + except tk.TclError: + return + + def on_tree_space(self, _event=None): + item = self._focused_tree_item() + if item: + self.toggle_check(item) + return "break" + return None + + def on_tree_enter(self, _event=None): + item = self._focused_tree_item() + if not item: + return None + try: + tags = self.tree.item(item, "tags") + if "folder" in tags: + self.tree.item(item, open=not bool(self.tree.item(item, "open"))) + else: + self.toggle_check(item) + except tk.TclError: + return None + return "break" + + def show_context_menu(self, event): + try: + item = self.tree.identify_row(event.y) + if item: + self.tree.focus(item) + self.tree.selection_set(item) + self.context_menu.tk_popup(event.x_root, event.y_root) + finally: + self.context_menu.grab_release() + return "break" + + def select_all(self): + for item in self._all_tree_items(): + self.toggle_check(item, force_check=True) + + def deselect_all(self): + for item in self._all_tree_items(): + self.toggle_check(item, force_check=False) + + def expand_all(self, parent=""): + for item in self.tree.get_children(parent): + self.tree.item(item, open=True) + self.expand_all(item) + + def collapse_all(self, parent=""): + for item in self.tree.get_children(parent): + self.tree.item(item, open=False) + self.collapse_all(item) + + def select_all_types(self): + for var in self.file_types.values(): + var.set(True) + self.apply_filters() + + def deselect_all_types(self): + for var in self.file_types.values(): + var.set(False) + self.apply_filters() + + def toggle_panels(self): + if self.panels_visible: + self.panels_notebook.grid_remove() + self.panels_visible = False + self.toggle_panels_btn.configure(text="Show Panels") + else: + self.panels_notebook.grid() + self.panels_visible = True + self.toggle_panels_btn.configure(text="Hide Panels") + + def _on_tree_select_all(self, _event=None): + self.select_all() + return "break" + + def on_tree_select_all(self, _event=None): + return self._on_tree_select_all(_event) + + def _safe_create_folder_and_add_file( + self, dir_path, url, file_name, size, file_type, full_path + ): + self._add_file_to_tree(dir_path, url, file_name, size, file_type, full_path) + def focus_search(self, _event=None): try: self.search_entry.focus_set() From 468ac39cb6ea370dd3f0f0489156c3b7c5cde273 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Mon, 9 Mar 2026 23:59:30 +0800 Subject: [PATCH 22/55] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=20index=5Frippe?= =?UTF-8?q?r.py=20=E6=AE=98=E7=95=99=E7=9A=84=20apply=5Fbootstrap=5Ftheme?= =?UTF-8?q?=20import=20=E8=88=87=E5=91=BC=E5=8F=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index_ripper.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/index_ripper.py b/index_ripper.py index a299f22..bcb8ff0 100644 --- a/index_ripper.py +++ b/index_ripper.py @@ -65,7 +65,6 @@ def _configure_tk_libraries() -> None: from ui_theme import ( action_button_style_name, apply_app_theme, - apply_bootstrap_theme, configure_action_button_styles, configure_treeview_style, treeview_tag_colors, @@ -90,7 +89,6 @@ def __init__(self): self.window.title("Index Ripper") self.window.geometry("1200x900") self.window.minsize(900, 650) - apply_bootstrap_theme("flatly") configure_action_button_styles(self.window, ctk, ttk) self.ui_tokens = ui_tokens() From 5e89fd8588e8574692b89e9d115b2f17843ceb45 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 16:09:47 +0800 Subject: [PATCH 23/55] docs: add FileTree redesign design doc (CTkScrollableFrame + emoji icons) --- .../2026-03-10-filetree-redesign-design.md | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 docs/plans/2026-03-10-filetree-redesign-design.md diff --git a/docs/plans/2026-03-10-filetree-redesign-design.md b/docs/plans/2026-03-10-filetree-redesign-design.md new file mode 100644 index 0000000..db5ca7f --- /dev/null +++ b/docs/plans/2026-03-10-filetree-redesign-design.md @@ -0,0 +1,107 @@ +# FileTree Redesign — Design Document + +**Date:** 2026-03-10 +**Status:** Approved + +## Goal + +Replace `ttk.Treeview` with a custom `FileTree` component built on `CTkScrollableFrame` + emoji icons, delivering a modern UI while preserving all existing functional interfaces. + +## Problem + +`ttk.Treeview` forces icons to be `tk.PhotoImage` pixel art (limited quality) and prevents per-row custom styling (hover, checked background, badges). The current pixel-art icons look like plain colored blocks. + +## Design + +### Architecture — three layers + +**1. Data model (pure Python, no widgets)** + +```python +@dataclass +class TreeNode: + node_id: str + parent_id: str # "" for root + name: str + kind: str # "folder" | "file" + full_path: str # "" for folders + size: str + file_type: str + icon_group: str # "folder" | "image" | "document" | ... + checked: bool + expanded: bool + children: list[str] # ordered child node_ids +``` + +```python +# Stored on WebsiteCopierCtk: +self.tree_nodes: dict[str, TreeNode] +self.tree_roots: list[str] # top-level node_ids in order +``` + +**2. View layer (CTkScrollableFrame)** + +- Container: `ctk.CTkScrollableFrame` replaces `ttk.Treeview` +- `_visible_nodes: list[str]` — flat ordered list of currently visible nodes +- `_row_widgets: dict[str, RowWidget]` — node_id → row frame (created lazily) +- `_rebuild_visible()` — recomputes `_visible_nodes` from data model (expand/collapse, filter) +- `_sync_rows()` — creates/removes row widgets to match `_visible_nodes` + +**3. Row widget (RowWidget)** + +Each row is a `ctk.CTkFrame` containing: +``` +[indent spacer] [▶/▼ or spacer] [emoji label] [name label] [size label] [type badge] +``` +- `indent` = `depth * 20` px spacer +- Expand/collapse button: 16×16 `ctk.CTkButton` with `▶`/`▼` text, no border +- Emoji icon: `ctk.CTkLabel` 22px font +- Name: `ctk.CTkLabel`, bold for folders +- Size/type: right-aligned small labels + +### Visual spec + +| State | Background | +|---|---| +| Normal | transparent (inherits CTkScrollableFrame) | +| Hover | `("#E2E8F0", "#2D3748")` | +| Checked | `("#DBEAFE", "#1E3A5F")` | +| Checked + Hover | `("#BFDBFE", "#1E40AF")` | + +Row height: 34px (via `height` on inner frame) +Font: SF Pro Text 13 (name), 11 (size/type) + +### Emoji icon mapping + +| group | emoji | +|---|---| +| folder | 📁 | +| image | 🖼️ | +| document | 📄 | +| archive | 🗜️ | +| code | 💻 | +| audio | 🎵 | +| video | 🎬 | +| text | 📝 | +| binary | ⚙️ | + +### Preserved public interfaces (no changes to callers) + +- `add_folder(dir_path, url) → str` +- `add_file(dir_path, url, file_name, size, file_type, full_path)` +- `toggle_check(item_id, force_check=None, _skip_children=False)` +- `select_all() / deselect_all()` +- `expand_all() / collapse_all()` +- `_backup_full_tree() / _restore_full_tree()` +- `_filter_tree_by_term(term)` +- `sort_tree(col)` +- `checked_items: set[str]` — still maintained + +### Implementation notes + +- `add_folder` / `add_file` still called from main thread (via `_poll_scan_queue`) +- `_rebuild_visible()` is O(n); call only on expand/collapse/filter, not per row +- `_sync_rows()` uses `grid()` / `grid_forget()` for show/hide (no widget destruction) +- Search filter: set `node.hidden = True` on non-matching nodes; `_rebuild_visible` skips them +- Backup/restore: serialise/deserialise `tree_nodes` + `tree_roots` dicts +- `checked_items` set is the source of truth for download; unchanged From c0b6127002ee04701f74f43cbff488b8622c403e Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 16:13:04 +0800 Subject: [PATCH 24/55] docs: add FileTree redesign implementation plan (9 tasks) --- docs/plans/2026-03-10-filetree-redesign.md | 960 +++++++++++++++++++++ 1 file changed, 960 insertions(+) create mode 100644 docs/plans/2026-03-10-filetree-redesign.md diff --git a/docs/plans/2026-03-10-filetree-redesign.md b/docs/plans/2026-03-10-filetree-redesign.md new file mode 100644 index 0000000..a5b33ac --- /dev/null +++ b/docs/plans/2026-03-10-filetree-redesign.md @@ -0,0 +1,960 @@ +# FileTree Redesign Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace `ttk.Treeview` with a custom `CTkScrollableFrame`-based tree that uses emoji icons and supports modern per-row styling (hover, checked background). + +**Architecture:** A `TreeNode` dataclass holds all state (no widgets). `_rebuild_visible()` computes the flat ordered list of visible nodes. `_sync_rows()` creates/updates `RowWidget` frames to match that list. All existing caller interfaces (`add_folder`, `add_file`, `toggle_check`, etc.) are preserved. + +**Tech Stack:** Python 3.11+, customtkinter 5.x, tkinter, `dataclasses` + +--- + +## Key files + +- **Modify:** `ui_ctk.py` — all changes are here +- **Remove usage of:** `configure_treeview_style`, `treeview_tag_colors` from `ui_theme.py` (imports only; functions stay in ui_theme.py for now) +- **Test:** `test_ui_ttkb.py` (smoke tests exist; run after each task to ensure nothing broken) + +## Baseline: run tests before starting + +```bash +uv run pytest test_ui_ttkb.py test_ui_downloads.py -v 2>&1 | tail -20 +``` + +--- + +### Task 1: Add `TreeNode` dataclass and data model state + +**Goal:** Pure-Python data model for the tree. No widgets yet. + +**Files:** +- Modify: `ui_ctk.py` — add dataclass near top (after imports, before `should_skip_file_row`) +- Modify: `ui_ctk.py:61-80` — add state fields to `__init__` + +**Step 1: Add import and dataclass at top of file (after existing imports, before `should_skip_file_row` on line 30)** + +```python +from dataclasses import dataclass, field + +@dataclass +class TreeNode: + node_id: str + parent_id: str # "" for root-level nodes + name: str + kind: str # "folder" | "file" + full_path: str # "" for folders + size: str + file_type: str + icon_group: str # "folder"|"image"|"document"|"archive"|"code"|"audio"|"video"|"text"|"binary" + checked: bool = False + expanded: bool = False + hidden: bool = False # True when filtered out by search + children: list = field(default_factory=list) # ordered list of child node_ids +``` + +**Step 2: Add state fields to `__init__` (after `self.checkbox_checked = "✔ "` on line 70)** + +```python +# FileTree data model +self.tree_nodes: dict[str, TreeNode] = {} +self.tree_roots: list[str] = [] # top-level node_ids in insertion order +self._node_counter: int = 0 # monotonic counter for unique node_ids +``` + +**Step 3: Write a quick manual verification** + +```python +# In a scratch test (not committed): confirm dataclass works +node = TreeNode(node_id="n1", parent_id="", name="test", kind="folder", + full_path="", size="", file_type="", icon_group="folder") +assert node.children == [] +assert not node.checked +``` + +**Step 4: Commit** + +```bash +git add ui_ctk.py +git commit -m "feat: add TreeNode dataclass and data model state" +``` + +--- + +### Task 2: Build `RowWidget` class + +**Goal:** A single row frame with indent, chevron, emoji icon, name, size, and type badge. Handles hover highlighting and checked-state background. + +**Files:** +- Modify: `ui_ctk.py` — add `RowWidget` class after `TreeNode` dataclass, before `should_skip_file_row` + +**Step 1: Add the `RowWidget` class** + +```python +_EMOJI_ICONS = { + "folder": "📁", + "image": "🖼️", + "document": "📄", + "archive": "🗜️", + "code": "💻", + "audio": "🎵", + "video": "🎬", + "text": "📝", + "binary": "⚙️", +} + +_BG_NORMAL = ("gray95", "gray17") +_BG_HOVER = ("#E2E8F0", "#2D3748") +_BG_CHECKED = ("#DBEAFE", "#1E3A5F") +_BG_CHECKED_HOVER = ("#BFDBFE", "#1E40AF") + + +class RowWidget: + """One visible row in the FileTree.""" + + INDENT_PX = 20 + ROW_HEIGHT = 34 + + def __init__(self, parent, app, node: TreeNode, depth: int): + self.app = app + self.node_id = node.node_id + self._checked = node.checked + self._hovered = False + + self.frame = ctk.CTkFrame(parent, height=self.ROW_HEIGHT, corner_radius=4) + self.frame.pack(fill="x", padx=4, pady=1) + self.frame.pack_propagate(False) + + # Indent spacer + if depth > 0: + ctk.CTkFrame( + self.frame, width=depth * self.INDENT_PX, + fg_color="transparent", height=self.ROW_HEIGHT, + ).pack(side="left") + + # Chevron (folders) or invisible spacer (files) + if node.kind == "folder": + self.chevron = ctk.CTkButton( + self.frame, text="▶" if not node.expanded else "▼", + width=22, height=22, fg_color="transparent", + hover_color=("gray85", "gray30"), text_color=("gray40", "gray60"), + font=ctk.CTkFont(size=10), + command=lambda: app._on_chevron_click(self.node_id), + ) + self.chevron.pack(side="left", padx=(2, 0)) + else: + ctk.CTkFrame( + self.frame, width=26, fg_color="transparent", height=self.ROW_HEIGHT, + ).pack(side="left") + + # Emoji icon + ctk.CTkLabel( + self.frame, + text=_EMOJI_ICONS.get(node.icon_group, "📄"), + font=ctk.CTkFont(size=16), + width=28, + ).pack(side="left", padx=(2, 4)) + + # Name label + self.name_label = ctk.CTkLabel( + self.frame, + text=node.name, + anchor="w", + font=ctk.CTkFont(size=13, weight="bold" if node.kind == "folder" else "normal"), + ) + self.name_label.pack(side="left", fill="x", expand=True) + + # Size + type (right side) + if node.kind == "file": + if node.size: + ctk.CTkLabel( + self.frame, + text=node.size, + font=ctk.CTkFont(size=11), + text_color=("gray50", "gray60"), + width=80, + anchor="e", + ).pack(side="right", padx=(0, 4)) + if node.icon_group and node.icon_group != "binary": + ctk.CTkLabel( + self.frame, + text=node.icon_group, + font=ctk.CTkFont(size=10), + text_color=("gray50", "gray60"), + width=60, + anchor="e", + ).pack(side="right", padx=(0, 2)) + + self._update_bg() + + # Hover + click bindings on frame and all children + self._bind_all(self.frame) + + def _bind_all(self, widget): + widget.bind("", self._on_enter) + widget.bind("", self._on_leave) + widget.bind("", self._on_click) + for child in widget.winfo_children(): + self._bind_all(child) + + def _on_enter(self, _event=None): + self._hovered = True + self._update_bg() + + def _on_leave(self, _event=None): + self._hovered = False + self._update_bg() + + def _on_click(self, event): + self.app._on_row_click(self.node_id, event) + + def set_checked(self, checked: bool): + self._checked = checked + self._update_bg() + + def set_chevron(self, expanded: bool): + if hasattr(self, "chevron"): + self.chevron.configure(text="▼" if expanded else "▶") + + def _update_bg(self): + if self._checked and self._hovered: + color = _BG_CHECKED_HOVER + elif self._checked: + color = _BG_CHECKED + elif self._hovered: + color = _BG_HOVER + else: + color = _BG_NORMAL + self.frame.configure(fg_color=color) + + def destroy(self): + self.frame.destroy() +``` + +**Step 2: Run smoke tests** + +```bash +uv run pytest test_ui_ttkb.py -v -k "smoke" 2>&1 | tail -10 +``` + +Expected: passes (RowWidget not yet wired up) + +**Step 3: Commit** + +```bash +git add ui_ctk.py +git commit -m "feat: add RowWidget class with emoji icons and hover/checked styling" +``` + +--- + +### Task 3: Build `_build_filetree()` to replace `_build_treeview()` + +**Goal:** Replace the `ttk.Treeview` container with a `CTkScrollableFrame`. Wire up search bar. Store references needed by later tasks. + +**Files:** +- Modify: `ui_ctk.py:393-455` — replace `_build_treeview` entirely +- Modify: `ui_ctk.py:136` — change `self._build_treeview()` to `self._build_filetree()` + +**Step 1: Replace `_build_treeview` method (lines 393–455 inclusive)** + +```python +def _build_filetree(self) -> None: + outer = ctk.CTkFrame(self.window, fg_color="transparent") + outer.grid(row=2, column=0, sticky="nsew", padx=10, pady=(4, 0)) + outer.grid_columnconfigure(0, weight=1) + outer.grid_rowconfigure(1, weight=1) + + # Search bar + search_bar = ctk.CTkFrame(outer, fg_color="transparent") + search_bar.grid(row=0, column=0, sticky="ew", pady=(0, 4)) + search_bar.grid_columnconfigure(1, weight=1) + ctk.CTkLabel(search_bar, text="Search").grid(row=0, column=0, padx=(0, 6)) + self.search_var = tk.StringVar() + self.search_entry = ctk.CTkEntry(search_bar, textvariable=self.search_var) + self.search_entry.grid(row=0, column=1, sticky="ew") + self.search_var.trace_add("write", self.on_search_filter_changed) + + # Scrollable tree container + self.tree_scroll_frame = ctk.CTkScrollableFrame( + outer, + fg_color=("gray95", "gray17"), + corner_radius=8, + ) + self.tree_scroll_frame.grid(row=1, column=0, sticky="nsew") + + # Runtime state for view layer + self._visible_nodes: list[str] = [] + self._row_widgets: dict[str, RowWidget] = {} +``` + +**Step 2: Update `_build_full_ui` to call the new method (line 136)** + +Change: +```python + self._build_treeview() +``` +To: +```python + self._build_filetree() +``` + +**Step 3: Update `_build_full_ui` state init (line 141-145): remove `self.sort_reverse` etc. temporarily is OK; keep them to avoid AttributeErrors** + +No change needed here — those lines stay. + +**Step 4: Update `_build_ui_smoke_only` — add missing attributes that callers expect** + +Find `_build_ui_smoke_only` (around line 121). After the existing lines add: + +```python + self.tree_nodes = {} + self.tree_roots = [] + self._node_counter = 0 + self._visible_nodes = [] + self._row_widgets = {} + self.tree_scroll_frame = None + self.full_tree_backup = {} + self.sort_reverse = False + self.drag_anchor_item = "" + self._last_toggle_time = 0.0 + self._search_after_id = None + self.context_menu = tk.Menu(self.window, tearoff=0) +``` + +**Step 5: Remove old imports that are no longer needed** + +In the `from ui_theme import (...)` block, remove: +- `configure_treeview_style` +- `treeview_tag_colors` + +Also remove `ttk` from `from tkinter import filedialog, messagebox, ttk` → `from tkinter import filedialog, messagebox` + +Wait — `ttk` is still used in `configure_action_button_styles` calls inside `_build_full_ui` (line 50). Leave `ttk` import for now; clean up in Task 8. + +**Step 6: Run smoke tests** + +```bash +uv run python index_ripper.py --ui-smoke 2>&1 | tail -5 +uv run pytest test_ui_ttkb.py -v 2>&1 | tail -15 +``` + +Expected: app launches without error; smoke tests pass. + +**Step 7: Commit** + +```bash +git add ui_ctk.py +git commit -m "feat: replace _build_treeview with _build_filetree (CTkScrollableFrame)" +``` + +--- + +### Task 4: Implement `_rebuild_visible()` and `_sync_rows()` + +**Goal:** Core view logic. `_rebuild_visible()` does a DFS walk of `tree_nodes` respecting `expanded` and `hidden`. `_sync_rows()` creates new `RowWidget`s and removes stale ones. + +**Files:** +- Modify: `ui_ctk.py` — add two methods after `_build_filetree` + +**Step 1: Add `_rebuild_visible` and `_sync_rows` methods** + +```python +def _rebuild_visible(self) -> None: + """Recompute self._visible_nodes from data model (DFS, respects expanded/hidden).""" + result: list[str] = [] + + def _walk(node_id: str) -> None: + node = self.tree_nodes.get(node_id) + if node is None or node.hidden: + return + result.append(node_id) + if node.kind == "folder" and node.expanded: + for child_id in node.children: + _walk(child_id) + + for root_id in self.tree_roots: + _walk(root_id) + + self._visible_nodes = result + +def _sync_rows(self) -> None: + """Create/destroy RowWidgets to match _visible_nodes.""" + if self.tree_scroll_frame is None: + return + + new_ids = set(self._visible_nodes) + # Remove rows no longer visible + for node_id in list(self._row_widgets): + if node_id not in new_ids: + self._row_widgets.pop(node_id).destroy() + + # Repack all rows in correct order + for widget in self.tree_scroll_frame.winfo_children(): + widget.pack_forget() + + for node_id in self._visible_nodes: + node = self.tree_nodes[node_id] + depth = self._node_depth(node_id) + if node_id not in self._row_widgets: + self._row_widgets[node_id] = RowWidget( + self.tree_scroll_frame, self, node, depth + ) + else: + self._row_widgets[node_id].frame.pack(fill="x", padx=4, pady=1) + +def _node_depth(self, node_id: str) -> int: + depth = 0 + node = self.tree_nodes.get(node_id) + while node and node.parent_id: + depth += 1 + node = self.tree_nodes.get(node.parent_id) + return depth + +def _next_node_id(self) -> str: + self._node_counter += 1 + return f"n{self._node_counter}" +``` + +**Step 2: Add click handlers referenced by RowWidget** + +```python +def _on_row_click(self, node_id: str, event=None) -> None: + now = time.monotonic() + if now - self._last_toggle_time < 0.25: + return + self._last_toggle_time = now + node = self.tree_nodes.get(node_id) + if node is None: + return + if node.kind == "folder": + self._on_chevron_click(node_id) + else: + self.toggle_check(node_id) + +def _on_chevron_click(self, node_id: str) -> None: + node = self.tree_nodes.get(node_id) + if node is None or node.kind != "folder": + return + node.expanded = not node.expanded + row = self._row_widgets.get(node_id) + if row: + row.set_chevron(node.expanded) + self._rebuild_visible() + self._sync_rows() +``` + +**Step 3: Run smoke tests** + +```bash +uv run pytest test_ui_ttkb.py -v 2>&1 | tail -15 +``` + +**Step 4: Commit** + +```bash +git add ui_ctk.py +git commit -m "feat: implement _rebuild_visible and _sync_rows for FileTree view layer" +``` + +--- + +### Task 5: Rewrite `add_folder()` and `add_file()` + +**Goal:** Insert nodes into the data model (not Treeview), then call `_rebuild_visible` + `_sync_rows`. + +**Files:** +- Modify: `ui_ctk.py:1092–1202` — replace both methods entirely + +**Step 1: Replace `add_folder` (currently line 1092)** + +```python +def add_folder(self, dir_path: str, url: str) -> str: + """Ensure all path segments exist as folder nodes; return leaf node_id.""" + if not dir_path: + dir_path = "/" + parts = [p for p in dir_path.split("/") if p] + + parent_id = "" + current_path = "" + for part in parts: + current_path = current_path + "/" + part + with self.folders_dict_lock: + existing_id = self.folders.get(current_path) + if existing_id: + parent_id = existing_id + continue + + node_id = self._next_node_id() + node = TreeNode( + node_id=node_id, + parent_id=parent_id, + name=part, + kind="folder", + full_path="", + size="", + file_type="", + icon_group="folder", + ) + self.tree_nodes[node_id] = node + if parent_id: + self.tree_nodes[parent_id].children.append(node_id) + else: + self.tree_roots.append(node_id) + + with self.folders_dict_lock: + self.folders[current_path] = node_id + parent_id = node_id + + self._rebuild_visible() + self._sync_rows() + return parent_id +``` + +**Step 2: Replace `add_file` (currently line 1135)** + +```python +def add_file(self, dir_path: str, url: str, file_name: str, size, file_type: str, full_path: str) -> None: + """Add a file node to the tree.""" + if not file_name: + return + is_html_dir_like = ( + isinstance(file_type, str) + and "text/html" in file_type.lower() + and "." not in (file_name or "") + ) + parent_id = self.add_folder(dir_path, url) + + with self.files_dict_lock: + existing_entry = self.files_dict.get(full_path) + if should_skip_file_row(existing_entry): + return + if is_html_dir_like: + self.files_dict.pop(full_path, None) + else: + self.files_dict[full_path] = { + "url": url, + "file_name": file_name, + "size": size, + "file_type": file_type, + "path": dir_path, + } + + if is_html_dir_like: + folder_path = f"{dir_path.rstrip('/')}/{file_name}".replace("//", "/") + self.add_folder(folder_path, url) + return + + ext = normalize_extension(file_name) + self._add_file_type_filter(ext) + self.file_type_counts[ext] = self.file_type_counts.get(ext, 0) + 1 + cb = self.file_type_widgets.get(ext) + if cb: + label = ext if ext else "(no ext)" + cb.configure(text=f"{label} ({self.file_type_counts[ext]})") + var = self.file_types.get(ext) + if var is not None and not var.get(): + return + + group = self._file_icon_and_group(file_name, file_type) + node_id = self._next_node_id() + node = TreeNode( + node_id=node_id, + parent_id=parent_id, + name=file_name, + kind="file", + full_path=full_path or "", + size=size or "", + file_type=file_type or "", + icon_group=group, + checked=full_path in self.checked_items, + ) + self.tree_nodes[node_id] = node + if parent_id: + self.tree_nodes[parent_id].children.append(node_id) + else: + self.tree_roots.append(node_id) + + self._rebuild_visible() + self._sync_rows() +``` + +**Note:** `_rebuild_visible` + `_sync_rows` is called once per file during scan. This is acceptable for ~1000 items (each call is O(visible_nodes)). If performance is sluggish, batch them by adding a `_pending_rebuild` flag and scheduling via `window.after(0, ...)`. + +**Step 3: Run the app and do a real scan to verify folders and files appear** + +```bash +uv run python index_ripper.py +``` + +Enter a URL, click Scan, verify file/folder rows appear in the tree. + +**Step 4: Commit** + +```bash +git add ui_ctk.py +git commit -m "feat: rewrite add_folder and add_file to use TreeNode data model" +``` + +--- + +### Task 6: Rewrite `toggle_check()`, `select_all()`, `deselect_all()` + +**Goal:** Checkbox logic using data model. Update `RowWidget` background. Maintain `checked_items`. + +**Files:** +- Modify: `ui_ctk.py:821–932` — replace `toggle_check`, `select_all`, `deselect_all`, and related methods + +**Step 1: Replace `toggle_check` (currently line 821)** + +```python +def toggle_check(self, node_id: str, force_check=None, _skip_children: bool = False) -> None: + node = self.tree_nodes.get(node_id) + if node is None: + return + new_checked = (not node.checked) if force_check is None else bool(force_check) + node.checked = new_checked + + if node.kind == "file" and node.full_path: + if new_checked: + self.checked_items.add(node.full_path) + else: + self.checked_items.discard(node.full_path) + + row = self._row_widgets.get(node_id) + if row: + row.set_checked(new_checked) + + if node.kind == "folder" and not _skip_children: + for child_id in node.children: + self.toggle_check(child_id, force_check=new_checked) +``` + +**Step 2: Replace `select_all` and `deselect_all` (currently lines 926-932)** + +```python +def select_all(self) -> None: + for node_id in list(self.tree_nodes): + self.toggle_check(node_id, force_check=True, _skip_children=True) + +def deselect_all(self) -> None: + for node_id in list(self.tree_nodes): + self.toggle_check(node_id, force_check=False, _skip_children=True) +``` + +**Step 3: Replace `_all_tree_items` — keep for API compatibility but use tree_nodes** + +```python +def _all_tree_items(self) -> list[str]: + """Return all node_ids in DFS order.""" + out: list[str] = [] + def _walk(node_id: str) -> None: + out.append(node_id) + node = self.tree_nodes.get(node_id) + if node: + for child_id in node.children: + _walk(child_id) + for root_id in self.tree_roots: + _walk(root_id) + return out +``` + +**Step 4: Remove or stub old helper methods that referenced `self.tree` (ttk.Treeview)** + +Remove these methods entirely (they're Treeview-specific and no longer needed): +- `_set_item_checked_visual` +- `_strip_checkmark` +- `_focused_tree_item` +- `on_tree_click` — replace with `_on_row_click` (already added in Task 4) +- `on_tree_drag_select` +- `on_tree_space` +- `on_tree_enter` +- `_on_tree_select_all` +- `show_context_menu` — keep but update to not use `self.tree.identify_row` + +**Step 5: Update `show_context_menu`** + +```python +def show_context_menu(self, event) -> None: + try: + self.context_menu.tk_popup(event.x_root, event.y_root) + finally: + self.context_menu.grab_release() +``` + +**Step 6: Commit** + +```bash +git add ui_ctk.py +git commit -m "feat: rewrite toggle_check, select_all, deselect_all using TreeNode model" +``` + +--- + +### Task 7: Rewrite `expand_all`, `collapse_all`, `sort_tree`, and `_on_type_filter_changed` + +**Goal:** These methods previously walked the Treeview; rewrite to use `tree_nodes`. + +**Files:** +- Modify: `ui_ctk.py:934–961, 1486–1515` + +**Step 1: Replace `expand_all` and `collapse_all`** + +```python +def expand_all(self, parent: str = "") -> None: + targets = self.tree_roots if not parent else self.tree_nodes[parent].children + for node_id in targets: + node = self.tree_nodes.get(node_id) + if node and node.kind == "folder": + node.expanded = True + self.expand_all(node_id) + self._rebuild_visible() + self._sync_rows() + +def collapse_all(self, parent: str = "") -> None: + targets = self.tree_roots if not parent else self.tree_nodes[parent].children + for node_id in targets: + node = self.tree_nodes.get(node_id) + if node and node.kind == "folder": + node.expanded = False + self.collapse_all(node_id) + self._rebuild_visible() + self._sync_rows() +``` + +**Step 2: Replace `sort_tree`** + +```python +def sort_tree(self, col: str = "name") -> None: + def sort_children(children: list[str]) -> None: + if not children: + return + def key(node_id: str) -> str: + node = self.tree_nodes.get(node_id) + if node is None: + return "" + if col == "size": + return node.size.lower() + if col == "type": + return node.icon_group.lower() + return node.name.lower() + children.sort(key=key, reverse=self.sort_reverse) + for node_id in children: + node = self.tree_nodes.get(node_id) + if node: + sort_children(node.children) + + sort_children(self.tree_roots) + self.sort_reverse = not self.sort_reverse + self._rebuild_visible() + self._sync_rows() +``` + +**Step 3: Replace `_on_type_filter_changed`** + +```python +def _on_type_filter_changed(self, ext: str) -> None: + if self.full_tree_backup: + return # search filter active; handled by _filter_tree_by_term + var = self.file_types.get(ext) + if var is None: + return + visible = var.get() + for node in self.tree_nodes.values(): + if node.kind == "file" and normalize_extension(node.name) == ext: + node.hidden = not visible + self._rebuild_visible() + self._sync_rows() +``` + +**Step 4: Commit** + +```bash +git add ui_ctk.py +git commit -m "feat: rewrite expand_all, collapse_all, sort_tree, type filter using TreeNode" +``` + +--- + +### Task 8: Rewrite `_backup_full_tree`, `_restore_full_tree`, `_filter_tree_by_term` + +**Goal:** Search filter backup/restore using data model instead of Treeview state. + +**Files:** +- Modify: `ui_ctk.py:1373–1448` + +**Step 1: Replace `_backup_full_tree`** + +```python +def _backup_full_tree(self) -> None: + """Snapshot tree_nodes and tree_roots for search filter restore.""" + import copy + self.full_tree_backup = { + "nodes": copy.deepcopy(self.tree_nodes), + "roots": list(self.tree_roots), + "folders": dict(self.folders), + } +``` + +**Step 2: Replace `_restore_full_tree`** + +```python +def _restore_full_tree(self) -> None: + """Restore tree_nodes from backup (undo search filter).""" + if not self.full_tree_backup: + return + self.tree_nodes = self.full_tree_backup["nodes"] + self.tree_roots = self.full_tree_backup["roots"] + with self.folders_dict_lock: + self.folders = self.full_tree_backup["folders"] + self.full_tree_backup = {} + + # Destroy all existing row widgets and rebuild + for row in self._row_widgets.values(): + row.destroy() + self._row_widgets.clear() + self._rebuild_visible() + self._sync_rows() +``` + +**Step 3: Replace `_filter_tree_by_term`** + +```python +def _filter_tree_by_term(self, term: str) -> None: + """Mark nodes hidden if they (and their descendants) don't match term.""" + term = term.lower() + + def matches(node_id: str) -> bool: + node = self.tree_nodes.get(node_id) + if node is None: + return False + if term in node.name.lower() or term in node.full_path.lower(): + return True + return any(matches(child_id) for child_id in node.children) + + def apply_visibility(node_id: str) -> None: + node = self.tree_nodes.get(node_id) + if node is None: + return + if matches(node_id): + node.hidden = False + node.expanded = True # auto-expand matching folders + else: + node.hidden = True + for child_id in node.children: + apply_visibility(child_id) + + for root_id in self.tree_roots: + apply_visibility(root_id) + + self._rebuild_visible() + self._sync_rows() +``` + +**Step 4: Commit** + +```bash +git add ui_ctk.py +git commit -m "feat: rewrite backup/restore/filter using TreeNode model" +``` + +--- + +### Task 9: Rewrite `clear_scan_results` and remove all remaining `self.tree` (Treeview) references + +**Goal:** Final cleanup. Replace every `self.tree.*` call with tree_nodes model. Remove unused imports. + +**Files:** +- Modify: `ui_ctk.py:1040–1062` — `clear_scan_results` +- Modify: `ui_ctk.py` — remove `self.checkbox_checked`, remove `ttk`/`configure_treeview_style`/`treeview_tag_colors` imports + +**Step 1: Replace `clear_scan_results`** + +```python +def clear_scan_results(self) -> None: + if self.is_scanning: + return + self._drain_queues() + + # Clear data model + self.tree_nodes.clear() + self.tree_roots.clear() + self._node_counter = 0 + with self.folders_dict_lock: + self.folders.clear() + + # Destroy row widgets + for row in self._row_widgets.values(): + row.destroy() + self._row_widgets.clear() + self._visible_nodes.clear() + + # Clear other state + with self.files_dict_lock: + self.files_dict.clear() + if hasattr(self, "filters_container"): + for widget in self.filters_container.winfo_children(): + widget.destroy() + self.file_types.clear() + self.file_type_counts.clear() + self.file_type_widgets.clear() + self.checked_items.clear() + self.full_tree_backup.clear() if isinstance(self.full_tree_backup, dict) else None + self.full_tree_backup = {} + self.scanned_urls = 0 + self.total_urls = 0 + self.progress_bar.set(0) + self.progress_label.configure(text="") + self._set_status("Ready", "#059669") +``` + +**Step 2: Search for remaining `self.tree` references** + +```bash +grep -n "self\.tree\b" ui_ctk.py | grep -v "tree_nodes\|tree_roots\|tree_scroll\|_tree_icons\|file_tree\|tree_backup\|_node_counter\|_visible\|_row_widget" +``` + +Expected: only innocuous hits like `self.tree_roots` etc. Fix any remaining `self.tree.xxx` (Treeview) calls. + +**Step 3: Remove unused imports** + +- Remove `ttk` from `from tkinter import filedialog, messagebox, ttk` if no ttk usage remains +- Remove `configure_treeview_style`, `treeview_tag_colors` from `ui_theme` import +- Remove `_init_tree_icons` method (entire method, no longer needed) +- Remove `self.checkbox_checked = "✔ "` from `__init__` + +**Step 4: Run full test suite** + +```bash +uv run pytest test_ui_ttkb.py test_ui_downloads.py test_ui_theme.py -v 2>&1 | tail -20 +``` + +**Step 5: Launch the app and do an end-to-end test** + +```bash +uv run python index_ripper.py +``` + +Test checklist: +- [ ] App launches without errors +- [ ] Scan a URL → files appear with emoji icons +- [ ] Click a file → row turns blue (checked) +- [ ] Click a folder → expands/collapses +- [ ] Type in search → non-matching rows disappear +- [ ] Clear search → rows reappear +- [ ] Select All / Deselect All works +- [ ] Expand All / Collapse All works +- [ ] File type checkboxes show/hide rows + +**Step 6: Commit** + +```bash +git add ui_ctk.py +git commit -m "feat: complete FileTree migration - remove all ttk.Treeview references" +``` + +--- + +## Done + +All 9 tasks complete. The `ttk.Treeview` is fully replaced with a `CTkScrollableFrame`-based custom tree using emoji icons, hover effects, and checked-state background highlighting. From 6489ef9fe58bcb56533d9310b63fb26da55ae4af Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 16:16:32 +0800 Subject: [PATCH 25/55] feat: add TreeNode dataclass and data model state --- ui_ctk.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ui_ctk.py b/ui_ctk.py index 6eef760..5a32705 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -25,6 +25,23 @@ ui_tokens, ) +from dataclasses import dataclass, field + +@dataclass +class TreeNode: + node_id: str + parent_id: str # "" for root-level nodes + name: str + kind: str # "folder" | "file" + full_path: str # "" for folders + size: str + file_type: str + icon_group: str # "folder"|"image"|"document"|"archive"|"code"|"audio"|"video"|"text"|"binary" + checked: bool = False + expanded: bool = False + hidden: bool = False # True when filtered out by search + children: list = field(default_factory=list) # ordered list of child node_ids + def should_skip_file_row(existing_entry) -> bool: """Return True when a file row is already fully registered.""" @@ -68,6 +85,11 @@ def __init__(self, ui_smoke: bool = False): self.checked_items: set[str] = set() self.checkbox_checked = "✔ " + # FileTree data model + self.tree_nodes: dict[str, TreeNode] = {} + self.tree_roots: list[str] = [] # top-level node_ids in insertion order + self._node_counter: int = 0 # monotonic counter for unique node_ids + self.dir_queue = Queue() self.file_queue = Queue() self.scan_item_buffer = Queue() From 4d417b1b81f1d2959c1c64fd5b3279211eaf14d5 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 16:21:23 +0800 Subject: [PATCH 26/55] fix: move dataclasses import to stdlib block, type children list[str] --- ui_ctk.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ui_ctk.py b/ui_ctk.py index 5a32705..c259b5e 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -3,6 +3,7 @@ import os import threading from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass, field from queue import Empty, Queue import tkinter as tk @@ -25,8 +26,6 @@ ui_tokens, ) -from dataclasses import dataclass, field - @dataclass class TreeNode: node_id: str @@ -40,7 +39,7 @@ class TreeNode: checked: bool = False expanded: bool = False hidden: bool = False # True when filtered out by search - children: list = field(default_factory=list) # ordered list of child node_ids + children: list[str] = field(default_factory=list) # ordered list of child node_ids def should_skip_file_row(existing_entry) -> bool: From b6c385b5354e6c60dc8d5aa99a18eeddb5d220b9 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 18:24:10 +0800 Subject: [PATCH 27/55] feat: add RowWidget class with emoji icons and hover/checked styling --- ui_ctk.py | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/ui_ctk.py b/ui_ctk.py index c259b5e..fd6f4fc 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -42,6 +42,146 @@ class TreeNode: children: list[str] = field(default_factory=list) # ordered list of child node_ids +_EMOJI_ICONS = { + "folder": "📁", + "image": "🖼️", + "document": "📄", + "archive": "🗜️", + "code": "💻", + "audio": "🎵", + "video": "🎬", + "text": "📝", + "binary": "⚙️", +} + +_BG_NORMAL = ("gray95", "gray17") +_BG_HOVER = ("#E2E8F0", "#2D3748") +_BG_CHECKED = ("#DBEAFE", "#1E3A5F") +_BG_CHECKED_HOVER = ("#BFDBFE", "#1E40AF") + + +class RowWidget: + """One visible row in the FileTree.""" + + INDENT_PX = 20 + ROW_HEIGHT = 34 + + def __init__(self, parent, app, node: TreeNode, depth: int): + self.app = app + self.node_id = node.node_id + self._checked = node.checked + self._hovered = False + + self.frame = ctk.CTkFrame(parent, height=self.ROW_HEIGHT, corner_radius=4) + self.frame.pack(fill="x", padx=4, pady=1) + self.frame.pack_propagate(False) + + # Indent spacer + if depth > 0: + ctk.CTkFrame( + self.frame, width=depth * self.INDENT_PX, + fg_color="transparent", height=self.ROW_HEIGHT, + ).pack(side="left") + + # Chevron (folders) or invisible spacer (files) + if node.kind == "folder": + self.chevron = ctk.CTkButton( + self.frame, text="▶" if not node.expanded else "▼", + width=22, height=22, fg_color="transparent", + hover_color=("gray85", "gray30"), text_color=("gray40", "gray60"), + font=ctk.CTkFont(size=10), + command=lambda: app._on_chevron_click(self.node_id), + ) + self.chevron.pack(side="left", padx=(2, 0)) + else: + ctk.CTkFrame( + self.frame, width=26, fg_color="transparent", height=self.ROW_HEIGHT, + ).pack(side="left") + + # Emoji icon + ctk.CTkLabel( + self.frame, + text=_EMOJI_ICONS.get(node.icon_group, "📄"), + font=ctk.CTkFont(size=16), + width=28, + ).pack(side="left", padx=(2, 4)) + + # Name label + self.name_label = ctk.CTkLabel( + self.frame, + text=node.name, + anchor="w", + font=ctk.CTkFont(size=13, weight="bold" if node.kind == "folder" else "normal"), + ) + self.name_label.pack(side="left", fill="x", expand=True) + + # Size + type (right side, files only) + if node.kind == "file": + if node.size: + ctk.CTkLabel( + self.frame, + text=node.size, + font=ctk.CTkFont(size=11), + text_color=("gray50", "gray60"), + width=80, + anchor="e", + ).pack(side="right", padx=(0, 4)) + if node.icon_group and node.icon_group != "binary": + ctk.CTkLabel( + self.frame, + text=node.icon_group, + font=ctk.CTkFont(size=10), + text_color=("gray50", "gray60"), + width=60, + anchor="e", + ).pack(side="right", padx=(0, 2)) + + self._update_bg() + + # Bind hover + click on frame and all children + self._bind_all(self.frame) + + def _bind_all(self, widget) -> None: + widget.bind("", self._on_enter) + widget.bind("", self._on_leave) + widget.bind("", self._on_click) + for child in widget.winfo_children(): + self._bind_all(child) + + def _on_enter(self, _event=None) -> None: + self._hovered = True + self._update_bg() + + def _on_leave(self, _event=None) -> None: + self._hovered = False + self._update_bg() + + def _on_click(self, event) -> None: + self.app._on_row_click(self.node_id, event) + + def set_checked(self, checked: bool) -> None: + self._checked = checked + self._update_bg() + + def set_chevron(self, expanded: bool) -> None: + if hasattr(self, "chevron"): + self.chevron.configure(text="▼" if expanded else "▶") + + def _update_bg(self) -> None: + if self._checked and self._hovered: + color = _BG_CHECKED_HOVER + elif self._checked: + color = _BG_CHECKED + elif self._hovered: + color = _BG_HOVER + else: + color = _BG_NORMAL + self.frame.configure(fg_color=color) + + def destroy(self) -> None: + self.frame.destroy() + + def should_skip_file_row(existing_entry) -> bool: """Return True when a file row is already fully registered.""" return existing_entry is not None From 67fd9a08a2d0599f62eab0c4a7a9dd7ac2a60d03 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 18:26:56 +0800 Subject: [PATCH 28/55] fix: skip chevron in _bind_all to prevent double-trigger on click --- ui_ctk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui_ctk.py b/ui_ctk.py index fd6f4fc..edf6070 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -142,6 +142,9 @@ def __init__(self, parent, app, node: TreeNode, depth: int): self._bind_all(self.frame) def _bind_all(self, widget) -> None: + # Skip chevron button — it has its own command; don't overlay _on_click + if hasattr(self, "chevron") and widget is self.chevron: + return widget.bind("", self._on_enter) widget.bind("", self._on_leave) widget.bind("", self._on_click) From fa586174681ae5f243294132ccf23e001c82f5c2 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 18:29:28 +0800 Subject: [PATCH 29/55] feat: replace _build_treeview with _build_filetree (CTkScrollableFrame) Co-Authored-By: Claude Sonnet 4.6 --- ui_ctk.py | 77 ++++++++++++++++++++----------------------------------- 1 file changed, 28 insertions(+), 49 deletions(-) diff --git a/ui_ctk.py b/ui_ctk.py index edf6070..931132d 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -21,8 +21,6 @@ from ui_theme import ( apply_app_theme, configure_action_button_styles, - configure_treeview_style, - treeview_tag_colors, ui_tokens, ) @@ -294,6 +292,15 @@ def _build_ui_smoke_only(self) -> None: self.log_text.insert("end", "UI smoke minimal view initialized.\n") self.panels_notebook = None self.logs_tab = None + self._visible_nodes = [] + self._row_widgets = {} + self.tree_scroll_frame = None + self.full_tree_backup = {} + self.sort_reverse = False + self.drag_anchor_item = "" + self._last_toggle_time = 0.0 + self._search_after_id = None + self.context_menu = tk.Menu(self.window, tearoff=0) def _build_full_ui(self) -> None: self.window.grid_columnconfigure(0, weight=1) @@ -301,7 +308,7 @@ def _build_full_ui(self) -> None: self._build_header() self._build_filters_row() - self._build_treeview() + self._build_filetree() self._build_progress_section() self._build_panels() self._build_download_controls() @@ -541,15 +548,15 @@ def _build_filters_row(self) -> None: width=150, ).pack(side="left") - def _build_treeview(self) -> None: - tree_frame = ctk.CTkFrame(self.window, fg_color="transparent") - tree_frame.grid(row=2, column=0, sticky="nsew", padx=10, pady=(4, 0)) - tree_frame.grid_columnconfigure(0, weight=1) - tree_frame.grid_rowconfigure(1, weight=1) + def _build_filetree(self) -> None: + outer = ctk.CTkFrame(self.window, fg_color="transparent") + outer.grid(row=2, column=0, sticky="nsew", padx=10, pady=(4, 0)) + outer.grid_columnconfigure(0, weight=1) + outer.grid_rowconfigure(1, weight=1) - # 搜尋欄 - search_bar = ctk.CTkFrame(tree_frame, fg_color="transparent") - search_bar.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 4)) + # Search bar + search_bar = ctk.CTkFrame(outer, fg_color="transparent") + search_bar.grid(row=0, column=0, sticky="ew", pady=(0, 4)) search_bar.grid_columnconfigure(1, weight=1) ctk.CTkLabel(search_bar, text="Search").grid(row=0, column=0, padx=(0, 6)) self.search_var = tk.StringVar() @@ -557,45 +564,17 @@ def _build_treeview(self) -> None: self.search_entry.grid(row=0, column=1, sticky="ew") self.search_var.trace_add("write", self.on_search_filter_changed) - # Treeview - configure_treeview_style(self.window, ctk, ttk) - self.tree = ttk.Treeview( - tree_frame, - columns=("size", "type", "full_path"), - show="tree headings", - selectmode="extended", + # Scrollable tree container + self.tree_scroll_frame = ctk.CTkScrollableFrame( + outer, + fg_color=("gray95", "gray17"), + corner_radius=8, ) - self.tree.heading("#0", text="Path", command=lambda: self.sort_tree("#0")) - self.tree.heading("size", text="Size", command=lambda: self.sort_tree("size")) - self.tree.heading("type", text="Type", command=lambda: self.sort_tree("type")) - self.tree.column("#0", width=600, stretch=True) - self.tree.column("size", width=120, stretch=False, anchor="e") - self.tree.column("type", width=240, stretch=False) - self.tree.column("full_path", width=0, stretch=False) - - tag_colors = treeview_tag_colors(self.window) - self.tree.tag_configure("checked", foreground=tag_colors["checked"]) - - style = ttk.Style() - style.configure("Treeview", font=("SF Pro Text", 14), rowheight=34) - style.configure("Treeview.Heading", font=("SF Pro Text", 13, "bold")) - - self.tree.grid(row=1, column=0, sticky="nsew") - - tree_scroll = tk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview) - self.tree.configure(yscrollcommand=tree_scroll.set) - tree_scroll.grid(row=1, column=1, sticky="ns") - - # 鍵盤與滑鼠綁定 - self.tree.bind("", self._on_tree_select_all) - self.tree.bind("", self._on_tree_select_all) - self.tree.bind("", self.on_tree_click) - self.tree.bind("", self.show_context_menu) - self.tree.bind("", self.show_context_menu) - self.tree.bind("", self.show_context_menu) - self.tree.bind("", self.on_tree_drag_select) - self.tree.bind("", self.on_tree_space) - self.tree.bind("", self.on_tree_enter) + self.tree_scroll_frame.grid(row=1, column=0, sticky="nsew") + + # Runtime state for view layer + self._visible_nodes: list[str] = [] + self._row_widgets: dict[str, RowWidget] = {} def _build_progress_section(self) -> None: progress_frame = ctk.CTkFrame(self.window, fg_color="transparent") From b78dfa148d52bb5c01569afe40086bf17a09343e Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 18:32:15 +0800 Subject: [PATCH 30/55] fix: explicitly set tree_nodes/tree_roots/_node_counter in smoke init --- ui_ctk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui_ctk.py b/ui_ctk.py index 931132d..126db77 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -300,6 +300,9 @@ def _build_ui_smoke_only(self) -> None: self.drag_anchor_item = "" self._last_toggle_time = 0.0 self._search_after_id = None + self.tree_nodes = {} + self.tree_roots = [] + self._node_counter = 0 self.context_menu = tk.Menu(self.window, tearoff=0) def _build_full_ui(self) -> None: From 88ee4972759fff8f2363b64ea16a55802b89ca27 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 18:34:19 +0800 Subject: [PATCH 31/55] feat: implement _rebuild_visible, _sync_rows, click handlers Co-Authored-By: Claude Sonnet 4.6 --- ui_ctk.py | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/ui_ctk.py b/ui_ctk.py index 126db77..17816b1 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -2,6 +2,7 @@ import os import threading +import time from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass, field from queue import Empty, Queue @@ -229,6 +230,7 @@ def __init__(self, ui_smoke: bool = False): self.tree_nodes: dict[str, TreeNode] = {} self.tree_roots: list[str] = [] # top-level node_ids in insertion order self._node_counter: int = 0 # monotonic counter for unique node_ids + self._last_toggle_time: float = 0.0 # debounce timestamp for row clicks self.dir_queue = Queue() self.file_queue = Queue() @@ -579,6 +581,85 @@ def _build_filetree(self) -> None: self._visible_nodes: list[str] = [] self._row_widgets: dict[str, RowWidget] = {} + def _rebuild_visible(self) -> None: + """Recompute self._visible_nodes from data model (DFS, respects expanded/hidden).""" + result: list[str] = [] + + def _walk(node_id: str) -> None: + node = self.tree_nodes.get(node_id) + if node is None or node.hidden: + return + result.append(node_id) + if node.kind == "folder" and node.expanded: + for child_id in node.children: + _walk(child_id) + + for root_id in self.tree_roots: + _walk(root_id) + + self._visible_nodes = result + + def _sync_rows(self) -> None: + """Create/destroy RowWidgets to match _visible_nodes.""" + if self.tree_scroll_frame is None: + return + + new_ids = set(self._visible_nodes) + # Remove rows no longer visible + for node_id in list(self._row_widgets): + if node_id not in new_ids: + self._row_widgets.pop(node_id).destroy() + + # Repack all rows in correct order + for widget in self.tree_scroll_frame.winfo_children(): + widget.pack_forget() + + for node_id in self._visible_nodes: + node = self.tree_nodes[node_id] + depth = self._node_depth(node_id) + if node_id not in self._row_widgets: + self._row_widgets[node_id] = RowWidget( + self.tree_scroll_frame, self, node, depth + ) + else: + self._row_widgets[node_id].frame.pack(fill="x", padx=4, pady=1) + + def _node_depth(self, node_id: str) -> int: + depth = 0 + node = self.tree_nodes.get(node_id) + while node and node.parent_id: + depth += 1 + node = self.tree_nodes.get(node.parent_id) + return depth + + def _next_node_id(self) -> str: + self._node_counter += 1 + return f"n{self._node_counter}" + + def _on_row_click(self, node_id: str, event=None) -> None: + now = time.monotonic() + if now - self._last_toggle_time < 0.25: + return + self._last_toggle_time = now + node = self.tree_nodes.get(node_id) + if node is None: + return + if node.kind == "folder": + self._on_chevron_click(node_id) + else: + self.toggle_check(node_id) + + def _on_chevron_click(self, node_id: str) -> None: + node = self.tree_nodes.get(node_id) + if node is None or node.kind != "folder": + return + node.expanded = not node.expanded + row = self._row_widgets.get(node_id) + if row: + row.set_chevron(node.expanded) + self._rebuild_visible() + self._sync_rows() + def _build_progress_section(self) -> None: progress_frame = ctk.CTkFrame(self.window, fg_color="transparent") progress_frame.grid(row=3, column=0, sticky="ew", padx=10, pady=(4, 0)) From 1c7cd08e5764c99ec71c812255876a4d5f851059 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 18:37:31 +0800 Subject: [PATCH 32/55] feat: rewrite add_folder and add_file to use TreeNode data model Co-Authored-By: Claude Sonnet 4.6 --- ui_ctk.py | 83 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 30 deletions(-) diff --git a/ui_ctk.py b/ui_ctk.py index 17816b1..bfd33da 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -1074,37 +1074,48 @@ def update_progress(self, scanned: int, total: int) -> None: self.progress_label.configure(text=f"Scan: {scanned}/{total}") def add_folder(self, dir_path: str, url: str) -> str: - """Create folder nodes in the treeview for the given path.""" + """Ensure all path segments exist as folder nodes; return leaf node_id.""" if not dir_path: dir_path = "/" parts = [p for p in dir_path.split("/") if p] - current = "" parent_id = "" + current_path = "" for part in parts: - current = current + "/" + part + current_path = current_path + "/" + part with self.folders_dict_lock: - existing = self.folders.get(current) - if existing: - parent_id = existing + existing_id = self.folders.get(current_path) + if existing_id: + parent_id = existing_id continue - node_id = self.tree.insert( - parent_id, - "end", - text=f"📁 {part}", - values=("", "dir", ""), - tags=("folder",), - open=True, + node_id = self._next_node_id() + node = TreeNode( + node_id=node_id, + parent_id=parent_id, + name=part, + kind="folder", + full_path="", + size="", + file_type="", + icon_group="folder", ) + self.tree_nodes[node_id] = node + if parent_id: + self.tree_nodes[parent_id].children.append(node_id) + else: + self.tree_roots.append(node_id) + with self.folders_dict_lock: - self.folders[current] = node_id + self.folders[current_path] = node_id parent_id = node_id + self._rebuild_visible() + self._sync_rows() return parent_id def add_file(self, dir_path: str, url: str, file_name: str, size, file_type: str, full_path: str) -> None: - """Add a file row to the treeview.""" + """Add a file node to the tree.""" if not file_name: return is_html_dir_like = ( @@ -1113,6 +1124,7 @@ def add_file(self, dir_path: str, url: str, file_name: str, size, file_type: str and "." not in (file_name or "") ) parent_id = self.add_folder(dir_path, url) + with self.files_dict_lock: existing_entry = self.files_dict.get(full_path) if should_skip_file_row(existing_entry): @@ -1135,25 +1147,36 @@ def add_file(self, dir_path: str, url: str, file_name: str, size, file_type: str ext = normalize_extension(file_name) self._add_file_type_filter(ext) + self.file_type_counts[ext] = self.file_type_counts.get(ext, 0) + 1 + cb = self.file_type_widgets.get(ext) + if cb: + label = ext if ext else "(no ext)" + cb.configure(text=f"{label} ({self.file_type_counts[ext]})") var = self.file_types.get(ext) if var is not None and not var.get(): return - icon, group = self._file_icon_and_group(file_name, file_type) - display_type = group if not file_type else f"{group} | {file_type}" - - node_id = self.tree.insert( - parent_id, - "end", - text=f"{icon} {file_name}", - values=(size or "", display_type, full_path or ""), - tags=("file", ext), + + _icon, group = self._file_icon_and_group(file_name, file_type) + node_id = self._next_node_id() + node = TreeNode( + node_id=node_id, + parent_id=parent_id, + name=file_name, + kind="file", + full_path=full_path or "", + size=size or "", + file_type=file_type or "", + icon_group=group, + checked=full_path in self.checked_items, ) - if full_path and full_path in self.checked_items: - self._set_item_checked_visual(node_id, True) - tags = list(self.tree.item(node_id, "tags")) - if "checked" not in tags: - tags.append("checked") - self.tree.item(node_id, tags=tuple(tags)) + self.tree_nodes[node_id] = node + if parent_id: + self.tree_nodes[parent_id].children.append(node_id) + else: + self.tree_roots.append(node_id) + + self._rebuild_visible() + self._sync_rows() def _file_icon_and_group(self, file_name: str, file_type: str | None): ext = normalize_extension(file_name) From c0959fc3b2fb2c7bc2fa7128ee07c3d0caf95e47 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 18:41:03 +0800 Subject: [PATCH 33/55] fix: atomic check-create-register in add_folder to prevent TOCTOU race --- ui_ctk.py | 44 ++++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/ui_ctk.py b/ui_ctk.py index bfd33da..ff9e962 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -1085,30 +1085,26 @@ def add_folder(self, dir_path: str, url: str) -> str: current_path = current_path + "/" + part with self.folders_dict_lock: existing_id = self.folders.get(current_path) - if existing_id: - parent_id = existing_id - continue - - node_id = self._next_node_id() - node = TreeNode( - node_id=node_id, - parent_id=parent_id, - name=part, - kind="folder", - full_path="", - size="", - file_type="", - icon_group="folder", - ) - self.tree_nodes[node_id] = node - if parent_id: - self.tree_nodes[parent_id].children.append(node_id) - else: - self.tree_roots.append(node_id) - - with self.folders_dict_lock: - self.folders[current_path] = node_id - parent_id = node_id + if not existing_id: + node_id = self._next_node_id() + node = TreeNode( + node_id=node_id, + parent_id=parent_id, + name=part, + kind="folder", + full_path="", + size="", + file_type="", + icon_group="folder", + ) + self.tree_nodes[node_id] = node + if parent_id: + self.tree_nodes[parent_id].children.append(node_id) + else: + self.tree_roots.append(node_id) + self.folders[current_path] = node_id + existing_id = node_id + parent_id = existing_id self._rebuild_visible() self._sync_rows() From 81e30813244644c54bed03e2801b15eb1fb889ae Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 18:43:04 +0800 Subject: [PATCH 34/55] feat: rewrite toggle_check, select_all, deselect_all; remove Treeview helper methods Co-Authored-By: Claude Sonnet 4.6 --- ui_ctk.py | 167 +++++++++++------------------------------------------- 1 file changed, 34 insertions(+), 133 deletions(-) diff --git a/ui_ctk.py b/ui_ctk.py index ff9e962..6669d45 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -805,152 +805,53 @@ def _set_status(self, text: str, color: str = "#059669") -> None: # --- Treeview interaction helpers --- - def _all_tree_items(self): - out = [] - stack = list(self.tree.get_children("")) - while stack: - item = stack.pop() - out.append(item) - stack.extend(self.tree.get_children(item)) + def _all_tree_items(self) -> list[str]: + """Return all node_ids in DFS order.""" + out: list[str] = [] + def _walk(node_id: str) -> None: + out.append(node_id) + node = self.tree_nodes.get(node_id) + if node: + for child_id in node.children: + _walk(child_id) + for root_id in self.tree_roots: + _walk(root_id) return out - def _focused_tree_item(self): - try: - item = self.tree.focus() - if item and self.tree.exists(item): - return item - selected = self.tree.selection() - if selected: - return selected[0] - except tk.TclError: - return "" - return "" - - def _strip_checkmark(self, text: str) -> str: - if text.startswith(self.checkbox_checked): - return text[len(self.checkbox_checked):] - return text - - def _set_item_checked_visual(self, item: str, checked: bool) -> None: - try: - text = self.tree.item(item, "text") - except tk.TclError: - return - raw = self._strip_checkmark(text or "") - display = f"{self.checkbox_checked}{raw}" if checked else raw - try: - self.tree.item(item, text=display) - except tk.TclError: - return - - def toggle_check(self, item, force_check=None): - try: - if not self.tree.exists(item): - return - tags = list(self.tree.item(item, "tags")) - except tk.TclError: - return - - is_checked = "checked" in tags - new_checked = (not is_checked) if force_check is None else bool(force_check) - - if "file" in tags: - full_path = self.tree.set(item, "full_path") - if full_path: - if new_checked: - self.checked_items.add(full_path) - else: - self.checked_items.discard(full_path) - - if new_checked and "checked" not in tags: - tags.append("checked") - if (not new_checked) and "checked" in tags: - tags.remove("checked") - self.tree.item(item, tags=tuple(tags)) - self._set_item_checked_visual(item, new_checked) - - if "folder" in tags: - for child in self.tree.get_children(item): - self.toggle_check(child, force_check=new_checked) - - def on_tree_click(self, event): - try: - item = self.tree.identify("item", event.x, event.y) - if not item: - return - self.drag_anchor_item = item - self.tree.focus(item) - element = self.tree.identify_element(event.x, event.y) - if "indicator" in element: - return - modifier_mask = 0x0001 | 0x0004 | 0x0008 | 0x0010 | 0x0080 - if event.state & modifier_mask: - return - if self.tree.identify_column(event.x) == "#0": - self.toggle_check(item) - except tk.TclError: - return - - def on_tree_drag_select(self, event): - try: - if not self.drag_anchor_item: - return - target = self.tree.identify_row(event.y) - if not target: - return - items = self._all_tree_items() - if self.drag_anchor_item not in items or target not in items: - return - a = items.index(self.drag_anchor_item) - b = items.index(target) - lo, hi = (a, b) if a <= b else (b, a) - self.tree.selection_set(items[lo: hi + 1]) - except tk.TclError: + def toggle_check(self, node_id: str, force_check=None, _skip_children: bool = False) -> None: + node = self.tree_nodes.get(node_id) + if node is None: return + new_checked = (not node.checked) if force_check is None else bool(force_check) + node.checked = new_checked - def on_tree_space(self, _event=None): - item = self._focused_tree_item() - if item: - self.toggle_check(item) - return "break" - return None - - def on_tree_enter(self, _event=None): - item = self._focused_tree_item() - if not item: - return None - try: - tags = self.tree.item(item, "tags") - if "folder" in tags: - self.tree.item(item, open=not bool(self.tree.item(item, "open"))) + if node.kind == "file" and node.full_path: + if new_checked: + self.checked_items.add(node.full_path) else: - self.toggle_check(item) - except tk.TclError: - return None - return "break" + self.checked_items.discard(node.full_path) - def _on_tree_select_all(self, _event=None): - self.select_all() - return "break" + row = self._row_widgets.get(node_id) + if row: + row.set_checked(new_checked) + + if node.kind == "folder" and not _skip_children: + for child_id in node.children: + self.toggle_check(child_id, force_check=new_checked) - def show_context_menu(self, event): + def show_context_menu(self, event) -> None: try: - item = self.tree.identify_row(event.y) - if item: - self.tree.focus(item) - self.tree.selection_set(item) self.context_menu.tk_popup(event.x_root, event.y_root) finally: self.context_menu.grab_release() - return "break" - def select_all(self): - for item in self._all_tree_items(): - self.toggle_check(item, force_check=True) + def select_all(self) -> None: + for node_id in list(self.tree_nodes): + self.toggle_check(node_id, force_check=True, _skip_children=True) - def deselect_all(self): - for item in self._all_tree_items(): - self.toggle_check(item, force_check=False) + def deselect_all(self) -> None: + for node_id in list(self.tree_nodes): + self.toggle_check(node_id, force_check=False, _skip_children=True) def expand_all(self, parent=""): for item in self.tree.get_children(parent): From b789ff409b3f879c2a664cdd939fdf89bbd6c06f Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 18:46:28 +0800 Subject: [PATCH 35/55] feat: rewrite expand_all, collapse_all, sort_tree, type filter using TreeNode Co-Authored-By: Claude Sonnet 4.6 --- ui_ctk.py | 85 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 25 deletions(-) diff --git a/ui_ctk.py b/ui_ctk.py index 6669d45..b65b07b 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -853,34 +853,55 @@ def deselect_all(self) -> None: for node_id in list(self.tree_nodes): self.toggle_check(node_id, force_check=False, _skip_children=True) - def expand_all(self, parent=""): - for item in self.tree.get_children(parent): - self.tree.item(item, open=True) - self.expand_all(item) - - def collapse_all(self, parent=""): - for item in self.tree.get_children(parent): - self.tree.item(item, open=False) - self.collapse_all(item) - - def sort_tree(self, col: str) -> None: - def get_key(item_id: str) -> str: - if col == "#0": - return self._strip_checkmark(self.tree.item(item_id, "text")).lower() - return self.tree.set(item_id, col).lower() - - def sort_children(parent: str) -> None: - children = list(self.tree.get_children(parent)) + def expand_all(self, parent: str = "") -> None: + targets = self.tree_roots if not parent else ( + self.tree_nodes[parent].children if parent in self.tree_nodes else [] + ) + for node_id in targets: + node = self.tree_nodes.get(node_id) + if node and node.kind == "folder": + node.expanded = True + self.expand_all(node_id) + if not parent: # Only rebuild once at top-level call + self._rebuild_visible() + self._sync_rows() + + def collapse_all(self, parent: str = "") -> None: + targets = self.tree_roots if not parent else ( + self.tree_nodes[parent].children if parent in self.tree_nodes else [] + ) + for node_id in targets: + node = self.tree_nodes.get(node_id) + if node and node.kind == "folder": + node.expanded = False + self.collapse_all(node_id) + if not parent: # Only rebuild once at top-level call + self._rebuild_visible() + self._sync_rows() + + def sort_tree(self, col: str = "name") -> None: + def sort_children(children: list[str]) -> None: if not children: return - children.sort(key=get_key, reverse=self.sort_reverse) - for idx, item in enumerate(children): - self.tree.move(item, parent, idx) - for item in children: - sort_children(item) - - sort_children("") + def key(node_id: str) -> str: + node = self.tree_nodes.get(node_id) + if node is None: + return "" + if col == "size": + return node.size.lower() + if col == "type": + return node.icon_group.lower() + return node.name.lower() + children.sort(key=key, reverse=self.sort_reverse) + for node_id in children: + node = self.tree_nodes.get(node_id) + if node: + sort_children(node.children) + + sort_children(self.tree_roots) self.sort_reverse = not self.sort_reverse + self._rebuild_visible() + self._sync_rows() # --- Status color mapping --- @@ -1309,3 +1330,17 @@ def _add_file_type_filter(self, ext: str) -> None: ) cb.pack(side="left", padx=4, pady=4) self.file_type_widgets[ext] = cb + + def _on_type_filter_changed(self, ext: str) -> None: + """Show or hide existing tree rows when a file-type checkbox is toggled.""" + if self.full_tree_backup: + return # search filter active; handled by _filter_tree_by_term + var = self.file_types.get(ext) + if var is None: + return + visible = var.get() + for node in self.tree_nodes.values(): + if node.kind == "file" and normalize_extension(node.name) == ext: + node.hidden = not visible + self._rebuild_visible() + self._sync_rows() From fd5e508dbc9821c02bc41373c087173b8d31de74 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 18:49:36 +0800 Subject: [PATCH 36/55] feat: rewrite _backup_full_tree, _restore_full_tree, _filter_tree_by_term using TreeNode model --- ui_ctk.py | 93 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/ui_ctk.py b/ui_ctk.py index b65b07b..0bf22dd 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -1,5 +1,6 @@ from __future__ import annotations +import copy import os import threading import time @@ -1230,56 +1231,60 @@ def _apply_search_filter(self, query: str) -> None: self._filter_tree_by_term(term) def _backup_full_tree(self) -> None: - self.full_tree_backup.clear() - for item in self._all_tree_items(): - self.full_tree_backup[item] = { - "parent": self.tree.parent(item), - "index": self.tree.index(item), - "text": self.tree.item(item, "text"), - "values": self.tree.item(item, "values"), - "tags": self.tree.item(item, "tags"), - "open": self.tree.item(item, "open"), - } - # Snapshot folders so _restore_full_tree can rebuild the mapping. - with self.folders_dict_lock: - self._folders_backup = dict(self.folders) + """Snapshot tree_nodes and tree_roots for search filter restore.""" + self.full_tree_backup = { + "nodes": copy.deepcopy(self.tree_nodes), + "roots": list(self.tree_roots), + "folders": dict(self.folders), + } def _restore_full_tree(self) -> None: - backup = dict(self.full_tree_backup) - self.full_tree_backup.clear() - folders_snapshot = getattr(self, "_folders_backup", {}) - self._folders_backup = {} - - for item in self.tree.get_children(""): - self.tree.delete(item) + """Restore tree_nodes from backup (undo search filter).""" + if not self.full_tree_backup: + return + self.tree_nodes = self.full_tree_backup["nodes"] + self.tree_roots = self.full_tree_backup["roots"] with self.folders_dict_lock: - self.folders.clear() - - items_sorted = sorted(backup.items(), key=lambda kv: (kv[1]["parent"], kv[1]["index"])) - id_map = {} - for old_id, data in items_sorted: - parent_old = data["parent"] - parent_new = id_map.get(parent_old, "") if parent_old else "" - new_id = self.tree.insert(parent_new, "end", text=data["text"], values=data["values"], tags=data["tags"]) - self.tree.item(new_id, open=data["open"]) - id_map[old_id] = new_id + self.folders = self.full_tree_backup["folders"] + self.full_tree_backup = {} - # Rebuild self.folders by remapping old item IDs → new item IDs. - with self.folders_dict_lock: - for folder_path, old_id in folders_snapshot.items(): - new_id = id_map.get(old_id) - if new_id: - self.folders[folder_path] = new_id + # Destroy all existing row widgets and rebuild from scratch + for row in self._row_widgets.values(): + row.destroy() + self._row_widgets.clear() + self._rebuild_visible() + self._sync_rows() def _filter_tree_by_term(self, term: str) -> None: - for item in self._all_tree_items(): - text = (self.tree.item(item, "text") or "").lower() - if term in text: - continue - try: - self.tree.detach(item) - except tk.TclError: - pass + """Mark nodes hidden if they (and their descendants) don't match term.""" + term = term.lower() + + def matches(node_id: str) -> bool: + node = self.tree_nodes.get(node_id) + if node is None: + return False + if term in node.name.lower() or term in node.full_path.lower(): + return True + return any(matches(child_id) for child_id in node.children) + + def apply_visibility(node_id: str) -> None: + node = self.tree_nodes.get(node_id) + if node is None: + return + if matches(node_id): + node.hidden = False + if node.kind == "folder": + node.expanded = True # auto-expand matching folders + else: + node.hidden = True + for child_id in node.children: + apply_visibility(child_id) + + for root_id in self.tree_roots: + apply_visibility(root_id) + + self._rebuild_visible() + self._sync_rows() def choose_download_path(self) -> None: path = filedialog.askdirectory(title="Choose Download Location") From 5af108bebb1ac25443dd557b4de23df7fc348750 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 19:26:12 +0800 Subject: [PATCH 37/55] feat: complete FileTree migration - remove all ttk.Treeview references - Rewrote clear_scan_results to use tree_nodes/tree_roots/row_widgets instead of self.tree.get_children/delete; adds _drain_queues, resets _node_counter, scanned_urls, total_urls, and full_tree_backup - Rewrote download_selected to use self.checked_items set directly instead of self.tree.item/set/selection Treeview API calls - Removed dead state: self.checkbox_checked and self.drag_anchor_item (two assignments in __init__ and _build_full_ui) Co-Authored-By: Claude Sonnet 4.6 --- ui_ctk.py | 96 +++++++++++++++++++++---------------------------------- 1 file changed, 36 insertions(+), 60 deletions(-) diff --git a/ui_ctk.py b/ui_ctk.py index 0bf22dd..c0c010e 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -225,7 +225,6 @@ def __init__(self, ui_smoke: bool = False): self.files_dict: dict[str, dict] = {} self.folders: dict[str, str] = {} self.checked_items: set[str] = set() - self.checkbox_checked = "✔ " # FileTree data model self.tree_nodes: dict[str, TreeNode] = {} @@ -300,7 +299,6 @@ def _build_ui_smoke_only(self) -> None: self.tree_scroll_frame = None self.full_tree_backup = {} self.sort_reverse = False - self.drag_anchor_item = "" self._last_toggle_time = 0.0 self._search_after_id = None self.tree_nodes = {} @@ -321,7 +319,6 @@ def _build_full_ui(self) -> None: self.sort_reverse = False self.full_tree_backup = {} - self.drag_anchor_item = "" self.context_menu = tk.Menu(self.window, tearoff=0) self.context_menu.add_command(label="Select All", command=self.select_all) @@ -970,12 +967,24 @@ def toggle_scan_pause(self) -> None: def clear_scan_results(self) -> None: if self.is_scanning: return - for item in self.tree.get_children(""): - self.tree.delete(item) - with self.files_dict_lock: - self.files_dict.clear() + self._drain_queues() + + # Clear data model + self.tree_nodes.clear() + self.tree_roots.clear() + self._node_counter = 0 with self.folders_dict_lock: self.folders.clear() + + # Destroy row widgets + for row in self._row_widgets.values(): + row.destroy() + self._row_widgets.clear() + self._visible_nodes.clear() + + # Clear other state + with self.files_dict_lock: + self.files_dict.clear() if hasattr(self, "filters_container"): for widget in self.filters_container.winfo_children(): widget.destroy() @@ -983,6 +992,9 @@ def clear_scan_results(self) -> None: self.file_type_counts.clear() self.file_type_widgets.clear() self.checked_items.clear() + self.full_tree_backup = {} + self.scanned_urls = 0 + self.total_urls = 0 self.progress_bar.set(0) self.progress_label.configure(text="") self._set_status("Ready", "#059669") @@ -1127,63 +1139,21 @@ def download_selected(self) -> None: if not self.download_path: url = self.url_var.get().strip() if url: - try: - self.download_path = default_download_folder(url, os.getcwd()) - except Exception: - self.download_path = os.path.join(os.getcwd(), "downloads") - else: - self.download_path = os.path.join(os.getcwd(), "downloads") - - os.makedirs(self.download_path, exist_ok=True) - - # Step 1: Collect candidate full_paths from the treeview without holding - # files_dict_lock, since treeview operations must happen on the UI thread - # and should not be interleaved with background lock-holding work. - checked_paths: list[str] = [] - for item_id in self._all_tree_items(): - try: - tags = self.tree.item(item_id, "tags") - except tk.TclError: - continue - if "checked" in tags and "file" in tags: - full_path = self.tree.set(item_id, "full_path") - if full_path: - checked_paths.append(full_path) - - # Fall back to treeview selection when nothing is checked. - if not checked_paths: - for item_id in list(self.tree.selection()): - try: - tags = self.tree.item(item_id, "tags") - except tk.TclError: - continue - if "file" not in tags: - continue - full_path = self.tree.set(item_id, "full_path") - if full_path: - checked_paths.append(full_path) - - if not checked_paths: - self.notify_info("Info", "No selected files to download") - return - - # Step 2: Hold lock only for the brief dict look-ups; copy info so the - # lock is released before any further work. - selected: list[tuple[str, dict]] = [] - with self.files_dict_lock: - for full_path in checked_paths: - info = self.files_dict.get(full_path) - if info: - selected.append((full_path, dict(info))) + self.download_path = default_download_folder(url, os.getcwd()) + if not self.download_path: + self.notify_warning("Warning", "Please choose a download location first.") + return - if not selected: - self.notify_info("Info", "No selected files to download") + selected_paths = list(self.checked_items) + if not selected_paths: + self.notify_info("Info", "No files selected for download.") return - # Step 3: Submit download tasks without holding the lock. - self.pause_btn.configure(state="normal") futures = [] - for full_path, info in selected: + for full_path in selected_paths: + info = self.files_dict.get(full_path) + if not info: + continue url = info.get("url") file_name = info.get("file_name") if not url or not file_name: @@ -1195,6 +1165,12 @@ def download_selected(self) -> None: cancel_event = self.downloads_panel.ensure(file_path, safe_name) futures.append(self.executor.submit(self.backend.download_file, url, file_path, safe_name, cancel_event)) + if not futures: + return + try: + self.pause_btn.configure(state="normal") + except Exception: + pass monitor = threading.Thread(target=self.backend.monitor_downloads, args=(futures,), daemon=True) monitor.start() From 200e52b26bb4d658b4fe67f54d41f343345e8634 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 19:36:01 +0800 Subject: [PATCH 38/55] fix: restore _drain_queues method and wire _on_type_filter_changed to checkbox --- ui_ctk.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ui_ctk.py b/ui_ctk.py index c0c010e..f771cdb 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -964,6 +964,21 @@ def toggle_scan_pause(self) -> None: self.scan_pause_btn.configure(text="Pause Scan", state="normal") self._set_status("Scanning", "#0284C7") + def _drain_queues(self) -> None: + """Discard all pending items in scan queues and cancel any scheduled flush.""" + if self.scan_flush_job is not None: + try: + self.window.after_cancel(self.scan_flush_job) + except Exception: + pass + self.scan_flush_job = None + for q in (self.scan_item_buffer, self.dir_queue, self.file_queue): + while True: + try: + q.get_nowait() + except Exception: + break + def clear_scan_results(self) -> None: if self.is_scanning: return @@ -1310,6 +1325,7 @@ def _add_file_type_filter(self, ext: str) -> None: offvalue=False, ) cb.pack(side="left", padx=4, pady=4) + var.trace_add("write", lambda *_: self._on_type_filter_changed(ext)) self.file_type_widgets[ext] = cb def _on_type_filter_changed(self, ext: str) -> None: From c5291c7be973a7a3aa3a38621fde918cfb1c92f2 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 21:16:30 +0800 Subject: [PATCH 39/55] =?UTF-8?q?feat:=20UI=20=E9=87=8D=E6=A7=8B=20?= =?UTF-8?q?=E2=80=94=20=E6=96=B0=E5=A2=9E=E6=8E=83=E6=8F=8F=20hook?= =?UTF-8?q?=E3=80=81=E7=89=88=E9=9D=A2=E9=87=8D=E6=8E=92=E3=80=81=E5=AD=97?= =?UTF-8?q?=E9=AB=94=E8=AA=BF=E6=95=B4=E8=88=87=E6=BB=BE=E8=BC=AA=E6=94=AF?= =?UTF-8?q?=E6=8F=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 on_scan_item/on_scan_started/on_scan_progress/on_scan_finished hook, 修復掃描結果無法顯示及進度條不動的問題 - 修正 _sync_rows 使用 winfo_children() 導致畫面空白的 bug - 新增 _schedule_tree_update 批次更新,解決掃描時 O(n²) 閃爍問題 - 版面重排:Row1 動作按鈕列、Row2 Types 橫向 checkboxes、Row3 檔案樹 - Types checkbox 區支援滑鼠滾輪橫向捲動(xscrollincrement=20) - 修復 Cmd+V 無法貼上:改由 window 層轉發 <>/<> 等虛擬事件 - 修正點擊資料夾列觸發勾選(而非展開/收合) - 新增 Select All / Deselect All 按鈕及 Ctrl+A 快捷鍵 - 字體統一放大(名稱 13→14、emoji 16→18、列高 34→38px) - 新增 pyproject.toml,修復 uv run 無法執行問題 Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 14 +++ ui_ctk.py | 326 +++++++++++++++++++++++++++++++++++-------------- uv.lock | 302 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 546 insertions(+), 96 deletions(-) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0cb71f6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "index-ripper" +version = "0.1.0" +requires-python = ">=3.11" +dependencies = [ + "requests>=2.31.0,<3.0.0", + "beautifulsoup4>=4.12.0,<5.0.0", + "urllib3>=2.0.0,<3.0.0", + "customtkinter>=5.2.0,<6.0.0", + "ttkbootstrap>=1.10.1,<2.0.0", +] + +[project.scripts] +index-ripper = "ui_ctk:main" diff --git a/ui_ctk.py b/ui_ctk.py index f771cdb..ddcb763 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -64,7 +64,7 @@ class RowWidget: """One visible row in the FileTree.""" INDENT_PX = 20 - ROW_HEIGHT = 34 + ROW_HEIGHT = 38 def __init__(self, parent, app, node: TreeNode, depth: int): self.app = app @@ -102,8 +102,8 @@ def __init__(self, parent, app, node: TreeNode, depth: int): ctk.CTkLabel( self.frame, text=_EMOJI_ICONS.get(node.icon_group, "📄"), - font=ctk.CTkFont(size=16), - width=28, + font=ctk.CTkFont(size=18), + width=30, ).pack(side="left", padx=(2, 4)) # Name label @@ -111,7 +111,7 @@ def __init__(self, parent, app, node: TreeNode, depth: int): self.frame, text=node.name, anchor="w", - font=ctk.CTkFont(size=13, weight="bold" if node.kind == "folder" else "normal"), + font=ctk.CTkFont(size=14, weight="bold" if node.kind == "folder" else "normal"), ) self.name_label.pack(side="left", fill="x", expand=True) @@ -121,18 +121,18 @@ def __init__(self, parent, app, node: TreeNode, depth: int): ctk.CTkLabel( self.frame, text=node.size, - font=ctk.CTkFont(size=11), + font=ctk.CTkFont(size=12), text_color=("gray50", "gray60"), - width=80, + width=90, anchor="e", ).pack(side="right", padx=(0, 4)) if node.icon_group and node.icon_group != "binary": ctk.CTkLabel( self.frame, text=node.icon_group, - font=ctk.CTkFont(size=10), + font=ctk.CTkFont(size=11), text_color=("gray50", "gray60"), - width=60, + width=65, anchor="e", ).pack(side="right", padx=(0, 2)) @@ -231,6 +231,7 @@ def __init__(self, ui_smoke: bool = False): self.tree_roots: list[str] = [] # top-level node_ids in insertion order self._node_counter: int = 0 # monotonic counter for unique node_ids self._last_toggle_time: float = 0.0 # debounce timestamp for row clicks + self._tree_update_pending: bool = False # debounce flag for _schedule_tree_update self.dir_queue = Queue() self.file_queue = Queue() @@ -274,11 +275,29 @@ def __init__(self, ui_smoke: bool = False): self.window.bind("", self.focus_search) self.window.bind("", self.focus_logs) self.window.bind("", self.clear_search) - self.window.bind_all("", self._on_global_url_paste, add="+") - self.window.bind_all("", self._on_global_url_paste, add="+") - self.window.bind_all("", self._on_global_url_paste, add="+") - self.window.bind_all("", self._on_global_url_paste, add="+") - self.window.bind_all("<>", self._on_global_url_paste, add="+") + # macOS: forward Cmd+V / Cmd+C / Cmd+X / Cmd+A to the focused widget + # so native clipboard shortcuts work in every CTkEntry / CTkTextbox. + for seq, virt in ( + ("", "<>"), + ("", "<>"), + ("", "<>"), + ("", "<>"), + ("", "<>"), + ("", "<>"), + ("", "<>"), + ("", "<>"), + ): + virt_local = virt # capture for lambda + + def _fwd(e, _v=virt_local): + w = e.widget + try: + w.event_generate(_v) + except Exception: + pass + return "break" + + self.window.bind(seq, _fwd, add="+") def _build_ui(self) -> None: if self._ui_smoke: @@ -308,7 +327,8 @@ def _build_ui_smoke_only(self) -> None: def _build_full_ui(self) -> None: self.window.grid_columnconfigure(0, weight=1) - self.window.grid_rowconfigure(2, weight=1) + self.window.grid_rowconfigure(3, weight=1) # filetree expands (row shifted by types row) + self.window.grid_rowconfigure(5, weight=0) # panels fixed self._build_header() self._build_filters_row() @@ -332,15 +352,22 @@ def _build_header(self) -> None: header.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 0)) header.grid_columnconfigure(1, weight=1) - ctk.CTkLabel(header, text="URL", font=ctk.CTkFont(weight="bold")).grid( - row=0, column=0, sticky="w", padx=(0, 8) - ) + ctk.CTkLabel( + header, text="URL", font=ctk.CTkFont(size=14, weight="bold") + ).grid(row=0, column=0, sticky="w", padx=(0, 8)) self.url_var = tk.StringVar() - self.url_entry = ctk.CTkEntry(header, textvariable=self.url_var, placeholder_text="https://") + self.url_entry = ctk.CTkEntry( + header, textvariable=self.url_var, + placeholder_text="https://", + font=ctk.CTkFont(size=14), + height=36, + ) self.url_entry.grid(row=0, column=1, sticky="ew", padx=(0, 8)) - self.status_label = ctk.CTkLabel(header, text="Ready", text_color="#059669") + self.status_label = ctk.CTkLabel( + header, text="Ready", text_color="#059669", font=ctk.CTkFont(size=13) + ) self.status_label.grid(row=0, column=2, sticky="e", padx=(0, 8)) actions = ctk.CTkFrame(header, fg_color="transparent") @@ -370,11 +397,6 @@ def _build_header(self) -> None: self.url_context_menu = tk.Menu(self.window, tearoff=0) self.url_context_menu.add_command(label="Paste", command=self._paste_into_url_entry) - self.url_entry.bind("", self._on_url_paste) - self.url_entry.bind("", self._on_url_paste) - self.url_entry.bind("", self._on_url_paste) - self.url_entry.bind("", self._on_url_paste) - self.url_entry.bind("", self._on_url_paste) self.url_entry.bind("", self._show_url_context_menu) self.url_entry.bind("", self._show_url_context_menu) @@ -423,6 +445,25 @@ def notify_error(self, title: str, message: str) -> None: # --- Queue polling (thread-safe) --- + def on_scan_item( + self, + *, + is_directory: bool, + path: str, + url: str, + file_name: str = "", + size: str = "", + file_type: str = "", + full_path: str = "", + ) -> None: + """Backend hook — called from background thread when an item is found.""" + if not self.is_scanning: + return + self.scan_item_buffer.put( + (is_directory, path, url, file_name, size, file_type, full_path) + ) + self.window.after(0, self._schedule_flush) + def _schedule_flush(self) -> None: """Schedule a deferred flush of scan_item_buffer if not already pending.""" if self.scan_flush_job is not None: @@ -513,47 +554,46 @@ def _poll_file_queue(self) -> None: # --- Stub methods (implemented in later Tasks) --- def _build_filters_row(self) -> None: + # ── Row 1: action buttons ────────────────────────────────────────── self._ctrl_row_frame = ctk.CTkFrame(self.window, fg_color="transparent") - self._ctrl_row_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=(4, 0)) + self._ctrl_row_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=(6, 0)) self._ctrl_row_frame.grid_columnconfigure(0, weight=1) - filters_frame = ctk.CTkFrame(self._ctrl_row_frame, fg_color="transparent") - filters_frame.grid(row=0, column=0, sticky="ew") - - self.filters_container = ctk.CTkScrollableFrame( - filters_frame, - height=70, - orientation="horizontal", - fg_color="transparent", - ) - self.filters_container.pack(fill="x") - - type_actions = ctk.CTkFrame(filters_frame, fg_color="transparent") - type_actions.pack(fill="x", pady=(4, 0)) + # ── Row 2: Types label + checkboxes + type-action buttons ────────── + types_row = ctk.CTkFrame(self.window, fg_color="transparent") + types_row.grid(row=2, column=0, sticky="ew", padx=10, pady=(4, 0)) + types_row.grid_columnconfigure(2, weight=1) # checkboxes column expands - ctk.CTkButton( - type_actions, - text="Select All Types", + _s = dict( # shared small-button style fg_color=("gray80", "gray25"), text_color=("gray10", "gray90"), hover_color=("gray70", "gray35"), - command=self.select_all_types, - width=140, - ).pack(side="left", padx=(0, 6)) + height=28, + font=ctk.CTkFont(size=12), + ) - ctk.CTkButton( - type_actions, - text="Deselect All Types", - fg_color=("gray80", "gray25"), - text_color=("gray10", "gray90"), - hover_color=("gray70", "gray35"), - command=self.deselect_all_types, - width=150, - ).pack(side="left") + ctk.CTkLabel( + types_row, text="Types:", + font=ctk.CTkFont(size=13, weight="bold"), + ).grid(row=0, column=0, sticky="w", padx=(0, 6)) + + type_btns = ctk.CTkFrame(types_row, fg_color="transparent") + type_btns.grid(row=0, column=1, sticky="w", padx=(0, 8)) + ctk.CTkButton(type_btns, text="✓ All", command=self.select_all_types, width=68, **_s).pack(side="left", padx=(0, 4)) + ctk.CTkButton(type_btns, text="✗ All", command=self.deselect_all_types, width=68, **_s).pack(side="left") + + self.filters_container = ctk.CTkScrollableFrame( + types_row, + height=38, + orientation="horizontal", + fg_color="transparent", + ) + self.filters_container.grid(row=0, column=2, sticky="ew") + self._bind_hscroll_wheel(self.filters_container) def _build_filetree(self) -> None: outer = ctk.CTkFrame(self.window, fg_color="transparent") - outer.grid(row=2, column=0, sticky="nsew", padx=10, pady=(4, 0)) + outer.grid(row=3, column=0, sticky="nsew", padx=10, pady=(4, 0)) outer.grid_columnconfigure(0, weight=1) outer.grid_rowconfigure(1, weight=1) @@ -574,10 +614,15 @@ def _build_filetree(self) -> None: corner_radius=8, ) self.tree_scroll_frame.grid(row=1, column=0, sticky="nsew") + self.tree_scroll_frame.bind("", self.show_context_menu) + self.tree_scroll_frame.bind("", self.show_context_menu) + self.window.bind("", lambda _e: self.select_all()) + self.window.bind("", lambda _e: self.select_all()) # Runtime state for view layer self._visible_nodes: list[str] = [] self._row_widgets: dict[str, RowWidget] = {} + self._last_visible: list[str] = [] def _rebuild_visible(self) -> None: """Recompute self._visible_nodes from data model (DFS, respects expanded/hidden).""" @@ -608,19 +653,47 @@ def _sync_rows(self) -> None: if node_id not in new_ids: self._row_widgets.pop(node_id).destroy() - # Repack all rows in correct order - for widget in self.tree_scroll_frame.winfo_children(): - widget.pack_forget() - - for node_id in self._visible_nodes: - node = self.tree_nodes[node_id] - depth = self._node_depth(node_id) - if node_id not in self._row_widgets: + prev = self._last_visible + # Append-only optimisation: if the existing rows are still a prefix of + # the new list (no reordering), just pack the new rows at the end. + # This prevents the pack_forget→repack flash during scanning. + if ( + len(self._visible_nodes) >= len(prev) + and self._visible_nodes[: len(prev)] == prev + and all(nid in self._row_widgets for nid in prev) + ): + for node_id in self._visible_nodes[len(prev) :]: + node = self.tree_nodes[node_id] + depth = self._node_depth(node_id) self._row_widgets[node_id] = RowWidget( self.tree_scroll_frame, self, node, depth ) - else: - self._row_widgets[node_id].frame.pack(fill="x", padx=4, pady=1) + else: + # Full repack needed (expand/collapse, filter, sort, restore) + for row in list(self._row_widgets.values()): + row.frame.pack_forget() + for node_id in self._visible_nodes: + node = self.tree_nodes[node_id] + depth = self._node_depth(node_id) + if node_id not in self._row_widgets: + self._row_widgets[node_id] = RowWidget( + self.tree_scroll_frame, self, node, depth + ) + else: + self._row_widgets[node_id].frame.pack(fill="x", padx=4, pady=1) + + self._last_visible = list(self._visible_nodes) + + def _schedule_tree_update(self) -> None: + """Debounce _rebuild_visible + _sync_rows to avoid O(n²) during bulk add.""" + if not self._tree_update_pending: + self._tree_update_pending = True + self.window.after(50, self._do_tree_update) + + def _do_tree_update(self) -> None: + self._tree_update_pending = False + self._rebuild_visible() + self._sync_rows() def _node_depth(self, node_id: str) -> int: depth = 0 @@ -642,10 +715,7 @@ def _on_row_click(self, node_id: str, event=None) -> None: node = self.tree_nodes.get(node_id) if node is None: return - if node.kind == "folder": - self._on_chevron_click(node_id) - else: - self.toggle_check(node_id) + self.toggle_check(node_id) def _on_chevron_click(self, node_id: str) -> None: node = self.tree_nodes.get(node_id) @@ -660,19 +730,23 @@ def _on_chevron_click(self, node_id: str) -> None: def _build_progress_section(self) -> None: progress_frame = ctk.CTkFrame(self.window, fg_color="transparent") - progress_frame.grid(row=3, column=0, sticky="ew", padx=10, pady=(4, 0)) + progress_frame.grid(row=4, column=0, sticky="ew", padx=10, pady=(6, 2)) progress_frame.grid_columnconfigure(0, weight=1) - self.progress_bar = ctk.CTkProgressBar(progress_frame) + self.progress_bar = ctk.CTkProgressBar(progress_frame, height=14, corner_radius=6) self.progress_bar.set(0) - self.progress_bar.grid(row=0, column=0, sticky="ew") + self.progress_bar.grid(row=0, column=0, sticky="ew", pady=(0, 4)) - self.progress_label = ctk.CTkLabel(progress_frame, text="") - self.progress_label.grid(row=1, column=0, sticky="w") + self.progress_label = ctk.CTkLabel( + progress_frame, text="", + font=ctk.CTkFont(size=13), + anchor="w", + ) + self.progress_label.grid(row=1, column=0, sticky="ew") def _build_panels(self) -> None: self.panels_notebook = ctk.CTkTabview(self.window, height=180) - self.panels_notebook.grid(row=4, column=0, sticky="ew", padx=10, pady=(4, 10)) + self.panels_notebook.grid(row=5, column=0, sticky="ew", padx=10, pady=(4, 10)) self._panels_widget = self.panels_notebook # toggle_panels 用 downloads_tab = self.panels_notebook.add("Downloads") @@ -694,32 +768,49 @@ def _build_panels(self) -> None: self.log_text.pack(fill="both", expand=True) def _build_download_controls(self) -> None: + _s = dict( + fg_color=("gray80", "gray25"), + text_color=("gray10", "gray90"), + hover_color=("gray70", "gray35"), + height=30, + ) + + # Left side of row 1: Select All / Deselect All (file tree) + sel_frame = ctk.CTkFrame(self._ctrl_row_frame, fg_color="transparent") + sel_frame.grid(row=0, column=0, sticky="w") + ctk.CTkButton(sel_frame, text="Select All", command=self.select_all, width=100, **_s).pack(side="left", padx=(0, 4)) + ctk.CTkButton(sel_frame, text="Deselect All", command=self.deselect_all, width=110, **_s).pack(side="left") + + # Right side of row 1: download controls controls = ctk.CTkFrame(self._ctrl_row_frame, fg_color="transparent") controls.grid(row=0, column=1, sticky="e", padx=(10, 0)) self.download_btn = ctk.CTkButton( - controls, text="Download Selected", command=self.download_selected + controls, text="⬇ Download Selected", command=self.download_selected, + height=30, ) - self.download_btn.grid(row=0, column=0, padx=4) + self.download_btn.grid(row=0, column=0, padx=(0, 4)) self.pause_btn = ctk.CTkButton( - controls, text="Pause", + controls, text="⏸ Pause", fg_color=("gray70", "gray30"), hover_color=("gray60", "gray40"), command=self.toggle_pause, state="disabled", + height=30, ) - self.pause_btn.grid(row=0, column=1, padx=4) + self.pause_btn.grid(row=0, column=1, padx=(0, 4)) self.path_btn = ctk.CTkButton( - controls, text="Choose Folder", + controls, text="📁 Folder", fg_color=("gray70", "gray30"), hover_color=("gray60", "gray40"), command=self.choose_download_path, + height=30, ) - self.path_btn.grid(row=0, column=2, padx=4) + self.path_btn.grid(row=0, column=2, padx=(0, 8)) - ctk.CTkLabel(controls, text="Threads").grid(row=0, column=3, padx=(12, 4)) + ctk.CTkLabel(controls, text="Threads", font=ctk.CTkFont(size=13)).grid(row=0, column=3, padx=(0, 4)) self.threads_var = tk.StringVar(value="5") self.threads_combo = ctk.CTkOptionMenu( @@ -728,19 +819,18 @@ def _build_download_controls(self) -> None: variable=self.threads_var, command=self.update_thread_count, width=70, + height=30, ) - self.threads_combo.grid(row=0, column=4) + self.threads_combo.grid(row=0, column=4, padx=(0, 8)) self.panels_visible = True self.toggle_panels_btn = ctk.CTkButton( controls, text="Hide Panels", - fg_color=("gray80", "gray25"), - text_color=("gray10", "gray90"), - hover_color=("gray70", "gray35"), command=self.toggle_panels, - width=110, + width=100, + **_s, ) - self.toggle_panels_btn.grid(row=0, column=5, padx=(10, 0)) + self.toggle_panels_btn.grid(row=0, column=5) def focus_search(self, event=None) -> None: try: @@ -983,6 +1073,8 @@ def clear_scan_results(self) -> None: if self.is_scanning: return self._drain_queues() + self._tree_update_pending = False + self._last_visible = [] # Clear data model self.tree_nodes.clear() @@ -1014,14 +1106,39 @@ def clear_scan_results(self) -> None: self.progress_label.configure(text="") self._set_status("Ready", "#059669") + def on_scan_started(self, *, url: str = "") -> None: + self.scan_btn.configure(text="Stop Scan") + self.scan_pause_btn.configure(state="normal") + self.progress_bar.set(0) + self.progress_label.configure(text="Scanning…") + self._set_status("Scanning", "#B45309") + + def on_scan_progress(self, *, scanned_urls: int = 0, total_urls: int = 0) -> None: + self.window.after(0, lambda: self.update_progress(scanned_urls, total_urls)) + + def on_scan_finished(self, *, stopped: bool = False) -> None: + def _finish(): + self.scan_btn.configure(text="Scan") + self.scan_pause_btn.configure(text="Pause Scan", state="disabled") + if stopped: + self.progress_bar.set(0) + self.progress_label.configure(text="Scan stopped") + self._set_status("Stopped", "#B91C1C") + else: + self.progress_bar.set(1) + n = len(self.files_dict) + self.progress_label.configure(text=f"Done — {n} files found") + self._set_status("Ready", "#059669") + self.window.after(0, _finish) + def update_progress(self, scanned: int, total: int) -> None: """Update the scan progress bar (0–100%).""" if total <= 0: self.progress_bar.set(0) return - pct = (scanned / total) * 100 - self.progress_bar.set(pct / 100) - self.progress_label.configure(text=f"Scan: {scanned}/{total}") + pct = scanned / total + self.progress_bar.set(pct) + self.progress_label.configure(text=f"Scanning… {scanned}/{total} ({pct:.0%})") def add_folder(self, dir_path: str, url: str) -> str: """Ensure all path segments exist as folder nodes; return leaf node_id.""" @@ -1056,8 +1173,7 @@ def add_folder(self, dir_path: str, url: str) -> str: existing_id = node_id parent_id = existing_id - self._rebuild_visible() - self._sync_rows() + self._schedule_tree_update() return parent_id def add_file(self, dir_path: str, url: str, file_name: str, size, file_type: str, full_path: str) -> None: @@ -1121,8 +1237,7 @@ def add_file(self, dir_path: str, url: str, file_name: str, size, file_type: str else: self.tree_roots.append(node_id) - self._rebuild_visible() - self._sync_rows() + self._schedule_tree_update() def _file_icon_and_group(self, file_name: str, file_type: str | None): ext = normalize_extension(file_name) @@ -1309,6 +1424,28 @@ def deselect_all_types(self) -> None: for var in self.file_types.values(): var.set(False) + def _bind_hscroll_wheel(self, widget) -> None: + """讓 widget 的滾輪事件橫向捲動 filters_container(平滑版)。""" + try: + canvas = self.filters_container._parent_canvas + except AttributeError: + return + + # 每個 scroll unit = 20px,讓滾動距離合理 + canvas.configure(xscrollincrement=20) + + def _scroll(event): + delta = getattr(event, "delta", 0) + if not delta: + return + # macOS trackpad delta 很小 (1–5);滑鼠滾輪約 120 + # 統一換算為 unit 數,最少 1 + units = max(1, abs(delta) // 3) + canvas.xview_scroll(-units if delta > 0 else units, "units") + + widget.bind("", _scroll, add="+") + widget.bind("", _scroll, add="+") + def _add_file_type_filter(self, ext: str) -> None: if not hasattr(self, "filters_container"): return @@ -1325,6 +1462,7 @@ def _add_file_type_filter(self, ext: str) -> None: offvalue=False, ) cb.pack(side="left", padx=4, pady=4) + self._bind_hscroll_wheel(cb) var.trace_add("write", lambda *_: self._on_type_filter_changed(ext)) self.file_type_widgets[ext] = cb diff --git a/uv.lock b/uv.lock index 9d878e4..5f2f9f0 100644 --- a/uv.lock +++ b/uv.lock @@ -1,2 +1,300 @@ -# This lock file is optional when using uv with requirements.txt. -# Present here as a placeholder; uv will resolve from requirements.txt. +version = 1 +revision = 1 +requires-python = ">=3.11" + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721 }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/9e/bcec3b22c64ecec47d39bf5167c2613efd41898c019dccd4183f6aa5d6a7/charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694", size = 279531 }, + { url = "https://files.pythonhosted.org/packages/58/12/81fd25f7e7078ab5d1eedbb0fac44be4904ae3370a3bf4533c8f2d159acd/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5", size = 188006 }, + { url = "https://files.pythonhosted.org/packages/ae/6e/f2d30e8c27c1b0736a6520311982cf5286cfc7f6cac77d7bc1325e3a23f2/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281", size = 205085 }, + { url = "https://files.pythonhosted.org/packages/d0/90/d12cefcb53b5931e2cf792a33718d7126efb116a320eaa0742c7059a95e4/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923", size = 200545 }, + { url = "https://files.pythonhosted.org/packages/03/f4/44d3b830a20e89ff82a3134912d9a1cf6084d64f3b95dcad40f74449a654/charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81", size = 193863 }, + { url = "https://files.pythonhosted.org/packages/25/4b/f212119c18a6320a9d4a730d1b4057875cdeabf21b3614f76549042ef8a8/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497", size = 181827 }, + { url = "https://files.pythonhosted.org/packages/74/00/b26158e48b425a202a92965f8069e8a63d9af1481dfa206825d7f74d2a3c/charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c", size = 191085 }, + { url = "https://files.pythonhosted.org/packages/c4/c2/1c1737bf6fd40335fe53d28fe49afd99ee4143cc57a845e99635ce0b9b6d/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e", size = 190688 }, + { url = "https://files.pythonhosted.org/packages/5a/3d/abb5c22dc2ef493cd56522f811246a63c5427c08f3e3e50ab663de27fcf4/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f", size = 183077 }, + { url = "https://files.pythonhosted.org/packages/44/33/5298ad4d419a58e25b3508e87f2758d1442ff00c2471f8e0403dab8edad5/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e", size = 206706 }, + { url = "https://files.pythonhosted.org/packages/7b/17/51e7895ac0f87c3b91d276a449ef09f5532a7529818f59646d7a55089432/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af", size = 191665 }, + { url = "https://files.pythonhosted.org/packages/90/8f/cce9adf1883e98906dbae380d769b4852bb0fa0004bc7d7a2243418d3ea8/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85", size = 201950 }, + { url = "https://files.pythonhosted.org/packages/08/ca/bce99cd5c397a52919e2769d126723f27a4c037130374c051c00470bcd38/charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f", size = 195830 }, + { url = "https://files.pythonhosted.org/packages/87/4f/2e3d023a06911f1281f97b8f036edc9872167036ca6f55cc874a0be6c12c/charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4", size = 132029 }, + { url = "https://files.pythonhosted.org/packages/fe/1f/a853b73d386521fd44b7f67ded6b17b7b2367067d9106a5c4b44f9a34274/charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a", size = 142404 }, + { url = "https://files.pythonhosted.org/packages/b4/10/dba36f76b71c38e9d391abe0fd8a5b818790e053c431adecfc98c35cd2a9/charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c", size = 132796 }, + { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976 }, + { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356 }, + { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369 }, + { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285 }, + { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274 }, + { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715 }, + { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426 }, + { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780 }, + { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805 }, + { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342 }, + { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661 }, + { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819 }, + { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080 }, + { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630 }, + { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856 }, + { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982 }, + { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788 }, + { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890 }, + { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136 }, + { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551 }, + { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572 }, + { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438 }, + { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035 }, + { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340 }, + { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464 }, + { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014 }, + { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297 }, + { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321 }, + { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509 }, + { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284 }, + { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630 }, + { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254 }, + { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232 }, + { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688 }, + { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833 }, + { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879 }, + { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764 }, + { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728 }, + { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937 }, + { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040 }, + { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107 }, + { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310 }, + { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918 }, + { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615 }, + { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784 }, + { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009 }, + { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511 }, + { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775 }, + { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455 }, +] + +[[package]] +name = "customtkinter" +version = "5.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "darkdetect" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/48/c5a9d44188c44702e1e3db493c741e9c779596835a761b819fe15431d163/customtkinter-5.2.2.tar.gz", hash = "sha256:fd8db3bafa961c982ee6030dba80b4c2e25858630756b513986db19113d8d207", size = 261999 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/b1/b43b33001a77256b335511e75f257d001082350b8506c8807f30c98db052/customtkinter-5.2.2-py3-none-any.whl", hash = "sha256:14ad3e7cd3cb3b9eb642b9d4e8711ae80d3f79fb82545ad11258eeffb2e6b37c", size = 296062 }, +] + +[[package]] +name = "darkdetect" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/77/7575be73bf12dee231d0c6e60ce7fb7a7be4fcd58823374fc59a6e48262e/darkdetect-0.8.0.tar.gz", hash = "sha256:b5428e1170263eb5dea44c25dc3895edd75e6f52300986353cd63533fe7df8b1", size = 7681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl", hash = "sha256:a7509ccf517eaad92b31c214f593dbcf138ea8a43b2935406bbd565e15527a85", size = 8955 }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, +] + +[[package]] +name = "index-ripper" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "customtkinter" }, + { name = "requests" }, + { name = "ttkbootstrap" }, + { name = "urllib3" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.12.0,<5.0.0" }, + { name = "customtkinter", specifier = ">=5.2.0,<6.0.0" }, + { name = "requests", specifier = ">=2.31.0,<3.0.0" }, + { name = "ttkbootstrap", specifier = ">=1.10.1,<2.0.0" }, + { name = "urllib3", specifier = ">=2.0.0,<3.0.0" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366 }, +] + +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084 }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866 }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148 }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007 }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418 }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590 }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655 }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286 }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663 }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448 }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651 }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803 }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601 }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995 }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012 }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638 }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540 }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613 }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745 }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823 }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367 }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811 }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689 }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535 }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364 }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561 }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460 }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698 }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706 }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621 }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069 }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040 }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523 }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552 }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108 }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712 }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880 }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616 }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008 }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226 }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136 }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129 }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807 }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954 }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441 }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383 }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104 }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652 }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823 }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143 }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254 }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499 }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137 }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721 }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798 }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315 }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360 }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438 }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503 }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748 }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314 }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612 }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567 }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951 }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769 }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358 }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558 }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028 }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940 }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736 }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894 }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446 }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606 }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321 }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579 }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094 }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850 }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343 }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880 }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016 }, +] + +[[package]] +name = "ttkbootstrap" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/fd/b2c7378cc73989d0a9da5e9c99428cab20cdd6c2f34fd54c08caea1b1bdc/ttkbootstrap-1.20.2.tar.gz", hash = "sha256:e8408ff4cffd7eb2767d7b33c3882d59c1bdf37cfeba29d9a3d45f486d370c7a", size = 175346 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/cf/812a67729004c68ae58621c5a75ea242cd016826d033bbbd66e6e051553b/ttkbootstrap-1.20.2-py3-none-any.whl", hash = "sha256:6b3f25aa173c09423474ac31eeb2d766cf0f9514c8debe24da8d1f062e4afdcf", size = 190439 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584 }, +] From 4c145590f58ffb94f7b26e02c73ed81f81162523 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 21:22:17 +0800 Subject: [PATCH 40/55] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20README=20?= =?UTF-8?q?=E5=8F=8D=E6=98=A0=20CustomTkinter=20=E6=9E=B6=E6=A7=8B?= =?UTF-8?q?=EF=BC=9B=E4=BF=AE=E6=AD=A3=20on=5Fscan=5Fprogress=20hook=20?= =?UTF-8?q?=E5=91=BC=E5=8F=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README.md / README_zh.md:更新系統需求(Python 3.11+)、安裝指令(uv sync)、 執行方式(移除 ttkbootstrap 入口)、打包指令(--collect-all customtkinter)、 FAQ(CustomTkinter 內建 Tcl/Tk,新增深色/淺色主題說明),並補充 UI 架構章節 - ui_ctk.py:修正 on_scan_progress 誤呼叫 update_progress(下載 hook), 改呼叫 _update_scan_progress Co-Authored-By: Claude Sonnet 4.6 --- README.md | 74 +++++++++++++++++----------------------------------- README_zh.md | 73 +++++++++++++++++---------------------------------- ui_ctk.py | 12 +++++++-- 3 files changed, 58 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 40bd054..7027aad 100644 --- a/README.md +++ b/README.md @@ -66,35 +66,19 @@ A graphical tool specifically designed for downloading files from "Index of" pag - Click file type checkboxes to select/deselect - Use Select all/Deselect all buttons for quick operations -## System Requirements - -- Python 3.10 or higher -- Supports Windows, macOS, Linux +## UI Architecture -### Windows Users +IndexRipper uses **CustomTkinter** as its UI framework, providing a modern look with automatic dark/light theme switching. -- When installing Python, check "tcl/tk and IDLE" -- If not checked, re-run installer and modify +The file tree uses a custom `FileTree` component (built on `CTkScrollableFrame`) with emoji icons instead of pixel art: +- 📁 folder / 🖼️ image / 📄 document / 🗜️ archive / 💻 code / 🎵 audio / 🎬 video / 📝 text / ⚙️ binary -### macOS Users - -```bash -# Install Python and Tkinter using Homebrew -brew install python-tk@3.10 -``` - -### Linux Users - -```bash -# Ubuntu/Debian -sudo apt-get install python3-tk +## System Requirements -# Fedora -sudo dnf install python3-tkinter +- Python **3.11** or higher +- Supports Windows, macOS, Linux -# Arch Linux -sudo pacman -S tk -``` +> CustomTkinter bundles its own Tcl/Tk — **no separate system Tkinter installation needed**. ## Installing Dependencies @@ -104,39 +88,26 @@ Prefer uv for faster, reproducible installs: # Install uv (one-time) curl -LsSf https://astral.sh/uv/install.sh | sh -# Install required packages -uv pip install -r requirements.txt +# Install required packages (reads pyproject.toml) +uv sync ``` ## Running the Application ```bash -# Run with uv +# Run with uv (CustomTkinter UI) uv run python index_ripper.py -# Run ttkbootstrap UI entry -uv run python index_ripper_ttkb.py - # Quick non-interactive smoke check uv run python index_ripper.py --smoke -# UI smoke check (requires tkinter) +# UI smoke check uv run python index_ripper.py --ui-smoke -# ttkbootstrap UI smoke check -uv run python index_ripper_ttkb.py --ui-smoke - # Deterministic self-test (no real network) uv run python index_ripper.py --self-test - -# Deterministic self-test for ttkbootstrap entry -uv run python index_ripper_ttkb.py --self-test ``` -`index_ripper_ttkb.py --ui-smoke` intentionally uses a minimal/safe UI construction path, -so it can validate Tk window startup across environments where some Pillow Tk image -bridges may be unavailable. - ## Download Prebuilt Binaries You can download prebuilt executables from GitHub Actions artifacts: @@ -192,21 +163,21 @@ Build platform-specific executables with uv + PyInstaller. Windows (PowerShell): ```powershell -uv pip install -r requirements.txt pyinstaller pillow +uv pip install pyinstaller uv run pyinstaller --onefile --windowed --icon=app.png --name=IndexRipper ` - --hidden-import tkinter --hidden-import tkinter.ttk index_ripper.py + --collect-all customtkinter index_ripper.py ``` macOS/Linux (bash): ```bash -uv pip install -r requirements.txt pyinstaller pillow +uv pip install pyinstaller # macOS .app uv run pyinstaller -F --windowed --name=IndexRipper \ - --hidden-import tkinter --hidden-import tkinter.ttk --icon=app.png index_ripper.py + --collect-all customtkinter --icon=app.png index_ripper.py # Linux single binary uv run pyinstaller --onefile --windowed --name=IndexRipper \ - --hidden-import tkinter --hidden-import tkinter.ttk --icon=app.png index_ripper.py + --collect-all customtkinter --icon=app.png index_ripper.py ``` ## Important Notes @@ -224,13 +195,16 @@ MIT License ### 1. tkinter Related Errors -If you encounter "No module named '_tkinter'" error: +CustomTkinter bundles its own Tcl/Tk. If issues persist: -- Windows: Reinstall Python, ensure "tcl/tk and IDLE" is checked -- macOS: Run `brew install python-tk@3.10` -- Linux: Install tkinter package for your distribution +- macOS (Homebrew Python): `brew install python-tk@3.11` +- Linux: `sudo apt-get install python3-tk` (Ubuntu/Debian) ### 2. Display Issues - If interface displays abnormally, it might be a DPI scaling issue - Windows users can right-click Python.exe → Properties → Compatibility → Change high DPI settings, and enable high DPI scaling override + +### 3. Dark/Light Theme + +IndexRipper follows the system appearance by default, with automatic switching for macOS and Windows dark mode. diff --git a/README_zh.md b/README_zh.md index 63bc031..7ea03e7 100644 --- a/README_zh.md +++ b/README_zh.md @@ -66,35 +66,19 @@ - 點擊檔案類型勾選框選擇/取消選擇 - 使用全選/取消全選按鈕快速操作 -## 系統需求 - -- Python 3.10 或更高版本 -- 支援 Windows、macOS、Linux 系統 +## UI 架構 -### Windows 使用者 +IndexRipper 使用 **CustomTkinter** 作為 UI 框架,提供現代外觀與深色/淺色主題切換。 -- Python 安裝時請勾選 "tcl/tk and IDLE" -- 如果沒有勾選,可以重新執行安裝程式並修改 +檔案樹採用自訂 `FileTree` 元件(基於 `CTkScrollableFrame`),以 emoji 圖示取代像素圖,支援: +- 📁 資料夾 / 🖼️ 圖片 / 📄 文件 / 🗜️ 壓縮檔 / 💻 程式碼 / 🎵 音訊 / 🎬 影片 / 📝 文字 / ⚙️ 二進位 -### macOS 使用者 - -```bash -# 使用 Homebrew 安裝 Python 和 Tkinter -brew install python-tk@3.10 -``` - -### Linux 使用者 - -```bash -# Ubuntu/Debian -sudo apt-get install python3-tk +## 系統需求 -# Fedora -sudo dnf install python3-tkinter +- Python **3.11** 或更高版本 +- 支援 Windows、macOS、Linux 系統 -# Arch Linux -sudo pacman -S tk -``` +> CustomTkinter 內建 Tcl/Tk,**不需要**額外安裝系統 Tkinter 套件。 ## 安裝依賴 @@ -104,38 +88,26 @@ sudo pacman -S tk # 安裝 uv(僅需一次) curl -LsSf https://astral.sh/uv/install.sh | sh -# 安裝所需套件 -uv pip install -r requirements.txt +# 安裝所需套件(讀取 pyproject.toml) +uv sync ``` ## 執行方式 ```bash -# 使用 uv 執行 +# 使用 uv 執行(CustomTkinter UI) uv run python index_ripper.py -# 執行 ttkbootstrap UI 入口 -uv run python index_ripper_ttkb.py - # 快速無互動 smoke 檢查 uv run python index_ripper.py --smoke -# UI smoke 檢查(需要 tkinter) +# UI smoke 檢查 uv run python index_ripper.py --ui-smoke -# ttkbootstrap UI smoke 檢查 -uv run python index_ripper_ttkb.py --ui-smoke - # 可重現的 self-test(不依賴真網路) uv run python index_ripper.py --self-test - -# ttkbootstrap 入口的可重現 self-test -uv run python index_ripper_ttkb.py --self-test ``` -`index_ripper_ttkb.py --ui-smoke` 會刻意使用最小且安全的 UI 建立路徑, -讓不同環境即使缺少部分 Pillow Tk 影像橋接能力,也能穩定驗證 Tk 視窗可啟動。 - ## 下載已編譯執行檔 可於 GitHub Actions 的 artifacts 下載預先編譯好的執行檔: @@ -191,21 +163,21 @@ chmod +x "/path/to/IndexRipper.app/Contents/MacOS/IndexRipper" Windows(PowerShell): ```powershell -uv pip install -r requirements.txt pyinstaller pillow +uv pip install pyinstaller uv run pyinstaller --onefile --windowed --icon=app.png --name=IndexRipper ` - --hidden-import tkinter --hidden-import tkinter.ttk index_ripper.py + --collect-all customtkinter index_ripper.py ``` macOS/Linux(bash): ```bash -uv pip install -r requirements.txt pyinstaller pillow +uv pip install pyinstaller # macOS .app uv run pyinstaller -F --windowed --name=IndexRipper \ - --hidden-import tkinter --hidden-import tkinter.ttk --icon=app.png index_ripper.py + --collect-all customtkinter --icon=app.png index_ripper.py # Linux 單一執行檔 uv run pyinstaller --onefile --windowed --name=IndexRipper \ - --hidden-import tkinter --hidden-import tkinter.ttk --icon=app.png index_ripper.py + --collect-all customtkinter --icon=app.png index_ripper.py ``` ## 注意事項 @@ -223,13 +195,16 @@ MIT License ### 1. tkinter 相關錯誤 -如果遇到 "No module named '_tkinter'" 錯誤: +CustomTkinter 內建 Tcl/Tk,通常不需要額外安裝。若仍出現問題: -- Windows:重新安裝 Python,確保勾選 "tcl/tk and IDLE" -- macOS:執行 `brew install python-tk@3.10` -- Linux:安裝對應發行版的 tkinter 套件 +- macOS(Homebrew Python):`brew install python-tk@3.11` +- Linux:`sudo apt-get install python3-tk`(Ubuntu/Debian) ### 2. 畫面顯示問題 - 如果介面顯示異常,可能是 DPI 縮放問題 - Windows 用戶可以右鍵點擊 Python.exe → 內容 → 相容性 → 變更高 DPI 設定,並啟用高 DPI 縮放覆寫 + +### 3. 深色/淺色主題 + +IndexRipper 預設跟隨系統外觀設定(system),支援 macOS 與 Windows 的深色模式自動切換。 diff --git a/ui_ctk.py b/ui_ctk.py index ddcb763..0d65675 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -1114,7 +1114,7 @@ def on_scan_started(self, *, url: str = "") -> None: self._set_status("Scanning", "#B45309") def on_scan_progress(self, *, scanned_urls: int = 0, total_urls: int = 0) -> None: - self.window.after(0, lambda: self.update_progress(scanned_urls, total_urls)) + self.window.after(0, lambda: self._update_scan_progress(scanned_urls, total_urls)) def on_scan_finished(self, *, stopped: bool = False) -> None: def _finish(): @@ -1131,7 +1131,15 @@ def _finish(): self._set_status("Ready", "#059669") self.window.after(0, _finish) - def update_progress(self, scanned: int, total: int) -> None: + def update_progress(self, file_path: str, file_name: str, progress: float) -> None: + """Backend hook — called from download thread with per-file progress (0–100).""" + self.window.after(0, lambda: self._update_download_progress(file_path, progress)) + + def update_download_status(self, file_path: str, status: str) -> None: + """Backend hook — called from download thread with file status.""" + self.window.after(0, lambda: self._set_download_status(file_path, status)) + + def _update_scan_progress(self, scanned: int, total: int) -> None: """Update the scan progress bar (0–100%).""" if total <= 0: self.progress_bar.set(0) From 76dc28de58034696beed50b5bcfc7fcd431910eb Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 21:28:01 +0800 Subject: [PATCH 41/55] =?UTF-8?q?ci:=20=E9=87=8D=E6=A7=8B=20CI=20=E2=80=94?= =?UTF-8?q?=20=E5=8A=A0=E5=85=A5=E6=B8=AC=E8=A9=A6=E6=AD=A5=E9=A9=9F?= =?UTF-8?q?=E3=80=81=E6=94=B9=E7=94=A8=20uv=20sync=E3=80=81=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20PyInstaller=20=E6=97=97=E6=A8=99=EF=BC=9B=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20ui=5Ftheme=20=E9=87=8D=E8=A4=87=E5=87=BD=E6=95=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ci.yml:重命名為 CI;新增 test job(先跑單元測試再 build); 移除 conda 安裝步驟(CTk 內建 Tcl/Tk);改用 uv sync + uv run pyinstaller; PyInstaller 改用 --collect-all customtkinter 取代 --hidden-import tkinter - pyproject.toml:新增 dev extra(pytest>=8.0.0);加入 [tool.pytest.ini_options] - ui_theme.py:刪除重複的 ui_tokens / action_button_style_name / configure_action_button_styles - 新增 test_ui_downloads.py、test_ui_ttkb.py 至版控 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 71 ++++++++++----------------- pyproject.toml | 7 +++ test_ui_downloads.py | 17 +++++++ test_ui_ttkb.py | 21 ++++++++ ui_theme.py | 103 --------------------------------------- uv.lock | 59 ++++++++++++++++++++++ 6 files changed, 130 insertions(+), 148 deletions(-) create mode 100644 test_ui_downloads.py create mode 100644 test_ui_ttkb.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45922ad..4df496f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Build Executables (uv + PyInstaller) +name: CI on: push: @@ -12,8 +12,24 @@ permissions: contents: write jobs: + test: + name: Test (ubuntu-latest) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + + - name: Install dependencies + run: uv sync --extra dev + + - name: Run unit tests + run: uv run python -m pytest test_backend.py test_app_utils.py test_settings_store.py test_security_utils.py test_ui_theme.py test_ui_downloads.py -v + build: name: Build (${{ matrix.os }}) + needs: test strategy: fail-fast: false matrix: @@ -35,66 +51,37 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python (Windows) - if: matrix.os == 'windows-latest' - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install uv uses: astral-sh/setup-uv@v3 - - name: Setup conda (macOS & Linux) - if: matrix.os != 'windows-latest' - uses: conda-incubator/setup-miniconda@v3 - with: - auto-update-conda: true - python-version: "3.12" - channels: conda-forge - - - name: Install tk (macOS & Linux) - if: matrix.os != 'windows-latest' - shell: bash -l {0} - run: | - conda install -y tk + - name: Install dependencies + run: uv sync - - name: Install dependencies (Windows) - if: matrix.os == 'windows-latest' - run: | - uv pip install --system -r requirements.txt pyinstaller pillow - - - name: Install dependencies (macOS & Linux) - if: matrix.os != 'windows-latest' - shell: bash -l {0} - run: | - uv pip install --system -r requirements.txt pyinstaller pillow + - name: Install PyInstaller + run: uv pip install pyinstaller - name: Build (Windows) if: matrix.os == 'windows-latest' run: | - pyinstaller --onefile --windowed --icon=app.png --name=IndexRipper --hidden-import tkinter --hidden-import tkinter.ttk index_ripper.py + uv run pyinstaller --onefile --windowed --icon=app.png --name=IndexRipper --collect-all customtkinter index_ripper.py - name: Build (macOS) if: startsWith(matrix.os, 'macos-') - shell: bash -l {0} run: | - pyinstaller -F --windowed --name=IndexRipper \ - --hidden-import tkinter --hidden-import tkinter.ttk --icon=app.png index_ripper.py + uv run pyinstaller -F --windowed --name=IndexRipper \ + --collect-all customtkinter --icon=app.png index_ripper.py [ -f dist/IndexRipper ] && rm -f dist/IndexRipper || true - name: Sign app (ad-hoc) if: startsWith(matrix.os, 'macos-') - shell: bash -l {0} run: | - # 依你提供的方法,於建置後進行 ad-hoc 簽章 codesign --force --deep --sign - "dist/IndexRipper.app" - name: Build (Linux) if: matrix.os == 'ubuntu-latest' - shell: bash -l {0} run: | - pyinstaller --onefile --windowed --name=IndexRipper \ - --hidden-import tkinter --hidden-import tkinter.ttk --icon=app.png index_ripper.py + uv run pyinstaller --onefile --windowed --name=IndexRipper \ + --collect-all customtkinter --icon=app.png index_ripper.py - name: Verify artifact exists shell: bash @@ -142,14 +129,10 @@ jobs: zip_name="${dir}.app.zip" app_src="" if [ -d "$dir/IndexRipper.app" ]; then - # 常見情況:artifact 內已有 .app 目錄 app_src="$dir/IndexRipper.app" elif [ -d "$dir/dist/IndexRipper.app" ]; then - # 另一種情況:保留了 dist 前綴 app_src="$dir/dist/IndexRipper.app" elif [ -d "$dir/Contents" ]; then - # 展平情況:artifact 只包含 .app 內容,重建包裹 - # 先清理可能存在的 _pkg,再逐一搬移,並忽略 _pkg 本身,避免遞迴移動 rm -rf "$dir/_pkg" mkdir -p "$dir/_pkg/IndexRipper.app" shopt -s dotglob nullglob @@ -184,8 +167,6 @@ jobs: mv artifacts/IndexRipper-Windows/IndexRipper.exe artifacts/IndexRipper-Windows.exe fi if [ -f artifacts/IndexRipper-Linux/IndexRipper ]; then - # 目的地若與資料夾同名會造成 mv 視為移動到同一資料夾,導致「same file」錯誤 - # 因此改以 .bin 作為檔名,避免與資料夾名稱衝突 mv artifacts/IndexRipper-Linux/IndexRipper artifacts/IndexRipper-Linux.bin fi diff --git a/pyproject.toml b/pyproject.toml index 0cb71f6..cde480f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,5 +10,12 @@ dependencies = [ "ttkbootstrap>=1.10.1,<2.0.0", ] +[project.optional-dependencies] +dev = ["pytest>=8.0.0"] + [project.scripts] index-ripper = "ui_ctk:main" + +[tool.pytest.ini_options] +testpaths = ["."] +python_files = ["test_*.py"] diff --git a/test_ui_downloads.py b/test_ui_downloads.py new file mode 100644 index 0000000..01bdf04 --- /dev/null +++ b/test_ui_downloads.py @@ -0,0 +1,17 @@ +import unittest + +import ui_downloads + + +class TestUIDownloadsSemantics(unittest.TestCase): + def test_download_status_state_mapping(self): + self.assertEqual(ui_downloads.download_status_state("Queued"), "queued") + self.assertEqual(ui_downloads.download_status_state("Downloading 12.0%"), "active") + self.assertEqual(ui_downloads.download_status_state("Completed"), "success") + self.assertEqual(ui_downloads.download_status_state("Failed"), "error") + self.assertEqual(ui_downloads.download_status_state("Canceling..."), "warning") + self.assertEqual(ui_downloads.download_status_state("something else"), "queued") + + +if __name__ == "__main__": + unittest.main() diff --git a/test_ui_ttkb.py b/test_ui_ttkb.py new file mode 100644 index 0000000..6e8e2d4 --- /dev/null +++ b/test_ui_ttkb.py @@ -0,0 +1,21 @@ +import unittest + +from ui_ttkb import should_skip_file_row + + +class TestUiTtkbScanEntryGate(unittest.TestCase): + def test_should_not_skip_placeholder_none(self): + self.assertFalse(should_skip_file_row(None)) + + def test_should_skip_real_file_entry(self): + self.assertTrue(should_skip_file_row({"url": "https://example.com/a.txt"})) + + def test_should_skip_only_for_real_entries_not_placeholders(self): + placeholders = [None] + real_entries = [{"url": "x"}, {"file_name": "a.txt"}] + self.assertEqual([should_skip_file_row(v) for v in placeholders], [False]) + self.assertEqual([should_skip_file_row(v) for v in real_entries], [True, True]) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui_theme.py b/ui_theme.py index 4279cb1..a839601 100644 --- a/ui_theme.py +++ b/ui_theme.py @@ -109,109 +109,6 @@ def configure_action_button_styles(window, ctk, ttk): ) -def ui_tokens(): - return { - "option_menu": { - "height": 34, - "fg_color": ("#E2E8F0", "#334155"), - "button_color": ("#CBD5E1", "#475569"), - "button_hover_color": ("#94A3B8", "#64748B"), - "text_color": ("#0F172A", "#F8FAFC"), - "dropdown_fg_color": ("#FFFFFF", "#1E293B"), - "dropdown_text_color": ("#0F172A", "#F1F5F9"), - "dropdown_hover_color": ("#E2E8F0", "#334155"), - }, - "tabview": { - "segmented_button_fg_color": ("#E2E8F0", "#334155"), - "segmented_button_selected_color": ("#2563EB", "#3B82F6"), - "segmented_button_selected_hover_color": ("#1D4ED8", "#2563EB"), - "segmented_button_unselected_color": ("#CBD5E1", "#475569"), - "segmented_button_unselected_hover_color": ("#94A3B8", "#64748B"), - "text_color": ("#0F172A", "#F8FAFC"), - }, - "checkbox": { - "checkbox_width": 20, - "checkbox_height": 20, - "corner_radius": 6, - "border_width": 2, - "border_color": ("#94A3B8", "#64748B"), - "fg_color": ("#2563EB", "#3B82F6"), - "hover_color": ("#1D4ED8", "#2563EB"), - "checkmark_color": ("#FFFFFF", "#FFFFFF"), - "text_color": ("#0F172A", "#E2E8F0"), - }, - "logs": { - "height": 100, - "font": ("SF Mono", 12), - "text_color": ("#0F172A", "#E2E8F0"), - "fg_color": ("#F8FAFC", "#111827"), - }, - "downloads": { - "row_fg_color": ("#FFFFFF", "#1F2937"), - "row_border_width": 1, - "row_corner_radius": 10, - "name_text_color": ("#0F172A", "#E2E8F0"), - "status_colors": { - "queued": ("#475569", "#94A3B8"), - "active": ("#1D4ED8", "#60A5FA"), - "warning": ("#B45309", "#F59E0B"), - "success": ("#047857", "#34D399"), - "error": ("#B91C1C", "#F87171"), - }, - "cancel_button": { - "width": 88, - "height": 30, - "fg_color": "#DC2626", - "hover_color": "#B91C1C", - }, - }, - } - - -def action_button_style_name(kind: str) -> str: - mapping = { - "primary": "Primary.TButton", - "secondary": "Secondary.TButton", - "danger": "Danger.TButton", - "success": "Success.TButton", - } - return mapping.get(kind, "Secondary.TButton") - - -def configure_action_button_styles(window, ctk, ttk): - style = ttk.Style() - text_light = "#F8FAFC" - fg = { - "primary": window._apply_appearance_mode(("#2563EB", "#3B82F6")), - "secondary": window._apply_appearance_mode(("#334155", "#475569")), - "danger": window._apply_appearance_mode(("#DC2626", "#EF4444")), - "success": window._apply_appearance_mode(("#059669", "#10B981")), - } - active = { - "primary": window._apply_appearance_mode(("#1D4ED8", "#2563EB")), - "secondary": window._apply_appearance_mode(("#1E293B", "#334155")), - "danger": window._apply_appearance_mode(("#B91C1C", "#DC2626")), - "success": window._apply_appearance_mode(("#047857", "#059669")), - } - for kind in ("primary", "secondary", "danger", "success"): - style_name = action_button_style_name(kind) - style.configure( - style_name, - padding=(12, 7), - foreground=text_light, - background=fg[kind], - borderwidth=0, - focusthickness=1, - focuscolor=active[kind], - font=("SF Pro Text", 11, "bold"), - ) - style.map( - style_name, - background=[("active", active[kind]), ("disabled", "#6B7280")], - foreground=[("disabled", "#E5E7EB")], - ) - - def configure_treeview_style(window, ctk, ttk): style = ttk.Style() bg_color = window._apply_appearance_mode(ctk.ThemeManager.theme["CTkFrame"]["fg_color"]) diff --git a/uv.lock b/uv.lock index 5f2f9f0..ec43126 100644 --- a/uv.lock +++ b/uv.lock @@ -97,6 +97,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455 }, ] +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + [[package]] name = "customtkinter" version = "5.2.2" @@ -140,14 +149,30 @@ dependencies = [ { name = "urllib3" }, ] +[package.optional-dependencies] +dev = [ + { name = "pytest" }, +] + [package.metadata] requires-dist = [ { name = "beautifulsoup4", specifier = ">=4.12.0,<5.0.0" }, { name = "customtkinter", specifier = ">=5.2.0,<6.0.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "requests", specifier = ">=2.31.0,<3.0.0" }, { name = "ttkbootstrap", specifier = ">=1.10.1,<2.0.0" }, { name = "urllib3", specifier = ">=2.0.0,<3.0.0" }, ] +provides-extras = ["dev"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, +] [[package]] name = "packaging" @@ -245,6 +270,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880 }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801 }, +] + [[package]] name = "requests" version = "2.32.5" From 2ec609cc9ddc1b0ffe8af9666c69f1fdf092c636 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 23:06:32 +0800 Subject: [PATCH 42/55] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=20UI/UX=20?= =?UTF-8?q?=E9=87=8D=E6=96=B0=E8=A8=AD=E8=A8=88=E6=96=B9=E6=A1=88=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- docs/plans/2026-03-10-ui-redesign-design.md | 168 ++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 docs/plans/2026-03-10-ui-redesign-design.md diff --git a/docs/plans/2026-03-10-ui-redesign-design.md b/docs/plans/2026-03-10-ui-redesign-design.md new file mode 100644 index 0000000..5e85bc4 --- /dev/null +++ b/docs/plans/2026-03-10-ui-redesign-design.md @@ -0,0 +1,168 @@ +# UI/UX Redesign — Design Document + +**Date:** 2026-03-10 +**Status:** Approved + +## Goal + +Simplify and clean up the IndexRipper UI to match a macOS-native single-window app aesthetic. The primary workflow is linear: enter URL → scan → select files → download. + +## Layout Overview + +``` +┌─────────────────────────────────────────────────────────┐ +│ Row 0 Toolbar: URL + Scan + Status │ +├─────────────────────────────────────────────────────────┤ +│ Row 1 Search + Types filter │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ Row 2 FileTree (weight=1, fills remaining space) │ +│ │ +├─────────────────────────────────────────────────────────┤ +│ Row 3 Progress bar + Bottom control bar │ +├─────────────────────────────────────────────────────────┤ +│ Row 4 Logs / Downloads panel (collapsible, default │ +│ hidden) │ +└─────────────────────────────────────────────────────────┘ +``` + +Window: `1100×800`, minsize `800×550`. `grid_rowconfigure(2, weight=1)`. + +--- + +## Row 0 — Toolbar + +``` +[URL_________________________________] [Scan] ● Ready +``` + +- `CTkEntry` for URL, `fill="x"`, height 36px, `column=0 weight=1` +- **Scan button**: text toggles `"Scan"` ↔ `"Stop Scan"` (single button, no separate Stop) +- **Pause Scan button**: hidden by default (`grid_remove`); shown only while scanning; toggles `"Pause Scan"` ↔ `"Resume Scan"` +- **Status indicator**: small colored dot (6×6 `CTkFrame`, `corner_radius=3`) + text label, right-aligned + - Ready → green `#059669` + - Scanning → amber `#B45309` + - Stopped / Error → red `#B91C1C` +- **Remove Clear button**: scanning automatically clears previous results (`clear_scan_results()` called at scan start) + +--- + +## Row 1 — Search + Types + +``` +🔍 [Search____________] │ □ mp4 (12) □ zip (3) □ jpg (8) ↔ +``` + +- Left: `CTkEntry` with 🔍 prefix label, fixed width 260px, `height=34` +- Vertical separator: 1px `CTkFrame(width=1, fg_color=("gray80","gray30"))` +- Right: `CTkScrollableFrame(orientation="horizontal", height=38)` — same as current `filters_container`, `fill="x" expand=True` +- **Remove "Types:" label** and **"✓ All / ✗ All" buttons** from Row 1 +- Replace with right-click context menu on `filters_container`: + - `Select All Types` + - `Deselect All Types` +- Row total height: ~42px + +--- + +## Row 2 — FileTree + +``` +📁 videos/ ▶ + 🎬 movie.mp4 1.2 GB + 🎬 trailer.mp4 240 MB +📁 docs/ ▶ + 📄 readme.txt 4 KB +``` + +Changes from current implementation: + +| Property | Before | After | +|----------|--------|-------| +| Chevron position | left of name | right edge of row | +| Checked style | blue background fill | 3px blue left border accent | +| Row height | 38px | 36px | +| Row gap (pady) | 1px | 0px | +| Type badge | shown right (e.g. "video") | **removed** | +| Size column | right-aligned, width=90 | right-aligned, width=80 | +| Empty state | blank | "Scanning…" during scan; "No files found" after | + +### Checked accent border implementation + +`RowWidget` uses a narrow `CTkFrame(width=3)` as the leftmost child, colored: +- unchecked: `fg_color="transparent"` +- checked: `fg_color=("#2563EB","#3B82F6")` + +Background colors (hover/checked) remain but become more subtle: + +| State | Light | Dark | +|-------|-------|------| +| Normal | transparent | transparent | +| Hover | `#F1F5F9` | `#1E293B` | +| Checked | `#EFF6FF` | `#172554` | +| Checked+Hover | `#DBEAFE` | `#1E3A5F` | + +### Chevron on right + +`RowWidget` packs chevron with `side="right"` before other right-side widgets, so it appears at the far right. + +--- + +## Row 3 — Progress + Control bar + +### Progress bar (Row 3a) + +``` +████████████░░░░░░ 68% Scanning… 34/50 +``` + +- `CTkProgressBar(height=8, corner_radius=4)` — slimmer than current 14px +- Progress text on **same row** to the right of bar: `CTkLabel`, `anchor="w"`, auto-hide when text is empty +- When idle: progress bar hidden entirely (`grid_remove`); shown when scanning or downloading + +### Control bar (Row 3b) + +``` +[Select All] [Deselect All] [⬇ Download] [⏸ Pause] [📁 Folder] Threads [5▾] [Logs ▾] +``` + +- Left group: `Select All` / `Deselect All` (secondary style, height=30) +- Right group (right-aligned): + - `⬇ Download Selected` — primary blue button + - `⏸ Pause` — secondary, `state="disabled"` until download starts + - `📁 Folder` — secondary + - `Threads` label + `CTkOptionMenu` (1–10, width=65) + - `Logs ▾` toggle button — expands/collapses Row 4 panel + +--- + +## Row 4 — Logs / Downloads panel + +- **Default state: hidden** (`grid_remove` on startup) +- Fixed height: 160px +- `CTkTabview` with tabs: `Downloads` | `Logs` +- Default active tab: `Logs` +- Toggle button text: `"Logs ▾"` (collapsed) ↔ `"Logs ▴"` (expanded) + +--- + +## Preserved public interfaces (no changes to callers) + +All existing methods remain: +- `add_folder`, `add_file`, `toggle_check`, `select_all`, `deselect_all` +- `expand_all`, `collapse_all`, `sort_tree` +- `_backup_full_tree`, `_restore_full_tree`, `_filter_tree_by_term` +- `on_scan_started`, `on_scan_progress`, `on_scan_finished` +- `update_progress`, `update_download_status` +- `checked_items: set[str]` + +--- + +## Implementation notes + +- `_build_header()` → rename to `_build_toolbar()`, restructure columns +- `_build_filters_row()` → split into `_build_search_types_row()`; remove "Types:" label and All/None buttons; add context menu binding +- `_build_filetree()` → update `RowWidget` (chevron right, accent border, no type badge, smaller row) +- `_build_progress_section()` + `_build_download_controls()` → merge into `_build_bottom_bar()` +- `_build_panels()` → update default to hidden; update toggle button label +- `_build_full_ui()` → update `grid_rowconfigure`, call renamed builders +- `clear_scan_results()` → call automatically from `start_scan()` before starting thread From 9de12591b7785b38e77f954db4a485eae74ca7a2 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 23:09:40 +0800 Subject: [PATCH 43/55] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=20UI/UX=20?= =?UTF-8?q?=E9=87=8D=E6=96=B0=E8=A8=AD=E8=A8=88=E5=AF=A6=E4=BD=9C=E8=A8=88?= =?UTF-8?q?=E7=95=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- docs/plans/2026-03-10-ui-redesign.md | 953 +++++++++++++++++++++++++++ 1 file changed, 953 insertions(+) create mode 100644 docs/plans/2026-03-10-ui-redesign.md diff --git a/docs/plans/2026-03-10-ui-redesign.md b/docs/plans/2026-03-10-ui-redesign.md new file mode 100644 index 0000000..92daf5f --- /dev/null +++ b/docs/plans/2026-03-10-ui-redesign.md @@ -0,0 +1,953 @@ +# UI/UX Redesign Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Redesign IndexRipper's layout to a clean macOS-native style — slim toolbar, merged search+types row, dominant FileTree, slim bottom bar with collapsible panel. + +**Architecture:** All changes are confined to `ui_ctk.py`. No new files. Five builder methods are renamed/rewritten; `RowWidget` gets cosmetic updates. All public interfaces and backend hooks are preserved unchanged. + +**Tech Stack:** Python 3.11+, CustomTkinter (ctk), tkinter (tk/ttk) + +**Design reference:** `docs/plans/2026-03-10-ui-redesign-design.md` + +--- + +### Task 1: Update RowWidget — accent border, chevron right, remove type badge + +**Files:** +- Modify: `ui_ctk.py:57-186` (module-level constants + RowWidget class) + +**Context:** +`RowWidget` is the class that renders one visible row in the FileTree. Currently: +- Chevron (`▶`/`▼`) is on the **left** side +- Checked state = blue full-row background fill +- Type badge is shown on the right (`node.icon_group` text) +- `ROW_HEIGHT = 38`, `pady=1` in `pack()` + +After this task: +- Chevron moves to **right** side +- Checked state = transparent/subtle background + **3px blue left accent bar** +- Type badge **removed** +- `ROW_HEIGHT = 36`, `pady=0` + +**Step 1: Update module-level color constants** + +Replace lines 57-60 in `ui_ctk.py`: + +```python +_BG_NORMAL = "transparent" +_BG_HOVER = ("#F1F5F9", "#1E293B") +_BG_CHECKED = ("#EFF6FF", "#172554") +_BG_CHECKED_HOVER = ("#DBEAFE", "#1E3A5F") +``` + +**Step 2: Update `RowWidget.__init__` — ROW_HEIGHT, pady, accent bar, chevron right, no type badge** + +Replace the entire `RowWidget` class body (lines ~63-186) with: + +```python +class RowWidget: + """One visible row in the FileTree.""" + + INDENT_PX = 20 + ROW_HEIGHT = 36 + + def __init__(self, parent, app, node: TreeNode, depth: int): + self.app = app + self.node_id = node.node_id + self._checked = node.checked + self._hovered = False + + self.frame = ctk.CTkFrame(parent, height=self.ROW_HEIGHT, corner_radius=4) + self.frame.pack(fill="x", padx=4, pady=0) + self.frame.pack_propagate(False) + + # 3-px accent bar (left edge, blue when checked) + self._accent = ctk.CTkFrame( + self.frame, width=3, corner_radius=0, + fg_color="#2563EB" if node.checked else "transparent", + ) + self._accent.pack(side="left", fill="y") + + # Indent spacer + if depth > 0: + ctk.CTkFrame( + self.frame, width=depth * self.INDENT_PX, + fg_color="transparent", height=self.ROW_HEIGHT, + ).pack(side="left") + + # Emoji icon + ctk.CTkLabel( + self.frame, + text=_EMOJI_ICONS.get(node.icon_group, "📄"), + font=ctk.CTkFont(size=18), + width=28, + ).pack(side="left", padx=(4, 4)) + + # Name label + self.name_label = ctk.CTkLabel( + self.frame, + text=node.name, + anchor="w", + font=ctk.CTkFont(size=14, weight="bold" if node.kind == "folder" else "normal"), + ) + self.name_label.pack(side="left", fill="x", expand=True) + + # Chevron on RIGHT (folders only) + if node.kind == "folder": + self.chevron = ctk.CTkButton( + self.frame, text="▼" if node.expanded else "▶", + width=22, height=22, fg_color="transparent", + hover_color=("gray85", "gray30"), text_color=("gray40", "gray60"), + font=ctk.CTkFont(size=10), + command=lambda: app._on_chevron_click(self.node_id), + ) + self.chevron.pack(side="right", padx=(4, 4)) + elif node.kind == "file" and node.size: + # Size label (right side, files only) + ctk.CTkLabel( + self.frame, + text=node.size, + font=ctk.CTkFont(size=12), + text_color=("gray50", "gray60"), + width=80, + anchor="e", + ).pack(side="right", padx=(0, 8)) + + self._bind_all(self.frame) + + def _bind_all(self, widget) -> None: + widget.bind("", self._on_enter) + widget.bind("", self._on_leave) + widget.bind("", self._on_click) + for child in widget.winfo_children(): + self._bind_all(child) + + def _on_enter(self, _event=None) -> None: + self._hovered = True + self._update_bg() + + def _on_leave(self, _event=None) -> None: + self._hovered = False + self._update_bg() + + def _on_click(self, event) -> None: + self.app._on_row_click(self.node_id, event) + + def set_checked(self, checked: bool) -> None: + self._checked = checked + self._accent.configure( + fg_color="#2563EB" if checked else "transparent" + ) + self._update_bg() + + def set_chevron(self, expanded: bool) -> None: + if hasattr(self, "chevron"): + self.chevron.configure(text="▼" if expanded else "▶") + + def _update_bg(self) -> None: + if self._checked and self._hovered: + color = _BG_CHECKED_HOVER + elif self._checked: + color = _BG_CHECKED + elif self._hovered: + color = _BG_HOVER + else: + color = _BG_NORMAL + self.frame.configure(fg_color=color) + + def destroy(self) -> None: + self.frame.destroy() +``` + +**Step 3: Verify smoke test still passes** + +```bash +uv run python index_ripper.py --ui-smoke +``` + +Expected: exits with code 0, no exception. + +**Step 4: Run unit tests** + +```bash +uv run python -m pytest test_ui_theme.py test_ui_downloads.py test_backend.py -v +``` + +Expected: all pass. + +**Step 5: Commit** + +```bash +git add ui_ctk.py +git commit -m "feat: RowWidget — accent border, chevron right, remove type badge" +``` + +--- + +### Task 2: Rewrite `_build_header` → `_build_toolbar` + +**Files:** +- Modify: `ui_ctk.py` — method `_build_header` (~line 350) + +**Context:** +Current `_build_header`: +- Row layout: `URL label` | `URL entry` | `Status label` | `[Scan] [Pause Scan] [Clear]` +- Status is a plain text label; Pause Scan is always visible; Clear is a separate button + +After this task: +- Status = colored 6px dot + text (right-aligned) +- Pause Scan = hidden by default (`grid_remove`), shown only during scan +- Clear button = **removed** (auto-clear happens in `start_scan`) +- Method renamed to `_build_toolbar` + +**Step 1: Replace `_build_header` with `_build_toolbar`** + +Replace the entire `_build_header` method with: + +```python +def _build_toolbar(self) -> None: + toolbar = ctk.CTkFrame(self.window, fg_color="transparent") + toolbar.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 0)) + toolbar.grid_columnconfigure(1, weight=1) + + self.url_var = tk.StringVar() + self.url_entry = ctk.CTkEntry( + toolbar, textvariable=self.url_var, + placeholder_text="https://", + font=ctk.CTkFont(size=14), + height=36, + ) + self.url_entry.grid(row=0, column=0, sticky="ew", columnspan=2, padx=(0, 8)) + toolbar.grid_columnconfigure(0, weight=0) + toolbar.grid_columnconfigure(1, weight=1) + + # Re-do: URL takes column 0 with weight=1; buttons on right + # Use a simpler single-row approach: + toolbar.grid_columnconfigure(0, weight=1) + toolbar.grid_columnconfigure(1, weight=0) + toolbar.grid_columnconfigure(2, weight=0) + + self.url_entry.grid(row=0, column=0, sticky="ew", padx=(0, 8)) + + # Scan button (toggles Scan ↔ Stop Scan) + self.scan_btn = ctk.CTkButton(toolbar, text="Scan", command=self.start_scan, width=90) + self.scan_btn.grid(row=0, column=1, padx=(0, 4)) + + # Pause Scan button — hidden by default, shown only while scanning + self.scan_pause_btn = ctk.CTkButton( + toolbar, text="Pause", + fg_color=("gray70", "gray30"), + hover_color=("gray60", "gray40"), + command=self.toggle_scan_pause, + width=80, + state="disabled", + ) + self.scan_pause_btn.grid(row=0, column=2, padx=(0, 8)) + self.scan_pause_btn.grid_remove() # hidden until scan starts + + # Status: dot + text in a small frame + status_frame = ctk.CTkFrame(toolbar, fg_color="transparent") + status_frame.grid(row=0, column=3, sticky="e") + + self._status_dot = ctk.CTkFrame( + status_frame, width=8, height=8, corner_radius=4, + fg_color="#059669", + ) + self._status_dot.pack(side="left", padx=(0, 5)) + + self.status_label = ctk.CTkLabel( + status_frame, text="Ready", + text_color="#059669", font=ctk.CTkFont(size=13), + ) + self.status_label.pack(side="left") + + # URL context menu (right-click paste) + self.url_context_menu = tk.Menu(self.window, tearoff=0) + self.url_context_menu.add_command(label="Paste", command=self._paste_into_url_entry) + self.url_entry.bind("", self._show_url_context_menu) + self.url_entry.bind("", self._show_url_context_menu) +``` + +**Step 2: Update `_set_status` to also configure the dot color** + +Replace the `_set_status` method: + +```python +def _set_status(self, text: str, color: str = "#059669") -> None: + try: + self.status_label.configure(text=text, text_color=color) + self._status_dot.configure(fg_color=color) + except Exception: + pass +``` + +**Step 3: Update `on_scan_started` — show Pause Scan button** + +In `on_scan_started`: +```python +def on_scan_started(self, *, url: str = "") -> None: + self.scan_btn.configure(text="Stop Scan") + self.scan_pause_btn.grid() # show + self.scan_pause_btn.configure(state="normal") + self.progress_bar.set(0) + self.progress_label.configure(text="Scanning…") + self._set_status("Scanning", "#B45309") +``` + +**Step 4: Update `on_scan_finished` — hide Pause Scan button** + +In `on_scan_finished` inside `_finish()`: +```python +self.scan_btn.configure(text="Scan") +self.scan_pause_btn.grid_remove() # hide +self.scan_pause_btn.configure(text="Pause", state="disabled") +``` + +**Step 5: Update `_build_full_ui` to call `_build_toolbar` instead of `_build_header`** + +In `_build_full_ui`, replace the call `self._build_header()` with `self._build_toolbar()`. + +**Step 6: Smoke test + unit tests** + +```bash +uv run python index_ripper.py --ui-smoke +uv run python -m pytest test_backend.py test_ui_theme.py -v +``` + +Expected: all pass. + +**Step 7: Commit** + +```bash +git add ui_ctk.py +git commit -m "feat: toolbar redesign — status dot, hide pause btn, remove clear btn" +``` + +--- + +### Task 3: Rewrite `_build_filters_row` → `_build_search_types_row` + +**Files:** +- Modify: `ui_ctk.py` — method `_build_filters_row` (~line 556) + +**Context:** +Current `_build_filters_row` builds **two rows**: +- Row 1 (ctrl_row_frame): placeholder, later filled by `_build_download_controls` +- Row 2 (types_row): "Types:" label + `[✓ All]` `[✗ All]` + horizontal scrollable checkboxes + +After this task: **single row** combining Search + Types: +- Left: 🔍 + Search entry (fixed width 260px) +- Separator (1px vertical line) +- Right: horizontal scrollable checkboxes (no label, no ✓/✗ All buttons) +- Right-click on checkboxes area: context menu with "Select All Types" / "Deselect All Types" + +The `_ctrl_row_frame` is still created (Task 4 uses it for download controls). + +**Step 1: Replace `_build_filters_row`** + +```python +def _build_search_types_row(self) -> None: + # ── Row 1: download controls placeholder (filled in _build_bottom_bar) ── + self._ctrl_row_frame = ctk.CTkFrame(self.window, fg_color="transparent") + self._ctrl_row_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=(6, 0)) + self._ctrl_row_frame.grid_columnconfigure(0, weight=1) + + # ── Row 2: Search + separator + Types ──────────────────────────────── + row = ctk.CTkFrame(self.window, fg_color="transparent") + row.grid(row=2, column=0, sticky="ew", padx=10, pady=(6, 0)) + row.grid_columnconfigure(2, weight=1) + + # Search icon + entry + ctk.CTkLabel(row, text="🔍", font=ctk.CTkFont(size=15)).grid( + row=0, column=0, padx=(0, 4) + ) + self.search_var = tk.StringVar() + self.search_entry = ctk.CTkEntry( + row, textvariable=self.search_var, + placeholder_text="Search files…", + width=260, height=34, + ) + self.search_entry.grid(row=0, column=1, padx=(0, 8)) + self.search_var.trace_add("write", self.on_search_filter_changed) + + # Vertical separator + ctk.CTkFrame(row, width=1, height=30, fg_color=("gray80", "gray30")).grid( + row=0, column=2, sticky="ns", padx=(0, 8) + ) + row.grid_columnconfigure(2, weight=0) + row.grid_columnconfigure(3, weight=1) + + # Horizontal scrollable types + self.filters_container = ctk.CTkScrollableFrame( + row, + height=38, + orientation="horizontal", + fg_color="transparent", + ) + self.filters_container.grid(row=0, column=3, sticky="ew") + self._bind_hscroll_wheel(self.filters_container) + + # Right-click context menu on types container + self._types_context_menu = tk.Menu(self.window, tearoff=0) + self._types_context_menu.add_command( + label="Select All Types", command=self.select_all_types + ) + self._types_context_menu.add_command( + label="Deselect All Types", command=self.deselect_all_types + ) + self.filters_container.bind("", self._show_types_context_menu) + self.filters_container.bind("", self._show_types_context_menu) +``` + +**Step 2: Add `_show_types_context_menu` helper** + +```python +def _show_types_context_menu(self, event) -> None: + try: + self._types_context_menu.tk_popup(event.x_root, event.y_root) + finally: + self._types_context_menu.grab_release() +``` + +**Step 3: Update `_build_full_ui` to call `_build_search_types_row` instead of `_build_filters_row`** + +Replace `self._build_filters_row()` with `self._build_search_types_row()`. + +Also update `grid_rowconfigure` in `_build_full_ui` — the filetree is now at **row 3** (search+types moved from row 2 to row 2, filetree stays at row 3): + +```python +def _build_full_ui(self) -> None: + self.window.grid_columnconfigure(0, weight=1) + self.window.grid_rowconfigure(3, weight=1) # filetree + self.window.grid_rowconfigure(5, weight=0) # panels + + self._build_toolbar() # row 0 + self._build_search_types_row() # rows 1 (ctrl placeholder) + 2 (search+types) + self._build_filetree() # row 3 + self._build_bottom_bar() # rows 4 (progress) + … + self._build_panels() # row 5 + # _build_download_controls() content is now in _build_bottom_bar() + + self.sort_reverse = False + self.full_tree_backup = {} + + self.context_menu = tk.Menu(self.window, tearoff=0) + self.context_menu.add_command(label="Select All", command=self.select_all) + self.context_menu.add_command(label="Deselect All", command=self.deselect_all) + self.context_menu.add_separator() + self.context_menu.add_command(label="Expand All", command=self.expand_all) + self.context_menu.add_command(label="Collapse All", command=self.collapse_all) +``` + +**Step 4: Smoke test** + +```bash +uv run python index_ripper.py --ui-smoke +``` + +Expected: exits 0. + +**Step 5: Commit** + +```bash +git add ui_ctk.py +git commit -m "feat: merge search + types into single row, add types context menu" +``` + +--- + +### Task 4: Rewrite progress + controls → `_build_bottom_bar` + +**Files:** +- Modify: `ui_ctk.py` — methods `_build_progress_section`, `_build_download_controls`, `_build_full_ui` + +**Context:** +Currently: +- `_build_progress_section` (row 4): progress bar (height=14) + label on separate line +- `_build_download_controls` (appended into `_ctrl_row_frame` row 1): Select All, Deselect All, Download, Pause, Folder, Threads, Hide Panels + +After this task: +- **Row 4a** (progress row): slim bar (height=8) + text on same line; hidden when idle +- **Row 4b** (control bar): Select All, Deselect All | Download, Pause, Folder, Threads | **Logs ▾** toggle +- `_build_download_controls` is **removed** (absorbed into `_build_bottom_bar`) +- `_build_progress_section` is **removed** (absorbed into `_build_bottom_bar`) + +**Step 1: Add `_build_bottom_bar` method** + +```python +def _build_bottom_bar(self) -> None: + _s = dict( + fg_color=("gray80", "gray25"), + text_color=("gray10", "gray90"), + hover_color=("gray70", "gray35"), + height=30, + font=ctk.CTkFont(size=12), + ) + + # ── Row 4: progress bar + text ───────────────────────────────────── + progress_frame = ctk.CTkFrame(self.window, fg_color="transparent") + progress_frame.grid(row=4, column=0, sticky="ew", padx=10, pady=(4, 0)) + progress_frame.grid_columnconfigure(0, weight=1) + progress_frame.grid_remove() # hidden until scanning/downloading + self._progress_frame = progress_frame + + self.progress_bar = ctk.CTkProgressBar( + progress_frame, height=8, corner_radius=4 + ) + self.progress_bar.set(0) + self.progress_bar.grid(row=0, column=0, sticky="ew", padx=(0, 8)) + + self.progress_label = ctk.CTkLabel( + progress_frame, text="", + font=ctk.CTkFont(size=12), + anchor="w", width=220, + ) + self.progress_label.grid(row=0, column=1, sticky="w") + + # ── Row 5: control bar ───────────────────────────────────────────── + ctrl = ctk.CTkFrame(self.window, fg_color="transparent") + ctrl.grid(row=5, column=0, sticky="ew", padx=10, pady=(4, 6)) + ctrl.grid_columnconfigure(1, weight=1) # elastic gap between left and right + + # Left: select controls + left = ctk.CTkFrame(ctrl, fg_color="transparent") + left.grid(row=0, column=0, sticky="w") + ctk.CTkButton(left, text="Select All", command=self.select_all, width=100, **_s).pack(side="left", padx=(0, 4)) + ctk.CTkButton(left, text="Deselect All", command=self.deselect_all, width=110, **_s).pack(side="left") + + # Right: download controls + logs toggle + right = ctk.CTkFrame(ctrl, fg_color="transparent") + right.grid(row=0, column=2, sticky="e") + + self.download_btn = ctk.CTkButton( + right, text="⬇ Download", command=self.download_selected, height=30, + ) + self.download_btn.pack(side="left", padx=(0, 4)) + + self.pause_btn = ctk.CTkButton( + right, text="⏸ Pause", + fg_color=("gray70", "gray30"), + hover_color=("gray60", "gray40"), + command=self.toggle_pause, + state="disabled", height=30, + ) + self.pause_btn.pack(side="left", padx=(0, 4)) + + self.path_btn = ctk.CTkButton( + right, text="📁 Folder", + fg_color=("gray70", "gray30"), + hover_color=("gray60", "gray40"), + command=self.choose_download_path, height=30, + ) + self.path_btn.pack(side="left", padx=(0, 8)) + + ctk.CTkLabel(right, text="Threads", font=ctk.CTkFont(size=12)).pack(side="left", padx=(0, 4)) + + self.threads_var = tk.StringVar(value="5") + self.threads_combo = ctk.CTkOptionMenu( + right, + values=[str(i) for i in range(1, 11)], + variable=self.threads_var, + command=self.update_thread_count, + width=65, height=30, + ) + self.threads_combo.pack(side="left", padx=(0, 12)) + + self.panels_visible = False + self.toggle_panels_btn = ctk.CTkButton( + right, text="Logs ▾", + command=self.toggle_panels, + width=80, **_s, + ) + self.toggle_panels_btn.pack(side="left") +``` + +**Step 2: Update `toggle_panels` for new button text and row index** + +```python +def toggle_panels(self) -> None: + if self.panels_visible: + if hasattr(self, "_panels_widget"): + self._panels_widget.grid_remove() + self.panels_visible = False + self.toggle_panels_btn.configure(text="Logs ▾") + else: + if hasattr(self, "_panels_widget"): + self._panels_widget.grid() + self.panels_visible = True + self.toggle_panels_btn.configure(text="Logs ▴") +``` + +**Step 3: Update progress visibility** + +Add a helper to show/hide the progress row: + +```python +def _show_progress(self, visible: bool) -> None: + try: + if visible: + self._progress_frame.grid() + else: + self._progress_frame.grid_remove() + except Exception: + pass +``` + +Update `on_scan_started`: +```python +def on_scan_started(self, *, url: str = "") -> None: + self.scan_btn.configure(text="Stop Scan") + self.scan_pause_btn.grid() + self.scan_pause_btn.configure(state="normal") + self.progress_bar.set(0) + self.progress_label.configure(text="Scanning…") + self._show_progress(True) + self._set_status("Scanning", "#B45309") +``` + +Update `on_scan_finished` `_finish()`: +```python +self.scan_btn.configure(text="Scan") +self.scan_pause_btn.grid_remove() +self.scan_pause_btn.configure(text="Pause", state="disabled") +if stopped: + self.progress_bar.set(0) + self.progress_label.configure(text="Scan stopped") + self._set_status("Stopped", "#B91C1C") + self._show_progress(False) +else: + self.progress_bar.set(1) + n = len(self.files_dict) + self.progress_label.configure(text=f"Done — {n} files found") + self._set_status("Ready", "#059669") + # keep progress bar visible to show "done" state; hide after 3 s + self.window.after(3000, lambda: self._show_progress(False)) +``` + +**Step 4: Remove `_build_download_controls` and `_build_progress_section`** + +Delete both methods entirely from `ui_ctk.py`. + +**Step 5: Update `_build_full_ui` grid rows** + +```python +def _build_full_ui(self) -> None: + self.window.grid_columnconfigure(0, weight=1) + self.window.grid_rowconfigure(3, weight=1) # filetree + + self._build_toolbar() # row 0 + self._build_search_types_row() # row 1 (ctrl placeholder) + row 2 (search+types) + self._build_filetree() # row 3 + self._build_bottom_bar() # row 4 (progress) + row 5 (controls) + self._build_panels() # row 6 + + self.sort_reverse = False + self.full_tree_backup = {} + + self.context_menu = tk.Menu(self.window, tearoff=0) + self.context_menu.add_command(label="Select All", command=self.select_all) + self.context_menu.add_command(label="Deselect All", command=self.deselect_all) + self.context_menu.add_separator() + self.context_menu.add_command(label="Expand All", command=self.expand_all) + self.context_menu.add_command(label="Collapse All", command=self.collapse_all) +``` + +**Step 6: Smoke test** + +```bash +uv run python index_ripper.py --ui-smoke +uv run python -m pytest test_backend.py test_ui_theme.py test_ui_downloads.py -v +``` + +Expected: exits 0 / all pass. + +**Step 7: Commit** + +```bash +git add ui_ctk.py +git commit -m "feat: merge progress + controls into _build_bottom_bar; panels default hidden" +``` + +--- + +### Task 5: Update `_build_panels` and `_build_filetree` — row index + empty state + +**Files:** +- Modify: `ui_ctk.py` — methods `_build_panels`, `_build_filetree` + +**Context:** +After Task 4, the panels tab is at **row 6** (not row 5). Also: +- Panels default to **hidden** (already set in `_build_bottom_bar`) +- FileTree should show an empty-state label when no nodes exist + +**Step 1: Update `_build_panels` row index** + +```python +def _build_panels(self) -> None: + self.panels_notebook = ctk.CTkTabview(self.window, height=160) + self.panels_notebook.grid(row=6, column=0, sticky="ew", padx=10, pady=(0, 10)) + self._panels_widget = self.panels_notebook + + downloads_tab = self.panels_notebook.add("Downloads") + logs_tab = self.panels_notebook.add("Logs") + self.panels_notebook.set("Logs") + + downloads_scroll = ctk.CTkScrollableFrame(downloads_tab, height=100) + downloads_scroll.pack(fill="both", expand=True) + self.downloads_panel = DownloadsPanel( + parent_frame=downloads_scroll, + ctk=ctk, + tk=tk, + tokens=self.ui_tokens, + ) + + self.log_text = ctk.CTkTextbox(logs_tab, height=100, wrap="word") + self.log_text.pack(fill="both", expand=True) + + # Start hidden (toggle via Logs ▾ button) + self.panels_notebook.grid_remove() +``` + +**Step 2: Add empty-state label to `_build_filetree`** + +In `_build_filetree`, after creating `tree_scroll_frame`, add: + +```python +# Empty state label (shown when no nodes; hidden when rows exist) +self._empty_label = ctk.CTkLabel( + self.tree_scroll_frame, + text="Enter a URL above and click Scan", + text_color=("gray50", "gray60"), + font=ctk.CTkFont(size=14), +) +self._empty_label.pack(expand=True) +``` + +**Step 3: Update `_sync_rows` to show/hide empty label** + +At the end of `_sync_rows`: + +```python +# Show empty state if no visible rows +if hasattr(self, "_empty_label"): + if self._visible_nodes: + self._empty_label.pack_forget() + else: + self._empty_label.pack(expand=True) +``` + +**Step 4: Update empty label text during scanning** + +In `on_scan_started`: +```python +if hasattr(self, "_empty_label"): + self._empty_label.configure(text="Scanning…") +``` + +In `on_scan_finished` inside `_finish()`: +```python +if hasattr(self, "_empty_label") and not self.tree_nodes: + self._empty_label.configure( + text="No files found" if not stopped else "Enter a URL above and click Scan" + ) +``` + +**Step 5: Smoke test** + +```bash +uv run python index_ripper.py --ui-smoke +uv run python -m pytest -v +``` + +Expected: all pass. + +**Step 6: Commit** + +```bash +git add ui_ctk.py +git commit -m "feat: panels at row 6, default hidden; filetree empty state label" +``` + +--- + +### Task 6: Auto-clear on scan start + window size + final wiring + +**Files:** +- Modify: `ui_ctk.py` — `__init__`, `start_scan`, `_build_ui_smoke_only` + +**Context:** +- `start_scan` should call `clear_scan_results()` automatically (removing need for a Clear button) +- Window default size changed to `1100×800`, minsize `800×550` +- `_build_ui_smoke_only` must initialise `_progress_frame` stub to avoid AttributeError +- `_ctrl_row_frame` in `_build_full_ui` is no longer filled by `_build_download_controls` — it was the Row 1 placeholder used by the old control flow. It's now empty and can be removed entirely. Check if anything still references it. + +**Step 1: Update window geometry in `__init__`** + +```python +self.window.geometry("1100x800") +self.window.minsize(800, 550) +``` + +**Step 2: Update `start_scan` to auto-clear** + +```python +def start_scan(self) -> None: + url = self.url_var.get().strip() + if self.is_scanning: + self.backend.should_stop = True + self._set_status("Stopping", "#B45309") + return + if not url: + self.notify_error("Error", "Please enter a URL") + return + + # Auto-clear previous results before starting + self.clear_scan_results() + + self.backend.should_stop = False + try: + self.download_path = default_download_folder(url, os.getcwd()) + except Exception: + self.download_path = os.path.join(os.getcwd(), "downloads") + + t = threading.Thread(target=self.backend.scan_website, args=(url,), daemon=True) + t.start() +``` + +**Step 3: Update `clear_scan_results` to also reset progress and empty label** + +Add at end of `clear_scan_results`: +```python +self._show_progress(False) +if hasattr(self, "_empty_label"): + self._empty_label.configure(text="Enter a URL above and click Scan") + self._empty_label.pack(expand=True) +``` + +**Step 4: Remove `_ctrl_row_frame` (Row 1 placeholder) from `_build_search_types_row`** + +Check: `_ctrl_row_frame` is no longer referenced by any method after removing `_build_download_controls`. Remove these lines from `_build_search_types_row`: + +```python +# DELETE these lines: +self._ctrl_row_frame = ctk.CTkFrame(self.window, fg_color="transparent") +self._ctrl_row_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=(6, 0)) +self._ctrl_row_frame.grid_columnconfigure(0, weight=1) +``` + +And update `_build_full_ui` grid: rows are now `0=toolbar`, `1=search+types`, `2=filetree`, `3=progress`, `4=controls`, `5=panels`: + +```python +def _build_full_ui(self) -> None: + self.window.grid_columnconfigure(0, weight=1) + self.window.grid_rowconfigure(2, weight=1) # filetree at row 2 + + self._build_toolbar() # row 0 + self._build_search_types_row() # row 1 + self._build_filetree() # row 2 + self._build_bottom_bar() # row 3 (progress) + row 4 (controls) + self._build_panels() # row 5 + + self.sort_reverse = False + self.full_tree_backup = {} + + self.context_menu = tk.Menu(self.window, tearoff=0) + self.context_menu.add_command(label="Select All", command=self.select_all) + self.context_menu.add_command(label="Deselect All", command=self.deselect_all) + self.context_menu.add_separator() + self.context_menu.add_command(label="Expand All", command=self.expand_all) + self.context_menu.add_command(label="Collapse All", command=self.collapse_all) +``` + +Update `_build_search_types_row` to use **row=1** for the search+types row (not row 2): + +```python +row.grid(row=1, column=0, sticky="ew", padx=10, pady=(6, 0)) +``` + +Update `_build_filetree` to use **row=2**: +```python +outer.grid(row=2, column=0, sticky="nsew", padx=10, pady=(4, 0)) +``` + +Update `_build_bottom_bar` rows: progress=**3**, controls=**4**: +```python +progress_frame.grid(row=3, ...) +ctrl.grid(row=4, ...) +``` + +Update `_build_panels` to use **row=5**: +```python +self.panels_notebook.grid(row=5, ...) +``` + +**Step 5: Fix `_build_ui_smoke_only` stubs** + +Add `_progress_frame`, `_status_dot`, `_empty_label` stubs: + +```python +def _build_ui_smoke_only(self) -> None: + self.url_var = tk.StringVar(value="https://example.com/") + self.search_var = tk.StringVar() + self.log_text = ctk.CTkTextbox(self.window, height=80) + self.log_text.pack(fill="both", expand=True, padx=10, pady=10) + self.log_text.insert("end", "UI smoke minimal view initialized.\n") + self.panels_notebook = None + self.logs_tab = None + self._visible_nodes = [] + self._row_widgets = {} + self.tree_scroll_frame = None + self.full_tree_backup = {} + self.sort_reverse = False + self._last_toggle_time = 0.0 + self._search_after_id = None + self.tree_nodes = {} + self.tree_roots = [] + self._node_counter = 0 + self.context_menu = tk.Menu(self.window, tearoff=0) + # Stubs for attributes used by hooks + self._progress_frame = None + self._status_dot = None + self._empty_label = None + self.progress_bar = type("_Stub", (), {"set": lambda s, v: None, "grid": lambda s, **kw: None, "grid_remove": lambda s: None})() + self.progress_label = type("_Stub", (), {"configure": lambda s, **kw: None})() + self.status_label = type("_Stub", (), {"configure": lambda s, **kw: None})() + self.scan_btn = type("_Stub", (), {"configure": lambda s, **kw: None})() + self.scan_pause_btn = type("_Stub", (), {"configure": lambda s, **kw: None, "grid": lambda s: None, "grid_remove": lambda s: None})() +``` + +**Step 6: Run full test suite + smoke test** + +```bash +uv run python index_ripper.py --ui-smoke +uv run python -m pytest -v +``` + +Expected: smoke exits 0, all pytest tests pass. + +**Step 7: Commit** + +```bash +git add ui_ctk.py +git commit -m "feat: auto-clear on scan, final row wiring, window 1100x800" +``` + +--- + +## Final verification + +After all tasks complete, run: + +```bash +# Smoke test +uv run python index_ripper.py --ui-smoke + +# Full test suite +uv run python -m pytest -v + +# Self test (no real network) +uv run python index_ripper.py --self-test +``` + +All three should succeed with no errors. From cae24610c38d55f519213e25a3532911803c3171 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 23:13:00 +0800 Subject: [PATCH 44/55] =?UTF-8?q?feat:=20RowWidget=20=E2=80=94=20accent=20?= =?UTF-8?q?border,=20chevron=20right,=20remove=20type=20badge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- ui_ctk.py | 87 ++++++++++++++++++++++++------------------------------- 1 file changed, 38 insertions(+), 49 deletions(-) diff --git a/ui_ctk.py b/ui_ctk.py index 0d65675..a0d298f 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -54,17 +54,17 @@ class TreeNode: "binary": "⚙️", } -_BG_NORMAL = ("gray95", "gray17") -_BG_HOVER = ("#E2E8F0", "#2D3748") -_BG_CHECKED = ("#DBEAFE", "#1E3A5F") -_BG_CHECKED_HOVER = ("#BFDBFE", "#1E40AF") +_BG_NORMAL = "transparent" +_BG_HOVER = ("#F1F5F9", "#1E293B") +_BG_CHECKED = ("#EFF6FF", "#172554") +_BG_CHECKED_HOVER = ("#DBEAFE", "#1E3A5F") class RowWidget: """One visible row in the FileTree.""" INDENT_PX = 20 - ROW_HEIGHT = 38 + ROW_HEIGHT = 36 def __init__(self, parent, app, node: TreeNode, depth: int): self.app = app @@ -73,9 +73,16 @@ def __init__(self, parent, app, node: TreeNode, depth: int): self._hovered = False self.frame = ctk.CTkFrame(parent, height=self.ROW_HEIGHT, corner_radius=4) - self.frame.pack(fill="x", padx=4, pady=1) + self.frame.pack(fill="x", padx=4, pady=0) self.frame.pack_propagate(False) + # 3-px accent bar (left edge, blue when checked) + self._accent = ctk.CTkFrame( + self.frame, width=3, corner_radius=0, + fg_color="#2563EB" if node.checked else "transparent", + ) + self._accent.pack(side="left", fill="y") + # Indent spacer if depth > 0: ctk.CTkFrame( @@ -83,28 +90,13 @@ def __init__(self, parent, app, node: TreeNode, depth: int): fg_color="transparent", height=self.ROW_HEIGHT, ).pack(side="left") - # Chevron (folders) or invisible spacer (files) - if node.kind == "folder": - self.chevron = ctk.CTkButton( - self.frame, text="▶" if not node.expanded else "▼", - width=22, height=22, fg_color="transparent", - hover_color=("gray85", "gray30"), text_color=("gray40", "gray60"), - font=ctk.CTkFont(size=10), - command=lambda: app._on_chevron_click(self.node_id), - ) - self.chevron.pack(side="left", padx=(2, 0)) - else: - ctk.CTkFrame( - self.frame, width=26, fg_color="transparent", height=self.ROW_HEIGHT, - ).pack(side="left") - # Emoji icon ctk.CTkLabel( self.frame, text=_EMOJI_ICONS.get(node.icon_group, "📄"), font=ctk.CTkFont(size=18), - width=30, - ).pack(side="left", padx=(2, 4)) + width=28, + ).pack(side="left", padx=(4, 4)) # Name label self.name_label = ctk.CTkLabel( @@ -115,36 +107,30 @@ def __init__(self, parent, app, node: TreeNode, depth: int): ) self.name_label.pack(side="left", fill="x", expand=True) - # Size + type (right side, files only) - if node.kind == "file": - if node.size: - ctk.CTkLabel( - self.frame, - text=node.size, - font=ctk.CTkFont(size=12), - text_color=("gray50", "gray60"), - width=90, - anchor="e", - ).pack(side="right", padx=(0, 4)) - if node.icon_group and node.icon_group != "binary": - ctk.CTkLabel( - self.frame, - text=node.icon_group, - font=ctk.CTkFont(size=11), - text_color=("gray50", "gray60"), - width=65, - anchor="e", - ).pack(side="right", padx=(0, 2)) - - self._update_bg() + # Chevron on RIGHT (folders only) + if node.kind == "folder": + self.chevron = ctk.CTkButton( + self.frame, text="▼" if node.expanded else "▶", + width=22, height=22, fg_color="transparent", + hover_color=("gray85", "gray30"), text_color=("gray40", "gray60"), + font=ctk.CTkFont(size=10), + command=lambda: app._on_chevron_click(self.node_id), + ) + self.chevron.pack(side="right", padx=(4, 4)) + elif node.kind == "file" and node.size: + # Size label (right side, files only) + ctk.CTkLabel( + self.frame, + text=node.size, + font=ctk.CTkFont(size=12), + text_color=("gray50", "gray60"), + width=80, + anchor="e", + ).pack(side="right", padx=(0, 8)) - # Bind hover + click on frame and all children self._bind_all(self.frame) def _bind_all(self, widget) -> None: - # Skip chevron button — it has its own command; don't overlay _on_click - if hasattr(self, "chevron") and widget is self.chevron: - return widget.bind("", self._on_enter) widget.bind("", self._on_leave) widget.bind("", self._on_click) @@ -164,6 +150,9 @@ def _on_click(self, event) -> None: def set_checked(self, checked: bool) -> None: self._checked = checked + self._accent.configure( + fg_color="#2563EB" if checked else "transparent" + ) self._update_bg() def set_chevron(self, expanded: bool) -> None: From e967a72055484daac141465ad42ecfeeae8e33f3 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 23:17:10 +0800 Subject: [PATCH 45/55] =?UTF-8?q?fix:=20RowWidget=20=E2=80=94=20separate?= =?UTF-8?q?=20hover/click=20bindings,=20fix=20pady=20in=20=5Fsync=5Frows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- ui_ctk.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/ui_ctk.py b/ui_ctk.py index a0d298f..df84a71 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -128,14 +128,19 @@ def __init__(self, parent, app, node: TreeNode, depth: int): anchor="e", ).pack(side="right", padx=(0, 8)) - self._bind_all(self.frame) - - def _bind_all(self, widget) -> None: - widget.bind("", self._on_enter) - widget.bind("", self._on_leave) + # Bind hover only on outer frame (avoids flicker from child entry) + self.frame.bind("", self._on_enter) + self.frame.bind("", self._on_leave) + # Bind click on frame and all non-chevron children + self._bind_clicks(self.frame) + + def _bind_clicks(self, widget) -> None: + """Bind click handler on widget and all children except the chevron.""" + if hasattr(self, "chevron") and widget is self.chevron: + return # chevron has its own command; don't intercept clicks widget.bind("", self._on_click) for child in widget.winfo_children(): - self._bind_all(child) + self._bind_clicks(child) def _on_enter(self, _event=None) -> None: self._hovered = True @@ -669,7 +674,7 @@ def _sync_rows(self) -> None: self.tree_scroll_frame, self, node, depth ) else: - self._row_widgets[node_id].frame.pack(fill="x", padx=4, pady=1) + self._row_widgets[node_id].frame.pack(fill="x", padx=4, pady=0) self._last_visible = list(self._visible_nodes) From c83ca77aef9c05e1d0ed44970d8a740aa4074fed Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Tue, 10 Mar 2026 23:20:14 +0800 Subject: [PATCH 46/55] =?UTF-8?q?feat:=20toolbar=20=E2=80=94=20status=20do?= =?UTF-8?q?t,=20pause=20btn=20hidden=20by=20default,=20remove=20clear=20bt?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- ui_ctk.py | 67 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/ui_ctk.py b/ui_ctk.py index df84a71..cf5f8c7 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -324,7 +324,7 @@ def _build_full_ui(self) -> None: self.window.grid_rowconfigure(3, weight=1) # filetree expands (row shifted by types row) self.window.grid_rowconfigure(5, weight=0) # panels fixed - self._build_header() + self._build_toolbar() self._build_filters_row() self._build_filetree() self._build_progress_section() @@ -341,56 +341,56 @@ def _build_full_ui(self) -> None: self.context_menu.add_command(label="Expand All", command=self.expand_all) self.context_menu.add_command(label="Collapse All", command=self.collapse_all) - def _build_header(self) -> None: - header = ctk.CTkFrame(self.window, fg_color="transparent") - header.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 0)) - header.grid_columnconfigure(1, weight=1) - - ctk.CTkLabel( - header, text="URL", font=ctk.CTkFont(size=14, weight="bold") - ).grid(row=0, column=0, sticky="w", padx=(0, 8)) + def _build_toolbar(self) -> None: + toolbar = ctk.CTkFrame(self.window, fg_color="transparent") + toolbar.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 0)) + toolbar.grid_columnconfigure(0, weight=1) # URL entry expands self.url_var = tk.StringVar() self.url_entry = ctk.CTkEntry( - header, textvariable=self.url_var, + toolbar, textvariable=self.url_var, placeholder_text="https://", font=ctk.CTkFont(size=14), height=36, ) - self.url_entry.grid(row=0, column=1, sticky="ew", padx=(0, 8)) + self.url_entry.grid(row=0, column=0, sticky="ew", padx=(0, 8)) - self.status_label = ctk.CTkLabel( - header, text="Ready", text_color="#059669", font=ctk.CTkFont(size=13) - ) - self.status_label.grid(row=0, column=2, sticky="e", padx=(0, 8)) - - actions = ctk.CTkFrame(header, fg_color="transparent") - actions.grid(row=0, column=3, sticky="e") - - self.scan_btn = ctk.CTkButton(actions, text="Scan", command=self.start_scan) - self.scan_btn.pack(side="left", padx=3) + # Scan button (toggles "Scan" ↔ "Stop Scan") + self.scan_btn = ctk.CTkButton(toolbar, text="Scan", command=self.start_scan, width=90) + self.scan_btn.grid(row=0, column=1, padx=(0, 4)) + # Pause Scan button — hidden by default, shown only while scanning self.scan_pause_btn = ctk.CTkButton( - actions, text="Pause Scan", + toolbar, text="Pause", fg_color=("gray70", "gray30"), hover_color=("gray60", "gray40"), command=self.toggle_scan_pause, + width=80, state="disabled", ) - self.scan_pause_btn.pack(side="left", padx=3) + self.scan_pause_btn.grid(row=0, column=2, padx=(0, 8)) + self.scan_pause_btn.grid_remove() # hidden until scan starts - self.clear_scan_btn = ctk.CTkButton( - actions, text="Clear", - fg_color=("gray70", "gray30"), - hover_color=("gray60", "gray40"), - command=self.clear_scan_results, + # Status: dot + text + status_frame = ctk.CTkFrame(toolbar, fg_color="transparent") + status_frame.grid(row=0, column=3, sticky="e") + + self._status_dot = ctk.CTkFrame( + status_frame, width=8, height=8, corner_radius=4, + fg_color="#059669", + ) + self._status_dot.pack(side="left", padx=(0, 5)) + self._status_dot.pack_propagate(False) + + self.status_label = ctk.CTkLabel( + status_frame, text="Ready", + text_color="#059669", font=ctk.CTkFont(size=13), ) - self.clear_scan_btn.pack(side="left", padx=3) + self.status_label.pack(side="left") - # URL context menu + # URL context menu (right-click paste) self.url_context_menu = tk.Menu(self.window, tearoff=0) self.url_context_menu.add_command(label="Paste", command=self._paste_into_url_entry) - self.url_entry.bind("", self._show_url_context_menu) self.url_entry.bind("", self._show_url_context_menu) @@ -882,6 +882,7 @@ def _show_url_context_menu(self, event): def _set_status(self, text: str, color: str = "#059669") -> None: try: self.status_label.configure(text=text, text_color=color) + self._status_dot.configure(fg_color=color) except Exception: pass @@ -1102,6 +1103,7 @@ def clear_scan_results(self) -> None: def on_scan_started(self, *, url: str = "") -> None: self.scan_btn.configure(text="Stop Scan") + self.scan_pause_btn.grid() # show self.scan_pause_btn.configure(state="normal") self.progress_bar.set(0) self.progress_label.configure(text="Scanning…") @@ -1113,7 +1115,8 @@ def on_scan_progress(self, *, scanned_urls: int = 0, total_urls: int = 0) -> Non def on_scan_finished(self, *, stopped: bool = False) -> None: def _finish(): self.scan_btn.configure(text="Scan") - self.scan_pause_btn.configure(text="Pause Scan", state="disabled") + self.scan_pause_btn.grid_remove() # hide + self.scan_pause_btn.configure(text="Pause", state="disabled") if stopped: self.progress_bar.set(0) self.progress_label.configure(text="Scan stopped") From 409ffaa85aa84f759120578b5503c684530fb4f8 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Wed, 11 Mar 2026 07:55:21 +0800 Subject: [PATCH 47/55] =?UTF-8?q?fix:=20toolbar=20=E2=80=94=20wrap=20on=5F?= =?UTF-8?q?scan=5Fstarted=20in=20after(),=20add=20smoke=20stubs,=20fix=20c?= =?UTF-8?q?olumn=20jitter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- ui_ctk.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/ui_ctk.py b/ui_ctk.py index cf5f8c7..892ff14 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -317,6 +317,12 @@ def _build_ui_smoke_only(self) -> None: self.tree_nodes = {} self.tree_roots = [] self._node_counter = 0 + self.scan_pause_btn = type("_Stub", (), { + "configure": lambda s, **kw: None, + "grid": lambda s: None, + "grid_remove": lambda s: None, + })() + self._status_dot = type("_Stub", (), {"configure": lambda s, **kw: None})() self.context_menu = tk.Menu(self.window, tearoff=0) def _build_full_ui(self) -> None: @@ -345,6 +351,7 @@ def _build_toolbar(self) -> None: toolbar = ctk.CTkFrame(self.window, fg_color="transparent") toolbar.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 0)) toolbar.grid_columnconfigure(0, weight=1) # URL entry expands + toolbar.grid_columnconfigure(2, minsize=96) # reserve space for pause btn self.url_var = tk.StringVar() self.url_entry = ctk.CTkEntry( @@ -1102,12 +1109,14 @@ def clear_scan_results(self) -> None: self._set_status("Ready", "#059669") def on_scan_started(self, *, url: str = "") -> None: - self.scan_btn.configure(text="Stop Scan") - self.scan_pause_btn.grid() # show - self.scan_pause_btn.configure(state="normal") - self.progress_bar.set(0) - self.progress_label.configure(text="Scanning…") - self._set_status("Scanning", "#B45309") + def _start(): + self.scan_btn.configure(text="Stop Scan") + self.scan_pause_btn.grid() # show + self.scan_pause_btn.configure(state="normal") + self.progress_bar.set(0) + self.progress_label.configure(text="Scanning…") + self._set_status("Scanning", "#B45309") + self.window.after(0, _start) def on_scan_progress(self, *, scanned_urls: int = 0, total_urls: int = 0) -> None: self.window.after(0, lambda: self._update_scan_progress(scanned_urls, total_urls)) From d7026e54808829722ece47608b17091f199e8b1c Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Fri, 13 Mar 2026 21:56:20 +0800 Subject: [PATCH 48/55] fix: align CTk tree smoke test and harden download/executor handling --- backend.py | 13 +++++++++++++ index_ripper.py | 6 +++--- test_ui_ctk.py | 5 ++++- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/backend.py b/backend.py index fab72f2..c374350 100644 --- a/backend.py +++ b/backend.py @@ -14,6 +14,15 @@ from app_utils import is_url_in_scope +def _cleanup_partial_file(file_path: str) -> None: + """Remove partial download file if it exists.""" + try: + if os.path.exists(file_path): + os.remove(file_path) + except OSError: + pass # Best-effort cleanup + + class Backend: """Handles the backend logic for scanning and downloading.""" @@ -305,6 +314,7 @@ def download_file(self, url, file_path, file_name, cancel_event=None): self.ui_manager.pause_event.wait() if cancel_event is not None and cancel_event.is_set(): self.ui_manager.log_message(f"[Download] Canceled: {file_name}") + _cleanup_partial_file(file_path) try: self.ui_manager.update_download_status( file_path, "Canceled" @@ -314,6 +324,7 @@ def download_file(self, url, file_path, file_name, cancel_event=None): return False if self.should_stop: self._log(f"[Download] Stopping download for {file_name}") + _cleanup_partial_file(file_path) return False if not data: break @@ -333,6 +344,7 @@ def download_file(self, url, file_path, file_name, cancel_event=None): except requests.exceptions.RequestException as ex: self._log(f"[Download] Error downloading {file_name}: {str(ex)}") + _cleanup_partial_file(file_path) try: self.ui_manager.update_download_status(file_path, "Failed") self.ui_manager.log_message(f"[Download] Error: {file_name} - {ex}") @@ -341,6 +353,7 @@ def download_file(self, url, file_path, file_name, cancel_event=None): return False except IOError as ex: self._log(f"[Download] File error for {file_name}: {str(ex)}") + _cleanup_partial_file(file_path) try: self.ui_manager.update_download_status(file_path, "Failed") self.ui_manager.log_message( diff --git a/index_ripper.py b/index_ripper.py index bcb8ff0..4588b20 100644 --- a/index_ripper.py +++ b/index_ripper.py @@ -1185,11 +1185,11 @@ def update_thread_count(self, new_count_str: str): try: new_count = int(new_count_str) if 1 <= new_count <= 10: + old_executor = self.executor self.max_workers = new_count - # Re-create the executor with the new worker count - # Shut down the old one to release resources - self.executor.shutdown(wait=False, cancel_futures=True) self.executor = ThreadPoolExecutor(max_workers=self.max_workers) + # Shutdown old executor after new one is ready + old_executor.shutdown(wait=True, cancel_futures=False) except (ValueError, TypeError): pass diff --git a/test_ui_ctk.py b/test_ui_ctk.py index 722005c..fab8445 100644 --- a/test_ui_ctk.py +++ b/test_ui_ctk.py @@ -89,7 +89,10 @@ def test_has_treeview(self): app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) app.window.after(0, app.window.destroy) app.run() - self.assertTrue(hasattr(app, "tree")) + # CTk version uses custom FileTree components, not ttk.Treeview + self.assertTrue(hasattr(app, "tree_nodes")) + self.assertTrue(hasattr(app, "tree_roots")) + self.assertTrue(hasattr(app, "tree_scroll_frame")) self.assertTrue(hasattr(app, "search_var")) def test_search_var_traces(self): From de952116fd6658fb62d2b696743c4810446222a0 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Wed, 18 Mar 2026 13:59:45 +0800 Subject: [PATCH 49/55] docs: add implementation plan for code review fixes Co-Authored-By: Claude Opus 4.6 --- docs/plans/2026-03-13-code-review-fixes.md | 194 +++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 docs/plans/2026-03-13-code-review-fixes.md diff --git a/docs/plans/2026-03-13-code-review-fixes.md b/docs/plans/2026-03-13-code-review-fixes.md new file mode 100644 index 0000000..cd08e92 --- /dev/null +++ b/docs/plans/2026-03-13-code-review-fixes.md @@ -0,0 +1,194 @@ +# Index-Ripper Code Review Fixes Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Fix 3 issues identified in code review: (1) test failure due to UI architecture mismatch, (2) partial file cleanup on download failure, (3) ThreadPoolExecutor resource management. + +**Architecture:** Incremental fixes to existing code without architectural changes. + +**Tech Stack:** Python 3.11+, CustomTkinter, requests, BeautifulSoup + +--- + +### Task 1: Fix test_has_treeview Failure + +**Files:** +- Modify: `test_ui_ctk.py:86-93` +- Verify: Run `uv run pytest test_ui_ctk.py::TestWebsiteCopierCtkSmoke::test_has_treeview -v` + +**Root Cause:** Test checks for `app.tree` (ttk.Treeview) but `WebsiteCopierCtk` uses custom UI with `tree_nodes`, `tree_roots`, `_row_widgets`, `tree_scroll_frame`. + +**Step 1: Update test to check correct attributes** + +```python +def test_has_treeview(self): + import ui_ctk, importlib + importlib.reload(ui_ctk) + app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) + app.window.after(0, app.window.destroy) + app.run() + # CTk version uses custom FileTree components, not ttk.Treeview + self.assertTrue(hasattr(app, "tree_nodes")) + self.assertTrue(hasattr(app, "tree_roots")) + self.assertTrue(hasattr(app, "tree_scroll_frame")) + self.assertTrue(hasattr(app, "search_var")) +``` + +**Step 2: Run test to verify it passes** + +Run: `uv run pytest test_ui_ctk.py::TestWebsiteCopierCtkSmoke::test_has_treeview -v` +Expected: PASS + +**Step 3: Commit** + +```bash +git add test_ui_ctk.py +git commit -m "fix(test): update test_has_treeview to match CTk UI architecture" +``` + +--- + +### Task 2: Add Partial File Cleanup on Download Failure + +**Files:** +- Modify: `backend.py:303-351` + +**Root Cause:** When download is canceled/fails/stopped, partial file remains on disk. + +**Step 1: Add cleanup helper function** + +Add at top of `backend.py` after imports: +```python +def _cleanup_partial_file(file_path: str) -> None: + """Remove partial download file if it exists.""" + try: + import os + if os.path.exists(file_path): + os.remove(file_path) + except OSError: + pass # Best-effort cleanup +``` + +**Step 2: Add cleanup calls in download_file** + +Modify `backend.py` - add cleanup in these 3 locations: + +1. After cancel detection (around line 314): +```python +if cancel_event is not None and cancel_event.is_set(): + self.ui_manager.log_message(f"[Download] Canceled: {file_name}") + _cleanup_partial_file(file_path) # ADD THIS LINE + try: + self.ui_manager.update_download_status(file_path, "Canceled") + except AttributeError: + pass + return False +``` + +2. After should_stop check (around line 317): +```python +if self.should_stop: + self._log(f"[Download] Stopping download for {file_name}") + _cleanup_partial_file(file_path) # ADD THIS LINE + return False +``` + +3. In exception handlers (around lines 341 and 351): +```python +except requests.exceptions.RequestException as ex: + self._log(f"[Download] Error downloading {file_name}: {str(ex)}") + _cleanup_partial_file(file_path) # ADD THIS LINE + try: + self.ui_manager.update_download_status(file_path, "Failed") + self.ui_manager.log_message(f"[Download] Error: {file_name} - {ex}") + except AttributeError: + pass + return False +``` + +And: +```python +except IOError as ex: + self._log(f"[Download] File error for {file_name}: {str(ex)}") + _cleanup_partial_file(file_path) # ADD THIS LINE + try: + self.ui_manager.update_download_status(file_path, "Failed") + self.ui_manager.log_message(f"[Download] File error: {file_name} - {ex}") + except AttributeError: + pass + return False +``` + +**Step 3: Run tests to verify no regressions** + +Run: `uv run pytest test_backend.py -v` +Expected: All pass + +**Step 4: Commit** + +```bash +git add backend.py +git commit -m "fix(backend): cleanup partial files on download cancel/failure" +``` + +--- + +### Task 3: Fix ThreadPoolExecutor Resource Management + +**Files:** +- Modify: `index_ripper.py:1183-1194` +- Verify: Run `uv run pytest test_ui_ctk.py -v` + +**Root Cause:** `update_thread_count` shuts down old executor without waiting, then immediately creates new one, potentially causing resource leaks. + +**Step 1: Fix executor shutdown sequence** + +Modify `index_ripper.py` lines 1183-1194: + +```python +def update_thread_count(self, new_count_str: str): + """Updates the number of concurrent download threads.""" + try: + new_count = int(new_count_str) + if 1 <= new_count <= 10: + old_executor = self.executor + self.max_workers = new_count + self.executor = ThreadPoolExecutor(max_workers=self.max_workers) + # Shutdown old executor after new one is ready + old_executor.shutdown(wait=True, cancel_futures=False) + except (ValueError, TypeError): + pass +``` + +**Step 2: Run tests to verify no regressions** + +Run: `uv run pytest test_ui_ctk.py -v` +Expected: All pass + +**Step 3: Commit** + +```bash +git add index_ripper.py +git commit -m "fix: properly shutdown ThreadPoolExecutor before creating new one" +``` + +--- + +### Task 4: Final Verification + +**Step 1: Run full test suite** + +Run: `uv run pytest -v` +Expected: All tests pass (44 passed, 1 skipped) + +**Step 2: Verify no lint errors** + +Run: `uv run python -m py_compile index_ripper.py backend.py test_ui_ctk.py` +Expected: No output (success) + +**Step 3: Commit** + +```bash +git add -A +git commit -m "chore: code review fixes - test update, partial file cleanup, executor management" +``` From 97f40cfc79c69fea35fa700ebc746b0b49deca4f Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Wed, 18 Mar 2026 21:43:52 +0800 Subject: [PATCH 50/55] fix: executor shutdown deadlock and missing ui_ctk entry point - Move old_executor.shutdown() to a daemon thread to avoid blocking the Tk main thread (and deadlocking when downloads are paused) - Add main() to ui_ctk.py so pyproject.toml entry point works Co-Authored-By: Claude Opus 4.6 --- index_ripper.py | 4 ++-- ui_ctk.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/index_ripper.py b/index_ripper.py index 4588b20..83ae961 100644 --- a/index_ripper.py +++ b/index_ripper.py @@ -1188,8 +1188,8 @@ def update_thread_count(self, new_count_str: str): old_executor = self.executor self.max_workers = new_count self.executor = ThreadPoolExecutor(max_workers=self.max_workers) - # Shutdown old executor after new one is ready - old_executor.shutdown(wait=True, cancel_futures=False) + # Shutdown old executor in background to avoid blocking UI thread + threading.Thread(target=old_executor.shutdown, kwargs={"wait": True, "cancel_futures": False}, daemon=True).start() except (ValueError, TypeError): pass diff --git a/ui_ctk.py b/ui_ctk.py index 892ff14..342e939 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -1493,3 +1493,8 @@ def _on_type_filter_changed(self, ext: str) -> None: node.hidden = not visible self._rebuild_visible() self._sync_rows() + + +def main(): + app = WebsiteCopierCtk() + app.run() From 8ec37b064d06dbca0d40dfa76fc771e8ab726c97 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Wed, 18 Mar 2026 21:57:51 +0800 Subject: [PATCH 51/55] fix: cleanup outside open handle, rebuild executor, preserve download dirs - backend.py: move _cleanup_partial_file calls outside the `with open()` block so os.remove succeeds on Windows (file handle closed first) - ui_ctk.py: update_thread_count now rebuilds the executor (was a no-op) - ui_ctk.py: download_selected preserves subfolder structure and creates target directories via os.makedirs, matching legacy behavior Co-Authored-By: Claude Opus 4.6 --- backend.py | 32 ++++++++++++++++++++------------ ui_ctk.py | 22 +++++++++++++++++++--- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/backend.py b/backend.py index c374350..b012d96 100644 --- a/backend.py +++ b/backend.py @@ -308,24 +308,20 @@ def download_file(self, url, file_path, file_name, cancel_event=None): total_size = int(response.headers.get("content-length", 0)) block_size = 8192 downloaded = 0 + aborted = False + abort_reason = None with open(file_path, "wb") as file_handle: for data in response.iter_content(block_size): self.ui_manager.pause_event.wait() if cancel_event is not None and cancel_event.is_set(): - self.ui_manager.log_message(f"[Download] Canceled: {file_name}") - _cleanup_partial_file(file_path) - try: - self.ui_manager.update_download_status( - file_path, "Canceled" - ) - except AttributeError: - pass - return False + aborted = True + abort_reason = "canceled" + break if self.should_stop: - self._log(f"[Download] Stopping download for {file_name}") - _cleanup_partial_file(file_path) - return False + aborted = True + abort_reason = "stopped" + break if not data: break downloaded += len(data) @@ -335,6 +331,18 @@ def download_file(self, url, file_path, file_name, cancel_event=None): progress = (downloaded / total_size) * 100 self.ui_manager.update_progress(file_path, file_name, progress) + if aborted: + _cleanup_partial_file(file_path) + if abort_reason == "canceled": + self.ui_manager.log_message(f"[Download] Canceled: {file_name}") + try: + self.ui_manager.update_download_status(file_path, "Canceled") + except AttributeError: + pass + else: + self._log(f"[Download] Stopping download for {file_name}") + return False + self.ui_manager.update_progress(file_path, file_name, 100) try: self.ui_manager.update_download_status(file_path, "Completed") diff --git a/ui_ctk.py b/ui_ctk.py index 342e939..fe468f4 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -17,7 +17,7 @@ import customtkinter as ctk -from app_utils import default_download_folder, normalize_extension, safe_join, sanitize_filename +from app_utils import default_download_folder, normalize_extension, safe_join, sanitize_filename, sanitize_path_segment from backend import Backend from ui_downloads import DownloadsPanel from ui_theme import ( @@ -1304,7 +1304,17 @@ def download_selected(self) -> None: continue safe_name = sanitize_filename(file_name) - file_path = safe_join(self.download_path, [safe_name]) + + # Preserve subfolder structure from the scanned path + dir_path = info.get("path", "") + path_segments = [sanitize_path_segment(seg) for seg in dir_path.strip("/").split("/") if seg] + try: + target_dir = safe_join(self.download_path, path_segments) if path_segments else self.download_path + file_path = safe_join(self.download_path, path_segments + [safe_name]) + except ValueError: + self.log_message(f"[Download] Skipped unsafe path: {full_path}") + continue + os.makedirs(target_dir, exist_ok=True) cancel_event = self.downloads_panel.ensure(file_path, safe_name) futures.append(self.executor.submit(self.backend.download_file, url, file_path, safe_name, cancel_event)) @@ -1416,7 +1426,13 @@ def update_thread_count(self, value=None) -> None: n = int(self.threads_var.get()) except (ValueError, AttributeError): return - self.max_workers = max(1, min(10, n)) + new_count = max(1, min(10, n)) + if new_count == self.max_workers: + return + old_executor = self.executor + self.max_workers = new_count + self.executor = ThreadPoolExecutor(max_workers=self.max_workers) + threading.Thread(target=old_executor.shutdown, kwargs={"wait": True, "cancel_futures": False}, daemon=True).start() def toggle_panels(self) -> None: if self.panels_visible: From cc27079469c0ad4ddb6284e4cfa01010a6cc0dbf Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Wed, 18 Mar 2026 22:04:26 +0800 Subject: [PATCH 52/55] fix: type filter retention, directory heuristic, test name, Tcl/Tk detection - ui_ctk.py: always create TreeNode for filtered-out types (set hidden instead of skipping), so re-enabling the filter recovers files - backend.py: remove unreliable link_text.endswith("/") directory heuristic that could misclassify files as directories - test_ui_ctk.py: rename test_has_treeview to test_has_file_tree to match what it actually asserts - index_ripper.py, index_ripper_ttkb.py: dynamically detect Tcl/Tk library dirs instead of hardcoding tcl8.6/tk8.6 Co-Authored-By: Claude Opus 4.6 --- backend.py | 4 +--- index_ripper.py | 21 ++++++++++++++------- index_ripper_ttkb.py | 21 ++++++++++++++------- test_ui_ctk.py | 3 +-- ui_ctk.py | 4 ++-- 5 files changed, 32 insertions(+), 21 deletions(-) diff --git a/backend.py b/backend.py index b012d96..2ccffc8 100644 --- a/backend.py +++ b/backend.py @@ -193,8 +193,6 @@ def _get_all_urls(self, url, scanned_urls=None, base_url=None): href = link.get("href") if not href or href in [".", "..", "/"] or href.startswith("?"): continue - link_text = link.get_text(strip=True) - full_url = urljoin(url, href) if not full_url or not is_url_in_scope(base_url, full_url): continue @@ -206,7 +204,7 @@ def _get_all_urls(self, url, scanned_urls=None, base_url=None): final_url = f"{parsed_full.scheme}://{parsed_full.netloc}{path}" - is_directory_hint = href.endswith("/") or link_text.endswith("/") + is_directory_hint = href.endswith("/") if is_directory_hint: if not final_url.endswith("/"): final_url += "/" diff --git a/index_ripper.py b/index_ripper.py index 83ae961..9513f4a 100644 --- a/index_ripper.py +++ b/index_ripper.py @@ -31,13 +31,20 @@ def _configure_tk_libraries() -> None: for prefix in (sys.base_prefix, sys.prefix): lib_root = os.path.join(prefix, "lib") - tcl_library = os.path.join(lib_root, "tcl8.6") - tk_library = os.path.join(lib_root, "tk8.6") - if os.path.isfile(os.path.join(tcl_library, "init.tcl")) and os.path.isfile( - os.path.join(tk_library, "tk.tcl") - ): - os.environ.setdefault("TCL_LIBRARY", tcl_library) - os.environ.setdefault("TK_LIBRARY", tk_library) + if not os.path.isdir(lib_root): + continue + tcl_dir = tk_dir = None + try: + for entry in os.listdir(lib_root): + if entry.startswith("tcl") and os.path.isfile(os.path.join(lib_root, entry, "init.tcl")): + tcl_dir = os.path.join(lib_root, entry) + if entry.startswith("tk") and os.path.isfile(os.path.join(lib_root, entry, "tk.tcl")): + tk_dir = os.path.join(lib_root, entry) + except OSError: + continue + if tcl_dir and tk_dir: + os.environ.setdefault("TCL_LIBRARY", tcl_dir) + os.environ.setdefault("TK_LIBRARY", tk_dir) return diff --git a/index_ripper_ttkb.py b/index_ripper_ttkb.py index ca78d2d..80ec4fb 100644 --- a/index_ripper_ttkb.py +++ b/index_ripper_ttkb.py @@ -24,13 +24,20 @@ def _configure_tk_libraries() -> None: for prefix in (sys.base_prefix, sys.prefix): lib_root = os.path.join(prefix, "lib") - tcl_library = os.path.join(lib_root, "tcl8.6") - tk_library = os.path.join(lib_root, "tk8.6") - if os.path.isfile(os.path.join(tcl_library, "init.tcl")) and os.path.isfile( - os.path.join(tk_library, "tk.tcl") - ): - os.environ.setdefault("TCL_LIBRARY", tcl_library) - os.environ.setdefault("TK_LIBRARY", tk_library) + if not os.path.isdir(lib_root): + continue + tcl_dir = tk_dir = None + try: + for entry in os.listdir(lib_root): + if entry.startswith("tcl") and os.path.isfile(os.path.join(lib_root, entry, "init.tcl")): + tcl_dir = os.path.join(lib_root, entry) + if entry.startswith("tk") and os.path.isfile(os.path.join(lib_root, entry, "tk.tcl")): + tk_dir = os.path.join(lib_root, entry) + except OSError: + continue + if tcl_dir and tk_dir: + os.environ.setdefault("TCL_LIBRARY", tcl_dir) + os.environ.setdefault("TK_LIBRARY", tk_dir) return diff --git a/test_ui_ctk.py b/test_ui_ctk.py index fab8445..b247411 100644 --- a/test_ui_ctk.py +++ b/test_ui_ctk.py @@ -83,13 +83,12 @@ def test_has_download_controls(self): self.assertTrue(hasattr(app, "threads_var")) self.assertTrue(hasattr(app, "toggle_panels_btn")) - def test_has_treeview(self): + def test_has_file_tree(self): import ui_ctk, importlib importlib.reload(ui_ctk) app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) app.window.after(0, app.window.destroy) app.run() - # CTk version uses custom FileTree components, not ttk.Treeview self.assertTrue(hasattr(app, "tree_nodes")) self.assertTrue(hasattr(app, "tree_roots")) self.assertTrue(hasattr(app, "tree_scroll_frame")) diff --git a/ui_ctk.py b/ui_ctk.py index fe468f4..f73a9b3 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -1229,8 +1229,7 @@ def add_file(self, dir_path: str, url: str, file_name: str, size, file_type: str label = ext if ext else "(no ext)" cb.configure(text=f"{label} ({self.file_type_counts[ext]})") var = self.file_types.get(ext) - if var is not None and not var.get(): - return + filtered_out = var is not None and not var.get() _icon, group = self._file_icon_and_group(file_name, file_type) node_id = self._next_node_id() @@ -1244,6 +1243,7 @@ def add_file(self, dir_path: str, url: str, file_name: str, size, file_type: str file_type=file_type or "", icon_group=group, checked=full_path in self.checked_items, + hidden=filtered_out, ) self.tree_nodes[node_id] = node if parent_id: From 9efedc66461051c5bca3aaecf18978934863aaf3 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Thu, 19 Mar 2026 02:05:36 +0800 Subject: [PATCH 53/55] refactor: extract shared utilities, eliminate duplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move _configure_tk_libraries to app_utils.configure_tk_libraries (was duplicated in index_ripper.py and index_ripper_ttkb.py) - Move _cleanup_partial_file to app_utils.cleanup_partial_file and remove TOCTOU os.path.exists check before os.remove - Extract rebuild_executor() to app_utils — used by index_ripper.py, ui_ctk.py (was duplicated inline, ui_ttkb.py had different semantics) - Extract build_download_path() to app_utils — consolidates sanitize + safe_join pattern used by both download_selected implementations - Simplify backend.py download_file: eliminate stringly-typed abort_reason, re-check flags directly; merge duplicate RequestException/IOError handlers into one block - Memoize _filter_tree_by_term matches() to avoid O(N^2) re-evaluation Co-Authored-By: Claude Opus 4.6 --- app_utils.py | 50 ++++++++++++++++++++++++++++++++++++++++++++ backend.py | 49 ++++++++++++------------------------------- index_ripper.py | 36 ++++++------------------------- index_ripper_ttkb.py | 26 ++--------------------- ui_ctk.py | 30 +++++++++++++------------- 5 files changed, 86 insertions(+), 105 deletions(-) diff --git a/app_utils.py b/app_utils.py index f321077..36a1f60 100644 --- a/app_utils.py +++ b/app_utils.py @@ -2,6 +2,9 @@ import os import posixpath +import sys +import threading +from concurrent.futures import ThreadPoolExecutor from urllib.parse import urlparse from urllib.parse import unquote @@ -105,3 +108,50 @@ def default_download_folder(url: str, fallback_root: str) -> str: if not site_name: site_name = "downloads" return os.path.join(fallback_root, site_name) + + +def cleanup_partial_file(file_path: str) -> None: + """Best-effort removal of a partial download file.""" + try: + os.remove(file_path) + except OSError: + pass + + +def configure_tk_libraries() -> None: + """Set Tcl/Tk library env vars for uv-managed Python when missing.""" + if os.environ.get("TCL_LIBRARY") and os.environ.get("TK_LIBRARY"): + return + for prefix in (sys.base_prefix, sys.prefix): + lib_root = os.path.join(prefix, "lib") + if not os.path.isdir(lib_root): + continue + tcl_dir = tk_dir = None + try: + for entry in os.listdir(lib_root): + if entry.startswith("tcl") and os.path.isfile(os.path.join(lib_root, entry, "init.tcl")): + tcl_dir = os.path.join(lib_root, entry) + if entry.startswith("tk") and os.path.isfile(os.path.join(lib_root, entry, "tk.tcl")): + tk_dir = os.path.join(lib_root, entry) + except OSError: + continue + if tcl_dir and tk_dir: + os.environ.setdefault("TCL_LIBRARY", tcl_dir) + os.environ.setdefault("TK_LIBRARY", tk_dir) + return + + +def rebuild_executor(old_executor: ThreadPoolExecutor, max_workers: int) -> ThreadPoolExecutor: + """Create a new executor and shut down the old one in a background thread.""" + new_executor = ThreadPoolExecutor(max_workers=max_workers) + threading.Thread(target=old_executor.shutdown, kwargs={"wait": True, "cancel_futures": False}, daemon=True).start() + return new_executor + + +def build_download_path(download_root: str, path_segments: list[str], file_name: str) -> tuple[str, str]: + """Return (target_dir, file_path) for a download. Raises ValueError on path escape.""" + safe_segments = [sanitize_path_segment(s) for s in path_segments if s] + safe_file = sanitize_filename(file_name) + target_dir = safe_join(download_root, safe_segments) if safe_segments else download_root + file_path = safe_join(download_root, safe_segments + [safe_file]) + return target_dir, file_path diff --git a/backend.py b/backend.py index 2ccffc8..b8261d0 100644 --- a/backend.py +++ b/backend.py @@ -12,15 +12,7 @@ import requests from bs4 import BeautifulSoup -from app_utils import is_url_in_scope - -def _cleanup_partial_file(file_path: str) -> None: - """Remove partial download file if it exists.""" - try: - if os.path.exists(file_path): - os.remove(file_path) - except OSError: - pass # Best-effort cleanup +from app_utils import cleanup_partial_file, is_url_in_scope @@ -306,19 +298,11 @@ def download_file(self, url, file_path, file_name, cancel_event=None): total_size = int(response.headers.get("content-length", 0)) block_size = 8192 downloaded = 0 - aborted = False - abort_reason = None with open(file_path, "wb") as file_handle: for data in response.iter_content(block_size): self.ui_manager.pause_event.wait() - if cancel_event is not None and cancel_event.is_set(): - aborted = True - abort_reason = "canceled" - break - if self.should_stop: - aborted = True - abort_reason = "stopped" + if (cancel_event is not None and cancel_event.is_set()) or self.should_stop: break if not data: break @@ -329,9 +313,12 @@ def download_file(self, url, file_path, file_name, cancel_event=None): progress = (downloaded / total_size) * 100 self.ui_manager.update_progress(file_path, file_name, progress) - if aborted: - _cleanup_partial_file(file_path) - if abort_reason == "canceled": + # Re-check abort flags after file handle is closed + canceled = cancel_event is not None and cancel_event.is_set() + stopped = self.should_stop + if canceled or stopped: + cleanup_partial_file(file_path) + if canceled: self.ui_manager.log_message(f"[Download] Canceled: {file_name}") try: self.ui_manager.update_download_status(file_path, "Canceled") @@ -348,23 +335,13 @@ def download_file(self, url, file_path, file_name, cancel_event=None): pass return True - except requests.exceptions.RequestException as ex: - self._log(f"[Download] Error downloading {file_name}: {str(ex)}") - _cleanup_partial_file(file_path) + except (requests.exceptions.RequestException, IOError) as ex: + label = "File error" if isinstance(ex, IOError) else "Error downloading" + self._log(f"[Download] {label} {file_name}: {ex}") + cleanup_partial_file(file_path) try: self.ui_manager.update_download_status(file_path, "Failed") - self.ui_manager.log_message(f"[Download] Error: {file_name} - {ex}") - except AttributeError: - pass - return False - except IOError as ex: - self._log(f"[Download] File error for {file_name}: {str(ex)}") - _cleanup_partial_file(file_path) - try: - self.ui_manager.update_download_status(file_path, "Failed") - self.ui_manager.log_message( - f"[Download] File error: {file_name} - {ex}" - ) + self.ui_manager.log_message(f"[Download] {label}: {file_name} - {ex}") except AttributeError: pass return False diff --git a/index_ripper.py b/index_ripper.py index 9513f4a..847d5f0 100644 --- a/index_ripper.py +++ b/index_ripper.py @@ -24,31 +24,8 @@ raise SystemExit(0) -def _configure_tk_libraries() -> None: - """Set Tcl/Tk library env vars for uv-managed Python when missing.""" - if os.environ.get("TCL_LIBRARY") and os.environ.get("TK_LIBRARY"): - return - - for prefix in (sys.base_prefix, sys.prefix): - lib_root = os.path.join(prefix, "lib") - if not os.path.isdir(lib_root): - continue - tcl_dir = tk_dir = None - try: - for entry in os.listdir(lib_root): - if entry.startswith("tcl") and os.path.isfile(os.path.join(lib_root, entry, "init.tcl")): - tcl_dir = os.path.join(lib_root, entry) - if entry.startswith("tk") and os.path.isfile(os.path.join(lib_root, entry, "tk.tcl")): - tk_dir = os.path.join(lib_root, entry) - except OSError: - continue - if tcl_dir and tk_dir: - os.environ.setdefault("TCL_LIBRARY", tcl_dir) - os.environ.setdefault("TK_LIBRARY", tk_dir) - return - - -_configure_tk_libraries() +from app_utils import configure_tk_libraries +configure_tk_libraries() import tkinter as tk from tkinter import filedialog, messagebox, ttk @@ -61,8 +38,10 @@ def _configure_tk_libraries() -> None: from backend import Backend from ui_downloads import DownloadsPanel from app_utils import ( + build_download_path, default_download_folder, normalize_extension, + rebuild_executor, safe_join, sanitize_filename, sanitize_path_segment, @@ -1191,12 +1170,9 @@ def update_thread_count(self, new_count_str: str): """Updates the number of concurrent download threads.""" try: new_count = int(new_count_str) - if 1 <= new_count <= 10: - old_executor = self.executor + if 1 <= new_count <= 10 and new_count != self.max_workers: self.max_workers = new_count - self.executor = ThreadPoolExecutor(max_workers=self.max_workers) - # Shutdown old executor in background to avoid blocking UI thread - threading.Thread(target=old_executor.shutdown, kwargs={"wait": True, "cancel_futures": False}, daemon=True).start() + self.executor = rebuild_executor(self.executor, self.max_workers) except (ValueError, TypeError): pass diff --git a/index_ripper_ttkb.py b/index_ripper_ttkb.py index 80ec4fb..bc37b57 100644 --- a/index_ripper_ttkb.py +++ b/index_ripper_ttkb.py @@ -18,30 +18,8 @@ raise SystemExit(0) -def _configure_tk_libraries() -> None: - if os.environ.get("TCL_LIBRARY") and os.environ.get("TK_LIBRARY"): - return - - for prefix in (sys.base_prefix, sys.prefix): - lib_root = os.path.join(prefix, "lib") - if not os.path.isdir(lib_root): - continue - tcl_dir = tk_dir = None - try: - for entry in os.listdir(lib_root): - if entry.startswith("tcl") and os.path.isfile(os.path.join(lib_root, entry, "init.tcl")): - tcl_dir = os.path.join(lib_root, entry) - if entry.startswith("tk") and os.path.isfile(os.path.join(lib_root, entry, "tk.tcl")): - tk_dir = os.path.join(lib_root, entry) - except OSError: - continue - if tcl_dir and tk_dir: - os.environ.setdefault("TCL_LIBRARY", tcl_dir) - os.environ.setdefault("TK_LIBRARY", tk_dir) - return - - -_configure_tk_libraries() +from app_utils import configure_tk_libraries +configure_tk_libraries() def main() -> int: diff --git a/ui_ctk.py b/ui_ctk.py index f73a9b3..34f1b53 100644 --- a/ui_ctk.py +++ b/ui_ctk.py @@ -17,7 +17,7 @@ import customtkinter as ctk -from app_utils import default_download_folder, normalize_extension, safe_join, sanitize_filename, sanitize_path_segment +from app_utils import build_download_path, default_download_folder, normalize_extension, rebuild_executor, safe_join, sanitize_filename from backend import Backend from ui_downloads import DownloadsPanel from ui_theme import ( @@ -1303,14 +1303,10 @@ def download_selected(self) -> None: if not url or not file_name: continue - safe_name = sanitize_filename(file_name) - - # Preserve subfolder structure from the scanned path dir_path = info.get("path", "") - path_segments = [sanitize_path_segment(seg) for seg in dir_path.strip("/").split("/") if seg] + path_segments = [seg for seg in dir_path.strip("/").split("/") if seg] try: - target_dir = safe_join(self.download_path, path_segments) if path_segments else self.download_path - file_path = safe_join(self.download_path, path_segments + [safe_name]) + target_dir, file_path = build_download_path(self.download_path, path_segments, file_name) except ValueError: self.log_message(f"[Download] Skipped unsafe path: {full_path}") continue @@ -1389,13 +1385,19 @@ def _filter_tree_by_term(self, term: str) -> None: """Mark nodes hidden if they (and their descendants) don't match term.""" term = term.lower() + cache: dict[str, bool] = {} + def matches(node_id: str) -> bool: + if node_id in cache: + return cache[node_id] node = self.tree_nodes.get(node_id) if node is None: - return False - if term in node.name.lower() or term in node.full_path.lower(): - return True - return any(matches(child_id) for child_id in node.children) + cache[node_id] = False + elif term in node.name.lower() or term in node.full_path.lower(): + cache[node_id] = True + else: + cache[node_id] = any(matches(child_id) for child_id in node.children) + return cache[node_id] def apply_visibility(node_id: str) -> None: node = self.tree_nodes.get(node_id) @@ -1404,7 +1406,7 @@ def apply_visibility(node_id: str) -> None: if matches(node_id): node.hidden = False if node.kind == "folder": - node.expanded = True # auto-expand matching folders + node.expanded = True else: node.hidden = True for child_id in node.children: @@ -1429,10 +1431,8 @@ def update_thread_count(self, value=None) -> None: new_count = max(1, min(10, n)) if new_count == self.max_workers: return - old_executor = self.executor self.max_workers = new_count - self.executor = ThreadPoolExecutor(max_workers=self.max_workers) - threading.Thread(target=old_executor.shutdown, kwargs={"wait": True, "cancel_futures": False}, daemon=True).start() + self.executor = rebuild_executor(self.executor, self.max_workers) def toggle_panels(self) -> None: if self.panels_visible: From 64d78d4515d10da7cdce7ca2ef7c090acf45c616 Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Sun, 22 Mar 2026 18:44:07 +0800 Subject: [PATCH 54/55] refactor: restructure Python project into src/index_ripper package - Move all source modules into src/index_ripper/ with proper package structure - Split ui_ctk.py (1500 LOC) into app.py, ui/filetree.py, ui/filters.py - Move tests into tests/ directory, consolidate test_app_utils + test_security_utils - Remove legacy code: index_ripper.py (old UI), ui_ttkb.py, pyside_poc.py, POC_MATRIX.md - Remove ttkbootstrap dependency - Add hatchling build-system for src layout - Update CI, PyInstaller, and README for new structure - Fix: duplicate macOS keyboard bindings, expand folders by default - All 42 tests pass, smoke test and self-test verified Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 8 +- .gitignore | 7 + POC_MATRIX.md | 136 -- README.md | 14 +- README_zh.md | 14 +- index_ripper.py | 1434 ----------------- index_ripper_ttkb.py | 42 - macos.spec | 6 +- pyproject.toml | 12 +- pyside_poc.py | 194 --- src/index_ripper/__init__.py | 3 + src/index_ripper/__main__.py | 36 + ui_ctk.py => src/index_ripper/app.py | 301 +--- backend.py => src/index_ripper/backend.py | 2 +- self_test.py => src/index_ripper/self_test.py | 0 .../index_ripper/settings.py | 0 src/index_ripper/ui/__init__.py | 1 + .../index_ripper/ui/downloads.py | 0 src/index_ripper/ui/filetree.py | 163 ++ src/index_ripper/ui/filters.py | 75 + ui_theme.py => src/index_ripper/ui/theme.py | 0 app_utils.py => src/index_ripper/utils.py | 0 test_app_utils.py | 23 - test_ui_downloads.py | 17 - test_ui_ttkb.py | 21 - tests/__init__.py | 0 test_ui_ctk.py => tests/test_app_smoke.py | 38 +- test_backend.py => tests/test_backend.py | 2 +- tests/test_downloads.py | 17 + .../test_settings.py | 2 +- test_ui_theme.py => tests/test_theme.py | 16 +- test_security_utils.py => tests/test_utils.py | 21 +- ui_ttkb.py | 1387 ---------------- uv.lock | 103 +- 34 files changed, 429 insertions(+), 3666 deletions(-) delete mode 100644 POC_MATRIX.md delete mode 100644 index_ripper.py delete mode 100644 index_ripper_ttkb.py delete mode 100644 pyside_poc.py create mode 100644 src/index_ripper/__init__.py create mode 100644 src/index_ripper/__main__.py rename ui_ctk.py => src/index_ripper/app.py (83%) rename backend.py => src/index_ripper/backend.py (99%) rename self_test.py => src/index_ripper/self_test.py (100%) rename settings_store.py => src/index_ripper/settings.py (100%) create mode 100644 src/index_ripper/ui/__init__.py rename ui_downloads.py => src/index_ripper/ui/downloads.py (100%) create mode 100644 src/index_ripper/ui/filetree.py create mode 100644 src/index_ripper/ui/filters.py rename ui_theme.py => src/index_ripper/ui/theme.py (100%) rename app_utils.py => src/index_ripper/utils.py (100%) delete mode 100644 test_app_utils.py delete mode 100644 test_ui_downloads.py delete mode 100644 test_ui_ttkb.py create mode 100644 tests/__init__.py rename test_ui_ctk.py => tests/test_app_smoke.py (84%) rename test_backend.py => tests/test_backend.py (99%) create mode 100644 tests/test_downloads.py rename test_settings_store.py => tests/test_settings.py (93%) rename test_ui_theme.py => tests/test_theme.py (54%) rename test_security_utils.py => tests/test_utils.py (71%) delete mode 100644 ui_ttkb.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4df496f..d9812e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: run: uv sync --extra dev - name: Run unit tests - run: uv run python -m pytest test_backend.py test_app_utils.py test_settings_store.py test_security_utils.py test_ui_theme.py test_ui_downloads.py -v + run: uv run python -m pytest tests/ -v build: name: Build (${{ matrix.os }}) @@ -63,13 +63,13 @@ jobs: - name: Build (Windows) if: matrix.os == 'windows-latest' run: | - uv run pyinstaller --onefile --windowed --icon=app.png --name=IndexRipper --collect-all customtkinter index_ripper.py + uv run pyinstaller --onefile --windowed --icon=app.png --name=IndexRipper --collect-all customtkinter --paths src src/index_ripper/__main__.py - name: Build (macOS) if: startsWith(matrix.os, 'macos-') run: | uv run pyinstaller -F --windowed --name=IndexRipper \ - --collect-all customtkinter --icon=app.png index_ripper.py + --collect-all customtkinter --icon=app.png --paths src src/index_ripper/__main__.py [ -f dist/IndexRipper ] && rm -f dist/IndexRipper || true - name: Sign app (ad-hoc) @@ -81,7 +81,7 @@ jobs: if: matrix.os == 'ubuntu-latest' run: | uv run pyinstaller --onefile --windowed --name=IndexRipper \ - --collect-all customtkinter --icon=app.png index_ripper.py + --collect-all customtkinter --icon=app.png --paths src src/index_ripper/__main__.py - name: Verify artifact exists shell: bash diff --git a/.gitignore b/.gitignore index 0f7bf01..fe1038f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,10 @@ __pycache__/ .sisyphus/ .worktrees/ + +electron-app/node_modules/ +electron-app/dist/ +electron-app/out/ +electron-app/*.tsbuildinfo + +.superpowers/ diff --git a/POC_MATRIX.md b/POC_MATRIX.md deleted file mode 100644 index 2ba86e6..0000000 --- a/POC_MATRIX.md +++ /dev/null @@ -1,136 +0,0 @@ -# PySide6 POC Decision Matrix - -Goal: decide whether a PySide6-based UI POC is acceptable for this repo, using -repeatable, command-driven measurements. - -Artifacts: -- `pyside_poc.py` (POC UI entrypoint) - -## Gates (PASS/FAIL) - -| Metric | Gate (PASS) | FAIL rule | Why it matters | -| --- | ---: | --- | --- | -| Disk footprint (PySide6 venv delta) | <= 600 MB | > 600 MB | Impacts CI cache, local dev disk usage, and distribution practicality | -| Startup time (to `window.show()`) | <= 1.50 s | > 1.50 s | Impacts perceived responsiveness and UX viability | -| Idle memory (RSS after 5s) | <= 250 MB | > 250 MB | Impacts background resource cost and multi-app workflows | - -Notes: -- These gates are intentionally strict enough to be decision-driving. -- Always record OS, Python version, and PySide6 version with the measurements. - -## Measurement Steps (Commands) - -All steps below are manual, but command-driven (no subjective judgments). - -### 0) Create a clean venv for the POC - -Unix/macOS: - -```bash -python3 -m venv .venv-pyside6-poc -source .venv-pyside6-poc/bin/activate -python3 -m pip install -U pip -``` - -Windows PowerShell: - -```powershell -python -m venv .venv-pyside6-poc -.\.venv-pyside6-poc\Scripts\Activate.ps1 -python -m pip install -U pip -``` - -### 1) Disk footprint (venv delta) - -1) Measure baseline venv size (before PySide6 install): - -Unix/macOS: - -```bash -du -sk .venv-pyside6-poc -``` - -Windows PowerShell: - -```powershell -(Get-ChildItem -Recurse .\.venv-pyside6-poc | Measure-Object -Sum Length).Sum -``` - -2) Install PySide6: - -```bash -python3 -m pip install PySide6 -``` - -3) Measure venv size again: - -Unix/macOS: - -```bash -du -sk .venv-pyside6-poc -``` - -Windows PowerShell: - -```powershell -(Get-ChildItem -Recurse .\.venv-pyside6-poc | Measure-Object -Sum Length).Sum -``` - -4) Record delta: -- Unix/macOS: delta_MB = (after_kb - before_kb) / 1024 -- Windows: delta_MB = (after_bytes - before_bytes) / (1024 * 1024) - -### 2) Startup time (to `window.show()`) - -1) Capture versions: - -```bash -python3 -V -python3 -c "import PySide6; print('PySide6', PySide6.__version__)" -``` - -2) Run the POC with timing enabled (auto-quit so it is scriptable): - -```bash -python3 pyside_poc.py --measure-startup --quit-after 0.2 -``` - -Expected output includes a single line like: - -```text -startup_seconds import=0.1234 ui=0.0456 show=0.2345 -``` - -Gate uses: `show`. - -Optional (repeat 5 times): - -```bash -for i in 1 2 3 4 5; do python3 pyside_poc.py --measure-startup --quit-after 0.2; done -``` - -### 3) Idle memory (RSS after 5 seconds) - -1) Start the app and print PID (keep it alive long enough to sample): - -```bash -python3 pyside_poc.py --print-pid --quit-after 30 -``` - -2) In a second terminal, wait 5 seconds then sample RSS: - -Unix/macOS: - -```bash -ps -o rss= -p -``` - -Interpretation: output is RSS in KB. Convert to MB: MB = KB / 1024. - -Windows PowerShell: - -```powershell -Get-Process -Id | Select-Object Id, WorkingSet64 -``` - -Interpretation: `WorkingSet64` is bytes. Convert to MB: MB = bytes / (1024 * 1024). diff --git a/README.md b/README.md index 7027aad..0751f30 100644 --- a/README.md +++ b/README.md @@ -96,16 +96,16 @@ uv sync ```bash # Run with uv (CustomTkinter UI) -uv run python index_ripper.py +uv run python -m index_ripper # Quick non-interactive smoke check -uv run python index_ripper.py --smoke +uv run python -m index_ripper --smoke # UI smoke check -uv run python index_ripper.py --ui-smoke +uv run python -m index_ripper --ui-smoke # Deterministic self-test (no real network) -uv run python index_ripper.py --self-test +uv run python -m index_ripper --self-test ``` ## Download Prebuilt Binaries @@ -165,7 +165,7 @@ Windows (PowerShell): ```powershell uv pip install pyinstaller uv run pyinstaller --onefile --windowed --icon=app.png --name=IndexRipper ` - --collect-all customtkinter index_ripper.py + --collect-all customtkinter --paths src src/index_ripper/__main__.py ``` macOS/Linux (bash): @@ -174,10 +174,10 @@ macOS/Linux (bash): uv pip install pyinstaller # macOS .app uv run pyinstaller -F --windowed --name=IndexRipper \ - --collect-all customtkinter --icon=app.png index_ripper.py + --collect-all customtkinter --icon=app.png --paths src src/index_ripper/__main__.py # Linux single binary uv run pyinstaller --onefile --windowed --name=IndexRipper \ - --collect-all customtkinter --icon=app.png index_ripper.py + --collect-all customtkinter --icon=app.png --paths src src/index_ripper/__main__.py ``` ## Important Notes diff --git a/README_zh.md b/README_zh.md index 7ea03e7..67824dd 100644 --- a/README_zh.md +++ b/README_zh.md @@ -96,16 +96,16 @@ uv sync ```bash # 使用 uv 執行(CustomTkinter UI) -uv run python index_ripper.py +uv run python -m index_ripper # 快速無互動 smoke 檢查 -uv run python index_ripper.py --smoke +uv run python -m index_ripper --smoke # UI smoke 檢查 -uv run python index_ripper.py --ui-smoke +uv run python -m index_ripper --ui-smoke # 可重現的 self-test(不依賴真網路) -uv run python index_ripper.py --self-test +uv run python -m index_ripper --self-test ``` ## 下載已編譯執行檔 @@ -165,7 +165,7 @@ Windows(PowerShell): ```powershell uv pip install pyinstaller uv run pyinstaller --onefile --windowed --icon=app.png --name=IndexRipper ` - --collect-all customtkinter index_ripper.py + --collect-all customtkinter --paths src src/index_ripper/__main__.py ``` macOS/Linux(bash): @@ -174,10 +174,10 @@ macOS/Linux(bash): uv pip install pyinstaller # macOS .app uv run pyinstaller -F --windowed --name=IndexRipper \ - --collect-all customtkinter --icon=app.png index_ripper.py + --collect-all customtkinter --icon=app.png --paths src src/index_ripper/__main__.py # Linux 單一執行檔 uv run pyinstaller --onefile --windowed --name=IndexRipper \ - --collect-all customtkinter --icon=app.png index_ripper.py + --collect-all customtkinter --icon=app.png --paths src src/index_ripper/__main__.py ``` ## 注意事項 diff --git a/index_ripper.py b/index_ripper.py deleted file mode 100644 index 847d5f0..0000000 --- a/index_ripper.py +++ /dev/null @@ -1,1434 +0,0 @@ -"""Index Ripper UI using customtkinter with search, logs, and per-file downloads.""" - -# pylint: disable=too-many-lines -import os -import posixpath -import sys -import threading -from concurrent.futures import ThreadPoolExecutor -from queue import Empty, Queue -from threading import Event, Thread -from urllib.parse import unquote, urljoin, urlparse - -if "--smoke" in sys.argv: - print("SMOKE_OK") - raise SystemExit(0) - -if "--self-test" in sys.argv: - from self_test import run_self_test - - result = run_self_test() - print( - f"SELF_TEST_OK total={result.total} files={result.files} dirs={result.directories}" - ) - raise SystemExit(0) - - -from app_utils import configure_tk_libraries -configure_tk_libraries() - -import tkinter as tk -from tkinter import filedialog, messagebox, ttk - -import customtkinter as ctk # type: ignore -import requests # type: ignore -from requests.adapters import HTTPAdapter # type: ignore -from urllib3.util.retry import Retry # type: ignore - -from backend import Backend -from ui_downloads import DownloadsPanel -from app_utils import ( - build_download_path, - default_download_folder, - normalize_extension, - rebuild_executor, - safe_join, - sanitize_filename, - sanitize_path_segment, - shorten_path, -) -from settings_store import load_settings, save_settings -from ui_theme import ( - action_button_style_name, - apply_app_theme, - configure_action_button_styles, - configure_treeview_style, - treeview_tag_colors, - ui_tokens, -) - - -class WebsiteCopier: - """A GUI application for downloading files from a website directory listing.""" - - USER_AGENT = ( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " - "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/91.0.4472.124 Safari/537.36" - ) - - def __init__(self): - """Initializes the main window and UI components.""" - apply_app_theme(ctk) - - self.window = ctk.CTk() - self.window.title("Index Ripper") - self.window.geometry("1200x900") - self.window.minsize(900, 650) - configure_action_button_styles(self.window, ctk, ttk) - self.ui_tokens = ui_tokens() - - self.backend = Backend(self) - - # Download control - self.pause_event = Event() - self.current_downloads = [] - - # Add scan control - self.scan_pause_event = Event() - self.scan_pause_event.set() # Initially set to not paused - self.is_scanning = False - - # 添加线程锁,用于保护共享数据 - self.files_dict_lock = threading.Lock() - self.folders_dict_lock = threading.Lock() - - # 初始化队列和处理状态 - self.dir_queue = Queue() - self.file_queue = Queue() - self.scan_item_buffer = Queue() - self.is_processing_dirs = False - self.is_processing_files = False - self.scan_flush_interval_ms = 16 - self.scan_flush_batch_size = 200 - self.scan_flush_job = None - - # --- Main Layout --- - self.window.grid_columnconfigure(0, weight=1) - self.window.grid_rowconfigure(1, weight=1) - - # --- URL and filter area --- - self.top_frame = ctk.CTkFrame(self.window, corner_radius=14, border_width=1) - self.top_frame.grid(row=0, column=0, padx=10, pady=(10, 5), sticky="ew") - self.top_frame.grid_columnconfigure(0, weight=1) - ctk.CTkLabel( - self.top_frame, - text="Index Ripper Studio", - font=ctk.CTkFont(size=20, weight="bold"), - ).grid(row=0, column=0, padx=12, pady=(10, 2), sticky="w") - - # URL input - url_input_frame = ctk.CTkFrame(self.top_frame, fg_color="transparent") - url_input_frame.grid(row=1, column=0, padx=10, pady=5, sticky="ew") - url_input_frame.grid_columnconfigure(1, weight=1) - ctk.CTkLabel( - url_input_frame, text="URL:", font=ctk.CTkFont(weight="bold") - ).grid(row=0, column=0, padx=(0, 5)) - self.url_entry = ctk.CTkEntry( - url_input_frame, placeholder_text="https://example.com", height=36 - ) - self.url_entry.grid(row=0, column=1, sticky="ew") - - # Scan buttons - scan_buttons_frame = ctk.CTkFrame(self.top_frame, fg_color="transparent") - scan_buttons_frame.grid(row=1, column=1, padx=10, pady=5) - - self.status_chip = ctk.CTkLabel( - self.top_frame, - text="Ready", - fg_color=("#DCFCE7", "#14532D"), - text_color=("#14532D", "#DCFCE7"), - corner_radius=12, - padx=10, - pady=4, - ) - self.status_chip.grid(row=0, column=2, padx=(0, 10), pady=8, sticky="e") - - self.scan_btn = ttk.Button( - scan_buttons_frame, - text="Scan", - command=self.start_scan, - style=action_button_style_name("primary"), - ) - self.scan_btn.pack(side="left", padx=5) - - self.scan_pause_btn = ttk.Button( - scan_buttons_frame, - text="Pause Scan", - command=self.toggle_scan_pause, - state="disabled", - style=action_button_style_name("secondary"), - ) - self.scan_pause_btn.pack(side="left", padx=5) - - self.clear_scan_btn = ttk.Button( - scan_buttons_frame, - text="Clear", - command=self.clear_scan_results, - style=action_button_style_name("danger"), - ) - self.clear_scan_btn.pack(side="left", padx=5) - - # --- File type filter area --- - filter_frame = ctk.CTkFrame(self.top_frame, corner_radius=10) - filter_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky="ew") - filter_frame.grid_columnconfigure(0, weight=1) - - ctk.CTkLabel( - filter_frame, text="File Type Selection", font=ctk.CTkFont(weight="bold") - ).grid(row=0, column=0, padx=10, pady=(5, 0), sticky="w") - - # File type checkbox container - self.filter_checkboxes_frame = ctk.CTkScrollableFrame( - filter_frame, label_text="", fg_color="transparent" - ) - self.filter_checkboxes_frame.grid(row=1, column=0, padx=5, pady=5, sticky="ew") - - # Variable to store file types - self.file_types = {} # {'.pdf': BooleanVar(), '.jpg': BooleanVar(), ...} - self.file_type_counts = {} # {'.pdf': 0, '.jpg': 0, ...} - - # Select/deselect all buttons - select_buttons_frame = ctk.CTkFrame(filter_frame, fg_color="transparent") - select_buttons_frame.grid(row=2, column=0, padx=10, pady=5, sticky="w") - ctk.CTkButton( - select_buttons_frame, text="Select All", command=self.select_all_types - ).pack(side="left", padx=2) - ctk.CTkButton( - select_buttons_frame, text="Deselect All", command=self.deselect_all_types - ).pack(side="left", padx=2) - - # --- File list area --- - self.tree_frame = ctk.CTkFrame(self.window, corner_radius=14, border_width=1) - self.tree_frame.grid(row=1, column=0, padx=10, pady=5, sticky="nsew") - self.tree_frame.grid_rowconfigure(1, weight=1) - self.tree_frame.grid_columnconfigure(0, weight=1) - - # Search bar - search_frame = ctk.CTkFrame(self.tree_frame) - search_frame.grid(row=0, column=0, columnspan=2, padx=5, pady=5, sticky="ew") - search_frame.grid_columnconfigure(1, weight=1) - - ctk.CTkLabel(search_frame, text="Search:").grid(row=0, column=0, padx=(5, 2)) - self.search_var = ctk.StringVar() - self.search_var.trace_add("write", self.on_search_filter_changed) - self.search_entry = ctk.CTkEntry( - search_frame, - textvariable=self.search_var, - state="disabled", - placeholder_text="Filter by file or folder name...", - height=32, - ) - self.search_entry.grid(row=0, column=1, sticky="ew") - self.full_tree_backup = {} - - configure_treeview_style(self.window, ctk, ttk) - - self.tree = ttk.Treeview( - self.tree_frame, - selectmode="browse", - show=("tree", "headings"), - style="Treeview", - ) - - colors = treeview_tag_colors(self.window) - self.tree.tag_configure("oddrow", background=colors["oddrow"]) - self.tree.tag_configure("evenrow", background=colors["evenrow"]) - self.tree.tag_configure("checked", foreground=colors["checked"]) - - # Set column configuration - self.tree["columns"] = ("size", "type") - self.tree.heading("#0", text="Name", command=lambda: self.sort_tree("name")) - self.tree.heading("size", text="Size", command=lambda: self.sort_tree("size")) - self.tree.heading("type", text="Type", command=lambda: self.sort_tree("type")) - - self.tree.column("#0", minwidth=300, width=600, stretch=True) - self.tree.column("size", width=120, anchor="center") - self.tree.column("type", width=200, anchor="center") - - # Bind right-click menu - self.tree.bind("", self.show_context_menu) - - # Create right-click menu (using tk.Menu as customtkinter doesn't have one) - self.context_menu = tk.Menu(self.window, tearoff=0) - self.context_menu.add_command(label="Select All", command=self.select_all) - self.context_menu.add_command(label="Deselect All", command=self.deselect_all) - self.context_menu.add_separator() - self.context_menu.add_command(label="Expand All", command=self.expand_all) - self.context_menu.add_command(label="Collapse All", command=self.collapse_all) - - # Sort status - self.sort_column = None - self.sort_reverse = False - - self.checked_items = set() - self.checkbox_checked = "✔" - self.folder_icon = "📁" - self.file_icon = "📄" - self.folders = {} - - self.scrollbar = ctk.CTkScrollbar(self.tree_frame, command=self.tree.yview) - self.h_scrollbar = ctk.CTkScrollbar( - self.tree_frame, orientation="horizontal", command=self.tree.xview - ) - self.tree.configure( - yscrollcommand=self.scrollbar.set, xscrollcommand=self.h_scrollbar.set - ) - - self.tree.grid(row=1, column=0, sticky="nsew") - self.scrollbar.grid(row=1, column=1, sticky="ns") - self.h_scrollbar.grid(row=2, column=0, sticky="ew") - - self.tree.bind("", self.on_tree_click) - self.tree.bind("", self.on_tree_space) - self.tree.bind("", self.on_tree_enter) - self.tree.bind("", self.on_tree_select_all) - self.tree.bind("", self.on_tree_select_all) - - # --- Download control area --- - control_frame = ctk.CTkFrame(self.window, corner_radius=14, border_width=1) - control_frame.grid(row=2, column=0, padx=10, pady=5, sticky="ew") - - self.download_btn = ttk.Button( - control_frame, - text="Download Selected Files", - command=self.download_selected, - style=action_button_style_name("success"), - ) - self.download_btn.pack(side="left", padx=5, pady=5) - - self.pause_btn = ttk.Button( - control_frame, - text="Pause Download", - command=self.toggle_pause, - state="disabled", - style=action_button_style_name("secondary"), - ) - self.pause_btn.pack(side="left", padx=5, pady=5) - - self.path_btn = ttk.Button( - control_frame, - text="Choose Download Location", - command=self.choose_download_path, - style=action_button_style_name("secondary"), - ) - self.path_btn.pack(side="left", padx=5, pady=5) - - # Thread download count control - threads_frame = ctk.CTkFrame(control_frame, fg_color="transparent") - threads_frame.pack(side="right", padx=5, pady=5) - ctk.CTkLabel(threads_frame, text="Concurrent Downloads:").pack(side="left") - self.threads_var = ctk.StringVar(value="5") - threads_option_menu = ctk.CTkOptionMenu( - threads_frame, - values=[str(i) for i in range(1, 11)], - variable=self.threads_var, - command=self.update_thread_count, - **self.ui_tokens["option_menu"], - ) - threads_option_menu.pack(side="left", padx=5) - - # Toggle button to show/hide bottom panels (Downloads/Logs) - self.panels_visible = True - self.toggle_panels_btn = ttk.Button( - control_frame, - text="Hide Panels", - command=self.toggle_panels, - style=action_button_style_name("secondary"), - ) - self.toggle_panels_btn.pack(side="right", padx=5) - - # --- Progress bar --- - self.progress_frame = ctk.CTkFrame(self.window, corner_radius=14, border_width=1) - self.progress_frame.grid(row=3, column=0, padx=10, pady=(5, 10), sticky="ew") - self.progress_frame.grid_columnconfigure(0, weight=1) - - self.progress_var = ctk.DoubleVar() - self.progress_bar = ctk.CTkProgressBar( - self.progress_frame, variable=self.progress_var - ) - self.progress_bar.set(0) - self.progress_bar.grid(row=0, column=0, padx=5, pady=2, sticky="ew") - - self.progress_label = ctk.CTkLabel(self.progress_frame, text="") - self.progress_label.grid(row=1, column=0, padx=5, pady=2, sticky="w") - - # --- Panels (Downloads + Logs) inside a Tabview --- - self.panels_tabview = ctk.CTkTabview( - self.window, corner_radius=14, **self.ui_tokens["tabview"] - ) - self.panels_tabview.grid(row=4, column=0, padx=10, pady=(0, 10), sticky="ew") - - self.panels_tabview.add("Downloads") - self.panels_tabview.add("Logs") - - downloads_tab = self.panels_tabview.tab("Downloads") - self.downloads_frame = ctk.CTkScrollableFrame(downloads_tab, label_text="") - self.downloads_frame.pack(fill="x", expand=False) - - logs_tab = self.panels_tabview.tab("Logs") - self.log_text = ctk.CTkTextbox( - logs_tab, - height=self.ui_tokens["logs"]["height"], - font=self.ui_tokens["logs"]["font"], - text_color=self.ui_tokens["logs"]["text_color"], - fg_color=self.ui_tokens["logs"]["fg_color"], - ) - self.log_text.pack(fill="x", expand=False) - self.log_text.configure(state="disabled") - - try: - self.panels_tabview.set("Logs") - except tk.TclError: - pass - - # Track per-file download UI elements and cancel events - self.downloads_panel = DownloadsPanel( - self.downloads_frame, ctk, tk, threading, tokens=self.ui_tokens - ) - - # Load and apply saved settings (panels visibility and active tab) - self._settings_path = os.path.join( - os.path.expanduser("~"), - ".index_ripper_settings.json", - ) - saved = load_settings(self._settings_path) if os.path.exists(self._settings_path) else {} - # Active tab - active = saved.get("active_tab") - if active in ("Downloads", "Logs"): - try: - self.panels_tabview.set(active) - except tk.TclError: - pass - visible = bool(saved.get("panels_visible", True)) - if visible: - self.panels_tabview.grid() - self.panels_visible = True - self.toggle_panels_btn.configure(text="Hide Panels") - else: - self.panels_tabview.grid_remove() - self.panels_visible = False - self.toggle_panels_btn.configure(text="Show Panels") - - # Initialize session with retry - retry_strategy = Retry( - total=3, - backoff_factor=0.1, - status_forcelist=[500, 502, 503, 504], - ) - - self.files_dict = {} - self.download_path = "" - self.is_paused = False - self.download_queue = Queue() - self.max_workers = 5 - self.executor = ThreadPoolExecutor(max_workers=self.max_workers) - self.active_downloads = [] - - self.session = requests.Session() - adapter = HTTPAdapter( - max_retries=retry_strategy, - pool_connections=10, - pool_maxsize=10, - pool_block=False, - ) - self.session.mount("http://", adapter) - self.session.mount("https://", adapter) - self.timeout = (10, 30) - - self.total_urls = 0 - self.scanned_urls = 0 - self.window.protocol("WM_DELETE_WINDOW", self.on_closing) - self.should_stop = False - self.window.bind("", self.focus_search) - self.window.bind("", self.focus_logs) - self.window.bind("", self.clear_search) - - def notify_info(self, title: str, message: str) -> None: - try: - messagebox.showinfo(title, message) - except Exception: - self.log_message(f"[INFO] {title}: {message}") - - def notify_warning(self, title: str, message: str) -> None: - try: - messagebox.showwarning(title, message) - except Exception: - self.log_message(f"[WARN] {title}: {message}") - - def notify_error(self, title: str, message: str) -> None: - try: - messagebox.showerror(title, message) - except Exception: - self.log_message(f"[ERROR] {title}: {message}") - - def _set_status(self, text: str, state: str = "ready") -> None: - """Set status chip text and style based on scan state.""" - status_styles = { - "ready": { - "fg_color": ("#DCFCE7", "#14532D"), - "text_color": ("#14532D", "#DCFCE7"), - }, - "scanning": { - "fg_color": ("#DBEAFE", "#1E3A8A"), - "text_color": ("#1E3A8A", "#DBEAFE"), - }, - "paused": { - "fg_color": ("#FEF3C7", "#78350F"), - "text_color": ("#78350F", "#FEF3C7"), - }, - "stopping": { - "fg_color": ("#FEE2E2", "#7F1D1D"), - "text_color": ("#7F1D1D", "#FEE2E2"), - }, - } - - style = status_styles.get(state, status_styles["ready"]) - self.status_chip.configure( - text=text, - fg_color=style["fg_color"], - text_color=style["text_color"], - ) - - def toggle_panels(self): - """Show or hide the bottom panels (Downloads/Logs).""" - if self.panels_visible: - try: - self.panels_tabview.grid_remove() - except tk.TclError: - pass - self.panels_visible = False - self.toggle_panels_btn.configure(text="Show Panels") - else: - try: - self.panels_tabview.grid() - except tk.TclError: - pass - self.panels_visible = True - self.toggle_panels_btn.configure(text="Hide Panels") - # Save setting - # Persist setting (best-effort) - data = { - "panels_visible": bool(self.panels_visible), - "active_tab": str(self.panels_tabview.get()), - } - save_settings(self._settings_path, data) - - def choose_download_path(self): - """Opens a dialog to choose a download directory.""" - path = filedialog.askdirectory(title="Choose Download Location") - if path: - self.download_path = path - self.path_btn.configure(text=f"Location: {shorten_path(path)}") - - def toggle_pause(self): - """Pauses or resumes the download process.""" - if self.is_paused: - self.pause_event.set() - self.pause_btn.configure(text="Pause Download") - self.is_paused = False - else: - self.pause_event.clear() - self.pause_btn.configure(text="Resume Download") - self.is_paused = True - - def toggle_scan_pause(self): - """Toggle scan pause state""" - if self.scan_pause_event.is_set(): - self.scan_pause_event.clear() - self.scan_pause_btn.configure(text="Resume Scan") - self.progress_label.configure(text="Scan paused") - self._set_status("Scan Paused", "paused") - else: - self.scan_pause_event.set() - self.scan_pause_btn.configure(text="Pause Scan") - self._set_status("Scanning", "scanning") - - def filter_file_type(self, file_name): - """Check if file should be displayed""" - ext = normalize_extension(file_name) - - existing_ext = next( - (e for e in self.file_types if e.lower() == ext.lower()), None - ) - - return existing_ext is not None and self.file_types[existing_ext].get() - - def create_folder_structure(self, path, url): - """Create folder structure and return ID of last folder""" - path = unquote(path.replace("\\", "/")) - path = posixpath.normpath(path).strip("/") - - if not path: - return "" - - parts = path.split("/") - current_path = "" - parent = "" - - for part in parts: - if not part: - continue - - current_path = posixpath.join(current_path, part) if current_path else part - - with self.folders_dict_lock: - if current_path in self.folders: - parent = self.folders[current_path]["id"] - continue - - # Check for existing folder by name under the same parent - found = False - for child in self.tree.get_children(parent): - child_text = self.tree.item(child)["text"] - child_tags = self.tree.item(child)["tags"] - if "folder" in child_tags: - # Extract folder name (remove icon) - folder_name = child_text.replace(f"{self.folder_icon} ", "") - if folder_name == part: - parent = child - self.folders[current_path] = { - "id": child, - "url": urljoin(url, current_path), - "full_path": current_path, - } - found = True - break - if found: - continue - - folder_id = self.tree.insert( - parent, - "end", - text=f"{self.folder_icon} {part}", - values=("", "Directory"), - tags=("folder",), - ) - self.folders[current_path] = { - "id": folder_id, - "url": urljoin(url, current_path), - "full_path": current_path, - } - parent = folder_id - return parent - - def _get_ancestors(self, item): - """Get all ancestor nodes of an item""" - ancestors = [] - parent = self.tree.parent(item) - while parent: - ancestors.append(parent) - parent = self.tree.parent(parent) - return ancestors - - def on_tree_click(self, event): - """Handle tree item click event.""" - item = self.tree.identify("item", event.x, event.y) - if not item: - return - - self.tree.focus(item) - self.tree.selection_set(item) - - # Identify the specific element clicked - element = self.tree.identify_element(event.x, event.y) - - # If the click is on the expand/collapse indicator, let the default - # binding handle it and do nothing more. - if "indicator" in element: - return - - # If the click is on the cell (text/icon), toggle the check state. - column = self.tree.identify_column(event.x) - if column == "#0": # Only toggle check if name column is clicked - self.toggle_check(item) - - def _focused_tree_item(self): - item = self.tree.focus() - if item and self.tree.exists(item): - return item - selected = self.tree.selection() - if selected: - candidate = selected[0] - if self.tree.exists(candidate): - return candidate - return "" - - def on_tree_space(self, _event=None): - """Toggle checked state for the focused tree item.""" - item = self._focused_tree_item() - if item: - self.toggle_check(item) - return "break" - return None - - def on_tree_enter(self, _event=None): - """Expand/collapse folders or toggle files for the focused tree item.""" - item = self._focused_tree_item() - if not item: - return None - - tags = self.tree.item(item, "tags") - if "folder" in tags: - self.tree.item(item, open=not bool(self.tree.item(item, "open"))) - else: - self.toggle_check(item) - return "break" - - def on_tree_select_all(self, _event=None): - """Select all files in tree when tree widget has focus.""" - self.select_all() - return "break" - - def toggle_check(self, item, force_check=None): - """Toggle item checked status""" - if not self.tree.exists(item): - return - - tags = list(self.tree.item(item, "tags")) - is_checked = "checked" in tags - - if force_check is not None: - new_check_state = force_check - else: - new_check_state = not is_checked - - if new_check_state: - if "checked" not in tags: - tags.append("checked") - self.checked_items.add(item) - else: - if "checked" in tags: - tags.remove("checked") - self.checked_items.discard(item) - - self.tree.item(item, tags=tuple(tags)) - - if "folder" in self.tree.item(item, "tags"): - for child in self.tree.get_children(item): - self.toggle_check(child, force_check=new_check_state) - - def update_scan_progress(self): - """Update scan progress""" - if self.total_urls > 0: - progress = self.scanned_urls / self.total_urls - self.progress_bar.set(progress) - self.progress_label.configure( - text=f"Scanning... ({self.scanned_urls}/{self.total_urls}) {progress:.1%}" - ) - self.window.update_idletasks() - - def _run_on_ui_thread(self, callback, *args, wait=False): - """Run a callback on Tk main thread.""" - if threading.current_thread() is threading.main_thread(): - callback(*args) - return - - done = threading.Event() - error_box = {} - - def wrapped(): - try: - callback(*args) - except Exception as ex: - error_box["error"] = ex - finally: - done.set() - - self.window.after(0, wrapped) - if wait: - done.wait() - if "error" in error_box: - raise error_box["error"] - - def on_scan_started(self, url): - """Backend hook: initialize UI state before scan starts.""" - self._run_on_ui_thread(self._on_scan_started_ui, url, wait=True) - - def _on_scan_started_ui(self, _url): - self.is_scanning = True - self.scan_pause_btn.configure(state="normal", text="Pause Scan") - self.progress_label.configure(text="Scanning website...") - self.scan_btn.configure(state="normal", text="Stop Scan") - self._set_status("Scanning", "scanning") - - self._cancel_scan_item_flush() - while not self.scan_item_buffer.empty(): - self.scan_item_buffer.get() - - while not self.dir_queue.empty(): - self.dir_queue.get() - while not self.file_queue.empty(): - self.file_queue.get() - self.is_processing_dirs = False - self.is_processing_files = False - - with self.files_dict_lock: - self.files_dict.clear() - with self.folders_dict_lock: - self.folders.clear() - - self.tree.delete(*self.tree.get_children()) - self.checked_items.clear() - self.file_types.clear() - self.file_type_counts.clear() - for widget in self.filter_checkboxes_frame.winfo_children(): - widget.destroy() - - self.total_urls = 0 - self.scanned_urls = 0 - self.progress_bar.set(0) - self.search_entry.configure(state="disabled") - self.search_var.set("") - - def on_scan_progress(self, scanned_urls, total_urls): - """Backend hook: update scan counters/progress.""" - self._run_on_ui_thread(self._on_scan_progress_ui, scanned_urls, total_urls) - - def _on_scan_progress_ui(self, scanned_urls, total_urls): - self.scanned_urls = scanned_urls - self.total_urls = total_urls - self.update_scan_progress() - - def on_scan_item( - self, - is_directory, - path, - url, - file_name=None, - size=None, - file_type=None, - full_path=None, - ): - """Backend hook: enqueue a discovered directory/file for UI processing.""" - self._run_on_ui_thread( - self._on_scan_item_ui, - is_directory, - path, - url, - file_name, - size, - file_type, - full_path, - ) - - def _on_scan_item_ui( - self, - is_directory, - path, - url, - file_name, - size, - file_type, - full_path, - ): - if not self.is_scanning: - return - self.scan_item_buffer.put( - (is_directory, path, url, file_name, size, file_type, full_path) - ) - self._schedule_scan_item_flush() - - def _schedule_scan_item_flush(self): - if self.scan_flush_job is not None: - return - if not self.is_scanning: - return - self.scan_flush_job = self.window.after( - self.scan_flush_interval_ms, self._flush_scan_item_buffer - ) - - def _cancel_scan_item_flush(self): - if self.scan_flush_job is None: - return - try: - self.window.after_cancel(self.scan_flush_job) - except tk.TclError: - pass - finally: - self.scan_flush_job = None - - def _flush_scan_item_buffer(self, max_items=None, reschedule=True): - self.scan_flush_job = None - if max_items is None: - max_items = self.scan_flush_batch_size - - processed = 0 - added_dir = False - added_file = False - - while processed < max_items: - try: - ( - is_directory, - path, - url, - file_name, - size, - file_type, - full_path, - ) = self.scan_item_buffer.get_nowait() - except Empty: - break - - if is_directory: - with self.folders_dict_lock: - self.dir_queue.put((path, url)) - added_dir = True - else: - self.file_queue.put((path, url, file_name, size, file_type, full_path)) - added_file = True - processed += 1 - - if added_dir and not self.is_processing_dirs: - self.window.after(0, self.process_dir_queue) - if added_file and not self.is_processing_files: - self.window.after(0, self.process_file_queue) - - if reschedule and self.is_scanning and not self.scan_item_buffer.empty(): - self._schedule_scan_item_flush() - - def on_scan_finished(self, stopped): - """Backend hook: finalize UI state after scan ends.""" - self._run_on_ui_thread(self._on_scan_finished_ui, stopped) - - def _on_scan_finished_ui(self, stopped): - self.is_scanning = False - self._cancel_scan_item_flush() - while not self.scan_item_buffer.empty(): - self._flush_scan_item_buffer( - max_items=self.scan_flush_batch_size, - reschedule=False, - ) - - self.scan_btn.configure(state="normal", text="Scan") - self.scan_pause_btn.configure(state="disabled", text="Pause Scan") - self.scan_pause_event.set() - if not stopped: - self.progress_label.configure(text="Scan finished.") - self.log_message("[Scan] Finished.") - else: - self.progress_label.configure(text="Scan stopped.") - self.log_message("[Scan] Stopped by user.") - self.scan_completed() - - def on_downloads_finished(self, completed, total): - """Backend hook: finalize download summary on main thread.""" - self._run_on_ui_thread(self.downloads_completed, completed, total) - - def process_dir_queue(self): - """Processes the directory creation queue to avoid UI blocking.""" - try: - if not hasattr(self, "dir_queue"): - return - self.is_processing_dirs = True - if not self.dir_queue.empty(): - dir_path, url = self.dir_queue.get() - self.create_folder_structure(dir_path, url) - self.window.after(10, self.process_dir_queue) - else: - self.is_processing_dirs = False - except tk.TclError as ex: - self.is_processing_dirs = False - print(f"Error processing directory queue (UI): {str(ex)}") - - def process_file_queue(self): - """Processes the file queue to avoid UI blocking.""" - try: - if not hasattr(self, "file_queue"): - return - self.is_processing_files = True - if not self.file_queue.empty(): - ( - dir_path, - url, - file_name, - size, - file_type, - full_path, - ) = self.file_queue.get() - self.update_file_types(file_name) - self._safe_create_folder_and_add_file( - dir_path, url, file_name, size, file_type, full_path - ) - self.window.after(5, self.process_file_queue) - else: - self.is_processing_files = False - except tk.TclError as ex: - self.is_processing_files = False - print(f"Error processing file queue (UI): {str(ex)}") - - def _safe_create_folder_and_add_file( - self, dir_path, url, file_name, size, file_type, full_path - ): - """Create folder structure and add file in main thread""" - try: - with self.files_dict_lock: - if ( - full_path in self.files_dict - and self.files_dict[full_path] is not None - ): - return - - parent_id = self.create_folder_structure(dir_path, url) - - for child in self.tree.get_children(parent_id): - if "file" in self.tree.item(child)["tags"]: - child_text = self.tree.item(child)["text"] - child_name = child_text.replace(f"{self.file_icon} ", "") - if child_name == file_name: - with self.files_dict_lock: - if full_path in self.files_dict: - del self.files_dict[full_path] - return - - self._add_file_to_tree( - parent_id, file_name, size, file_type, full_path, url - ) - except (tk.TclError, OSError) as ex: - with self.files_dict_lock: - if full_path in self.files_dict: - del self.files_dict[full_path] - print(f"Error adding file to tree structure: {str(ex)}") - - def _add_file_to_tree(self, parent_id, file_name, size, file_type, full_path, url): - """Add file to tree structure in main thread""" - try: - items_count = len(self.tree.get_children(parent_id)) - row_tag = "evenrow" if items_count % 2 == 0 else "oddrow" - - self.tree.insert( - parent_id, - "end", - text=f"{self.file_icon} {file_name}", - values=(size, file_type), - tags=("file", row_tag), - ) - with self.files_dict_lock: - self.files_dict[full_path] = url - except tk.TclError as ex: - print(f"Error in _add_file_to_tree (TclError): {ex}") - except RuntimeError as ex: - print(f"Error in _add_file_to_tree (General): {ex}") - - def update_file_types(self, file_name): - """Updates the file type filter UI.""" - ext = normalize_extension(file_name) - - if ext not in self.file_types: - self.file_types[ext] = ctk.BooleanVar(value=False) - self.file_type_counts[ext] = 0 - # Re-draw checkboxes - self.redraw_file_type_filters() - self.file_type_counts[ext] += 1 - # Update count on the checkbox - for child in self.filter_checkboxes_frame.winfo_children(): - if child.cget("text").startswith(ext): - # Use dict expansion to avoid type-checker complaints about unknown kwargs - child.configure(**{"text": f"{ext} ({self.file_type_counts[ext]})"}) - break - - def redraw_file_type_filters(self): - """Clears and redraws the file type filter checkboxes.""" - for widget in self.filter_checkboxes_frame.winfo_children(): - widget.destroy() - - sorted_types = sorted(self.file_types.keys()) - for i, file_type in enumerate(sorted_types): - var = self.file_types[file_type] - count = self.file_type_counts.get(file_type, 0) - chk = ctk.CTkCheckBox( - self.filter_checkboxes_frame, - text=f"{file_type} ({count})", - variable=var, - command=self.apply_filters, - **self.ui_tokens["checkbox"], - ) - chk.grid(row=i // 4, column=i % 4, padx=5, pady=2, sticky="w") - - def apply_filters(self): - """Applies selected file type filters by checking/unchecking items in the tree.""" - all_items = self.get_all_tree_items() - items_to_expand = set() - - for item in all_items: - tags = self.tree.item(item, "tags") - if "file" in tags: - text = self.tree.item(item, "text") - file_name = text.replace(f"{self.file_icon} ", "") - - ext = normalize_extension(file_name) - - if ext in self.file_types: - should_be_checked = self.file_types[ext].get() - self.toggle_check(item, force_check=should_be_checked) - if should_be_checked: - # If a file is checked, we need to expand its ancestors - parent = self.tree.parent(item) - while parent: - items_to_expand.add(parent) - parent = self.tree.parent(parent) - - for item_id in items_to_expand: - self.tree.item(item_id, open=True) - - def get_all_tree_items(self, parent=""): - """Recursively gets all item IDs from the treeview.""" - items = [] - for child in self.tree.get_children(parent): - items.append(child) - items.extend(self.get_all_tree_items(child)) - return items - - def select_all_types(self): - """Selects all file type checkboxes.""" - for var in self.file_types.values(): - var.set(True) - self.apply_filters() - - def deselect_all_types(self): - """Deselects all file type checkboxes.""" - for var in self.file_types.values(): - var.set(False) - self.apply_filters() - - def clear_scan_results(self): - """Clears the treeview and all scan-related data.""" - if self.is_scanning: - messagebox.showwarning( - "Warning", "Cannot clear results while a scan is in progress." - ) - return - - # Clear Treeview - for item in self.tree.get_children(): - self.tree.delete(item) - - # Clear data structures - with self.files_dict_lock: - self.files_dict.clear() - with self.folders_dict_lock: - self.folders.clear() - - self.checked_items.clear() - self.file_types.clear() - self.file_type_counts.clear() - - # Clear file type filter UI - for widget in self.filter_checkboxes_frame.winfo_children(): - widget.destroy() - - # Reset scan progress - self.total_urls = 0 - self.scanned_urls = 0 - self.progress_bar.set(0) - self.progress_label.configure(text="Scan results cleared.") - self.search_var.set("") - self.search_entry.configure(state="disabled") - self.full_tree_backup.clear() - - def on_search_filter_changed(self, *_): - """Callback when the search entry text changes.""" - self.apply_search_filter() - - def apply_search_filter(self): - """Filters the treeview based on the search query.""" - query = self.search_var.get().lower() - - # Detach all items first - for item_id in self.full_tree_backup: - if self.tree.exists(item_id): - self.tree.detach(item_id) - - if not query: - # If query is empty, reattach all items in their original order - for item_id, data in self.full_tree_backup.items(): - if self.tree.exists(item_id): - self.tree.move(item_id, data["parent"], data["index"]) - return - - items_to_show = set() - # Find items that match the query - for item_id, data in self.full_tree_backup.items(): - if query in data["text"].lower(): - items_to_show.add(item_id) - # Add all parents to ensure the path is visible - parent = data["parent"] - while parent: - items_to_show.add(parent) - parent = self.full_tree_backup[parent]["parent"] - - # Re-attach only the items that should be visible - for item_id, data in self.full_tree_backup.items(): - if item_id in items_to_show: - if self.tree.exists(item_id): - self.tree.move(item_id, data["parent"], data["index"]) - if "folder" in data["tags"]: - self.tree.item(item_id, open=True) - - def update_thread_count(self, new_count_str: str): - """Updates the number of concurrent download threads.""" - try: - new_count = int(new_count_str) - if 1 <= new_count <= 10 and new_count != self.max_workers: - self.max_workers = new_count - self.executor = rebuild_executor(self.executor, self.max_workers) - except (ValueError, TypeError): - pass - - def update_progress(self, file_path, file_name, progress): - """Updates per-file or global progress from the main thread.""" - self.window.after( - 0, lambda: self._update_progress_ui(file_path, file_name, progress) - ) - - def _update_progress_ui(self, file_path, file_name, progress): - """Update per-file progress if present; fallback to global bar.""" - item = self.downloads_panel._items.get(file_path) - if item: - self.downloads_panel.set_progress(file_path, progress) - else: - self.progress_bar.set(progress / 100) - self.progress_label.configure( - text=f"Downloading: {file_name} ({progress:.1f}%)" - ) - - def update_download_status(self, file_path, status_text): - """Update status label for a specific download item.""" - item = self.downloads_panel._items.get(file_path) - if item: - self.downloads_panel.set_status(file_path, status_text) - - def log_message(self, message: str): - """Append a message to the log panel.""" - try: - self.log_text.configure(state="normal") - self.log_text.insert("end", message + "\n") - self.log_text.see("end") - self.log_text.configure(state="disabled") - except tk.TclError: - pass - - def download_selected(self): - """Download checked files""" - if not self.checked_items: - messagebox.showwarning("Warning", "Please select files to download") - return - - files_to_download = [ - item - for item in self.checked_items - if "file" in self.tree.item(item)["tags"] - ] - - if not files_to_download: - messagebox.showwarning("Warning", "No files selected to download") - return - - if not self.download_path: - self.choose_download_path() - if not self.download_path: # User cancelled - return - - self.pause_event.set() - self.pause_btn.configure(state="normal") - self.progress_label.configure(text="Preparing to download...") - - futures = [] - for item in files_to_download: - text = self.tree.item(item)["text"] - file_name = text.replace(f"{self.file_icon} ", "") - - path_parts = [] - sanitized_path_parts = [] - current = item - while current: - parent = self.tree.parent(current) - if not parent: - break - parent_text = self.tree.item(parent)["text"] - folder_name = parent_text.replace(f"{self.folder_icon} ", "") - path_parts.append(folder_name) - sanitized_path_parts.append(sanitize_path_segment(folder_name)) - current = parent - - path_parts.reverse() - sanitized_path_parts.reverse() - relative_path = os.path.join(*path_parts) if path_parts else "" - - full_path_key = os.path.join(relative_path, file_name).replace("\\", "/") - if full_path_key.startswith("/"): - full_path_key = full_path_key[1:] - - safe_file_name = sanitize_filename(file_name) - - try: - target_dir = safe_join(self.download_path, sanitized_path_parts) - file_path = safe_join(self.download_path, sanitized_path_parts + [safe_file_name]) - except ValueError: - self.log_message(f"[Download] Skipped unsafe path: {full_path_key}") - continue - - os.makedirs(target_dir, exist_ok=True) - - url = self.files_dict.get(full_path_key) - - if url: - cancel_event = self.downloads_panel.ensure(file_path, safe_file_name) - # Submit with cancel support and per-file progress - future = self.executor.submit( - self.backend.download_file, - url, - file_path, - safe_file_name, - cancel_event, - ) - futures.append(future) - - threading.Thread(target=self.backend.monitor_downloads, args=(futures,)).start() - - def downloads_completed(self, completed, total): - """Finalizes the download process and shows a summary.""" - self.progress_label.configure( - text=f"Download completed: {completed}/{total} files." - ) - self.pause_btn.configure(state="disabled") - messagebox.showinfo( - "Completed", - f"{completed} of {total} selected files downloaded successfully.", - ) - - def start_scan(self): - """Starts the website scanning process.""" - url = self.url_entry.get() - if not url: - messagebox.showerror("Error", "Please enter URL") - return - - if self.is_scanning: - self.backend.should_stop = True - self.scan_btn.configure(text="Stopping...") - self.scan_btn.configure(state="disabled") - self._set_status("Stopping", "stopping") - return - - self.backend.should_stop = False - try: - self.download_path = default_download_folder(url, os.getcwd()) - except (OSError, TypeError) as ex: - print(f"Error setting download path: {str(ex)}") - self.download_path = os.path.join(os.getcwd(), "downloads") - - self.progress_label.configure(text="Preparing to scan...") - self._set_status("Scanning", "scanning") - self.scan_btn.configure(text="Stop Scan") - self.search_entry.configure(state="disabled") - self.search_var.set("") - Thread(target=self.backend.scan_website, args=(url,)).start() - - def scan_completed(self): - """Called by the backend when scanning is finished.""" - self.search_entry.configure(state="normal") - self.full_tree_backup.clear() - for item in self.get_all_tree_items(): - self.full_tree_backup[item] = { - "parent": self.tree.parent(item), - "index": self.tree.index(item), - "text": self.tree.item(item, "text"), - "tags": self.tree.item(item, "tags"), - } - self._set_status("Ready", "ready") - - def focus_search(self, _event=None): - if self.search_entry.cget("state") != "disabled": - self.search_entry.focus_set() - self.search_entry.icursor("end") - - def focus_logs(self, _event=None): - try: - if not self.panels_visible: - self.panels_tabview.grid() - self.panels_visible = True - self.toggle_panels_btn.configure(text="Hide Panels") - self.panels_tabview.set("Logs") - self.log_text.focus_set() - except (AttributeError, tk.TclError): - pass - - def clear_search(self, _event=None): - if self.search_entry.cget("state") != "disabled": - self.search_var.set("") - - def run(self): - """Starts the Tkinter main loop.""" - self.window.mainloop() - - def show_context_menu(self, event): - """Show right-click menu""" - self.context_menu.post(event.x_root, event.y_root) - - def select_all(self): - """Select all items""" - for item in self.get_all_tree_items(): - self.toggle_check(item, force_check=True) - - def deselect_all(self): - """Deselect all items""" - for item in self.get_all_tree_items(): - self.toggle_check(item, force_check=False) - - def expand_all(self, parent=""): - """Expand all nodes in the treeview.""" - for item in self.tree.get_children(parent): - self.tree.item(item, open=True) - self.expand_all(item) - - def collapse_all(self, parent=""): - """Collapse all nodes in the treeview.""" - for item in self.tree.get_children(parent): - self.tree.item(item, open=False) - self.collapse_all(item) - - def on_closing(self): - """Handles the window closing event.""" - # Persist settings on close (best-effort) - data = { - "panels_visible": bool(getattr(self, "panels_visible", False)), - "active_tab": str(self.panels_tabview.get()), - } - settings_path = getattr( - self, - "_settings_path", - os.path.join(os.path.expanduser("~"), ".index_ripper_settings.json"), - ) - save_settings(settings_path, data) - if messagebox.askokcancel("Quit", "Do you want to quit?"): - self.backend.should_stop = True - self.executor.shutdown(wait=False, cancel_futures=True) - self.window.destroy() - - def sort_tree(self, col): - """Sorts the treeview columns.""" - items = [(self.tree.set(i, col), i) for i in self.tree.get_children("")] - - # A simple string sort - items.sort(reverse=self.sort_reverse) - - for index, (_, i) in enumerate(items): - self.tree.move(i, "", index) - - self.sort_reverse = not self.sort_reverse - - -if __name__ == "__main__": - if "--ui-smoke" in sys.argv: - from ui_ctk import WebsiteCopierCtk - app = WebsiteCopierCtk(ui_smoke=True) - app.window.after(0, app.window.destroy) - app.run() - print("UI_SMOKE_OK") - raise SystemExit(0) - - from ui_ctk import WebsiteCopierCtk - app = WebsiteCopierCtk() - app.run() diff --git a/index_ripper_ttkb.py b/index_ripper_ttkb.py deleted file mode 100644 index bc37b57..0000000 --- a/index_ripper_ttkb.py +++ /dev/null @@ -1,42 +0,0 @@ -from __future__ import annotations - -import os -import sys - - -if "--smoke" in sys.argv: - print("SMOKE_OK") - raise SystemExit(0) - -if "--self-test" in sys.argv: - from self_test import run_self_test - - result = run_self_test() - print( - f"SELF_TEST_OK total={result.total} files={result.files} dirs={result.directories}" - ) - raise SystemExit(0) - - -from app_utils import configure_tk_libraries -configure_tk_libraries() - - -def main() -> int: - if "--ui-smoke" in sys.argv: - from ui_ttkb import WebsiteCopierTtkb - - app = WebsiteCopierTtkb(ui_smoke=True) - app.run() - print("UI_SMOKE_OK") - return 0 - - from ui_ttkb import WebsiteCopierTtkb - - app = WebsiteCopierTtkb() - app.run() - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/macos.spec b/macos.spec index 1404f3b..4c4fed7 100644 --- a/macos.spec +++ b/macos.spec @@ -4,11 +4,11 @@ from PyInstaller.utils.hooks import collect_data_files block_cipher = None a = Analysis( - ["index_ripper.py"], - pathex=[], + ["src/index_ripper/__main__.py"], + pathex=["src"], binaries=[], datas=[], - hiddenimports=["tkinter", "tkinter.ttk"], + hiddenimports=["tkinter", "tkinter.ttk", "index_ripper"], hookspath=[], hooksconfig={}, runtime_hooks=[], diff --git a/pyproject.toml b/pyproject.toml index cde480f..4692e50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [project] name = "index-ripper" version = "0.1.0" @@ -7,15 +11,17 @@ dependencies = [ "beautifulsoup4>=4.12.0,<5.0.0", "urllib3>=2.0.0,<3.0.0", "customtkinter>=5.2.0,<6.0.0", - "ttkbootstrap>=1.10.1,<2.0.0", ] [project.optional-dependencies] dev = ["pytest>=8.0.0"] [project.scripts] -index-ripper = "ui_ctk:main" +index-ripper = "index_ripper.app:main" + +[tool.hatch.build.targets.wheel] +packages = ["src/index_ripper"] [tool.pytest.ini_options] -testpaths = ["."] +testpaths = ["tests"] python_files = ["test_*.py"] diff --git a/pyside_poc.py b/pyside_poc.py deleted file mode 100644 index 376028f..0000000 --- a/pyside_poc.py +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import argparse -import importlib -import os -import sys -import time - - -def _parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser( - prog="pyside_poc.py", - description="Minimal PySide6 POC UI (Tree / Logs / Downloads / Status).", - ) - parser.add_argument( - "--qt-platform", - default=None, - help=( - "Optional QT_QPA_PLATFORM override (e.g. offscreen, minimal). " - "Applied before importing PySide6." - ), - ) - parser.add_argument( - "--quit-after", - type=float, - default=None, - help="Auto-quit after N seconds (useful for manual measurements).", - ) - parser.add_argument( - "--print-pid", - action="store_true", - help="Print process PID on startup.", - ) - parser.add_argument( - "--measure-startup", - action="store_true", - help="Print rough startup timing checkpoints to stdout.", - ) - return parser.parse_args(argv) - - -def _print_missing_pyside6_instructions() -> None: - msg = """PySide6 is not installed. - -Install: - python3 -m pip install PySide6 - # or: python -m pip install PySide6 - -Run: - python3 pyside_poc.py - # or: python pyside_poc.py - -Notes: - - This is a discardable POC and exits with code 0 when PySide6 is missing. - - For headless experiments you may try: python3 pyside_poc.py --qt-platform offscreen -""" - sys.stdout.write(msg) - - -def main(argv: list[str] | None = None) -> int: - argv = list(sys.argv[1:] if argv is None else argv) - args = _parse_args(argv) - - if args.qt_platform: - os.environ.setdefault("QT_QPA_PLATFORM", str(args.qt_platform)) - - t0 = time.perf_counter() - try: - QtCore = importlib.import_module("PySide6.QtCore") - QtWidgets = importlib.import_module("PySide6.QtWidgets") - - Qt = QtCore.Qt - QTimer = QtCore.QTimer - - QApplication = QtWidgets.QApplication - QFrame = QtWidgets.QFrame - QGroupBox = QtWidgets.QGroupBox - QHBoxLayout = QtWidgets.QHBoxLayout - QLabel = QtWidgets.QLabel - QListWidget = QtWidgets.QListWidget - QMainWindow = QtWidgets.QMainWindow - QPlainTextEdit = QtWidgets.QPlainTextEdit - QProgressBar = QtWidgets.QProgressBar - QSplitter = QtWidgets.QSplitter - QTreeWidget = QtWidgets.QTreeWidget - QTreeWidgetItem = QtWidgets.QTreeWidgetItem - QVBoxLayout = QtWidgets.QVBoxLayout - QWidget = QtWidgets.QWidget - except Exception: - _print_missing_pyside6_instructions() - return 0 - - t_import = time.perf_counter() - - app = QApplication(["pyside_poc"]) - app.setApplicationName("Index-Ripper PySide6 POC") - - if args.print_pid: - sys.stdout.write(f"PID: {os.getpid()}\n") - sys.stdout.flush() - - window = QMainWindow() - window.setWindowTitle("Index-Ripper - PySide6 POC") - window.resize(1080, 720) - - central = QWidget() - root_layout = QVBoxLayout(central) - root_layout.setContentsMargins(12, 12, 12, 12) - root_layout.setSpacing(10) - - def boxed(title: str, child): - box = QGroupBox(title) - layout = QVBoxLayout(box) - layout.setContentsMargins(10, 10, 10, 10) - layout.addWidget(child) - return box - - tree = QTreeWidget() - tree.setHeaderHidden(True) - root_item = QTreeWidgetItem(["index-ripper"]) - sources_item = QTreeWidgetItem(["sources"]) - sources_item.addChild(QTreeWidgetItem(["example.torrent"])) - root_item.addChild(sources_item) - root_item.addChild(QTreeWidgetItem(["outputs"])) - tree.addTopLevelItem(root_item) - tree.expandAll() - tree_box = boxed("Tree", tree) - - logs = QPlainTextEdit() - logs.setReadOnly(True) - logs.setPlaceholderText("Logs will appear here.") - logs.appendPlainText("[info] PySide6 POC started") - logs.appendPlainText("[info] This UI is intentionally minimal") - logs_box = boxed("Logs", logs) - - downloads = QListWidget() - downloads.addItem("example_01.zip (queued)") - downloads.addItem("example_02.zip (waiting)") - downloads_box = boxed("Downloads", downloads) - - right_split = QSplitter() - right_split.setOrientation(Qt.Orientation.Vertical) - right_split.addWidget(logs_box) - right_split.addWidget(downloads_box) - right_split.setStretchFactor(0, 3) - right_split.setStretchFactor(1, 2) - - main_split = QSplitter() - main_split.addWidget(tree_box) - main_split.addWidget(right_split) - main_split.setStretchFactor(0, 2) - main_split.setStretchFactor(1, 5) - - status_frame = QFrame() - status_frame.setFrameShape(QFrame.StyledPanel) - status_layout = QHBoxLayout(status_frame) - status_layout.setContentsMargins(10, 8, 10, 8) - status_layout.setSpacing(10) - status_label = QLabel("Status: idle") - status_progress = QProgressBar() - status_progress.setRange(0, 100) - status_progress.setValue(0) - status_progress.setTextVisible(False) - status_layout.addWidget(status_label, 1) - status_layout.addWidget(status_progress, 2) - status_box = boxed("Status", status_frame) - - root_layout.addWidget(main_split, 1) - root_layout.addWidget(status_box, 0) - window.setCentralWidget(central) - - t_ui = time.perf_counter() - - if args.quit_after is not None and args.quit_after >= 0: - QTimer.singleShot(int(args.quit_after * 1000), app.quit) - - window.show() - t_show = time.perf_counter() - - if args.measure_startup: - sys.stdout.write( - "startup_seconds " - f"import={t_import - t0:.4f} " - f"ui={t_ui - t_import:.4f} " - f"show={t_show - t0:.4f}\n" - ) - sys.stdout.flush() - - return int(app.exec()) - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/index_ripper/__init__.py b/src/index_ripper/__init__.py new file mode 100644 index 0000000..3b6fb18 --- /dev/null +++ b/src/index_ripper/__init__.py @@ -0,0 +1,3 @@ +"""Index Ripper — download files from directory listing pages.""" + +__version__ = "0.1.0" diff --git a/src/index_ripper/__main__.py b/src/index_ripper/__main__.py new file mode 100644 index 0000000..3ebe16f --- /dev/null +++ b/src/index_ripper/__main__.py @@ -0,0 +1,36 @@ +"""Entry point for `python -m index_ripper`.""" +import sys + + +def _main() -> None: + if "--smoke" in sys.argv: + print("SMOKE_OK") + raise SystemExit(0) + + if "--self-test" in sys.argv: + from index_ripper.self_test import run_self_test + + result = run_self_test() + print( + f"SELF_TEST_OK total={result.total} files={result.files} dirs={result.directories}" + ) + raise SystemExit(0) + + from index_ripper.utils import configure_tk_libraries + configure_tk_libraries() + + if "--ui-smoke" in sys.argv: + from index_ripper.app import WebsiteCopierCtk + + app = WebsiteCopierCtk(ui_smoke=True) + app.window.after(0, app.window.destroy) + app.run() + print("UI_SMOKE_OK") + raise SystemExit(0) + + from index_ripper.app import main + main() + + +if __name__ == "__main__": + _main() diff --git a/ui_ctk.py b/src/index_ripper/app.py similarity index 83% rename from ui_ctk.py rename to src/index_ripper/app.py index 34f1b53..17fad68 100644 --- a/ui_ctk.py +++ b/src/index_ripper/app.py @@ -1,3 +1,4 @@ +"""Main application window.""" from __future__ import annotations import copy @@ -5,7 +6,6 @@ import threading import time from concurrent.futures import ThreadPoolExecutor -from dataclasses import dataclass, field from queue import Empty, Queue import tkinter as tk @@ -17,174 +17,27 @@ import customtkinter as ctk -from app_utils import build_download_path, default_download_folder, normalize_extension, rebuild_executor, safe_join, sanitize_filename -from backend import Backend -from ui_downloads import DownloadsPanel -from ui_theme import ( +from index_ripper.utils import ( + build_download_path, + default_download_folder, + normalize_extension, + rebuild_executor, + safe_join, + sanitize_filename, +) +from index_ripper.backend import Backend +from index_ripper.settings import load_settings, save_settings +from index_ripper.ui.downloads import DownloadsPanel +from index_ripper.ui.theme import ( apply_app_theme, configure_action_button_styles, ui_tokens, ) +from index_ripper.ui.filetree import TreeNode, RowWidget, should_skip_file_row +from index_ripper.ui.filters import FileTypeFilterMixin -@dataclass -class TreeNode: - node_id: str - parent_id: str # "" for root-level nodes - name: str - kind: str # "folder" | "file" - full_path: str # "" for folders - size: str - file_type: str - icon_group: str # "folder"|"image"|"document"|"archive"|"code"|"audio"|"video"|"text"|"binary" - checked: bool = False - expanded: bool = False - hidden: bool = False # True when filtered out by search - children: list[str] = field(default_factory=list) # ordered list of child node_ids - - -_EMOJI_ICONS = { - "folder": "📁", - "image": "🖼️", - "document": "📄", - "archive": "🗜️", - "code": "💻", - "audio": "🎵", - "video": "🎬", - "text": "📝", - "binary": "⚙️", -} - -_BG_NORMAL = "transparent" -_BG_HOVER = ("#F1F5F9", "#1E293B") -_BG_CHECKED = ("#EFF6FF", "#172554") -_BG_CHECKED_HOVER = ("#DBEAFE", "#1E3A5F") - - -class RowWidget: - """One visible row in the FileTree.""" - - INDENT_PX = 20 - ROW_HEIGHT = 36 - - def __init__(self, parent, app, node: TreeNode, depth: int): - self.app = app - self.node_id = node.node_id - self._checked = node.checked - self._hovered = False - - self.frame = ctk.CTkFrame(parent, height=self.ROW_HEIGHT, corner_radius=4) - self.frame.pack(fill="x", padx=4, pady=0) - self.frame.pack_propagate(False) - - # 3-px accent bar (left edge, blue when checked) - self._accent = ctk.CTkFrame( - self.frame, width=3, corner_radius=0, - fg_color="#2563EB" if node.checked else "transparent", - ) - self._accent.pack(side="left", fill="y") - - # Indent spacer - if depth > 0: - ctk.CTkFrame( - self.frame, width=depth * self.INDENT_PX, - fg_color="transparent", height=self.ROW_HEIGHT, - ).pack(side="left") - - # Emoji icon - ctk.CTkLabel( - self.frame, - text=_EMOJI_ICONS.get(node.icon_group, "📄"), - font=ctk.CTkFont(size=18), - width=28, - ).pack(side="left", padx=(4, 4)) - - # Name label - self.name_label = ctk.CTkLabel( - self.frame, - text=node.name, - anchor="w", - font=ctk.CTkFont(size=14, weight="bold" if node.kind == "folder" else "normal"), - ) - self.name_label.pack(side="left", fill="x", expand=True) - - # Chevron on RIGHT (folders only) - if node.kind == "folder": - self.chevron = ctk.CTkButton( - self.frame, text="▼" if node.expanded else "▶", - width=22, height=22, fg_color="transparent", - hover_color=("gray85", "gray30"), text_color=("gray40", "gray60"), - font=ctk.CTkFont(size=10), - command=lambda: app._on_chevron_click(self.node_id), - ) - self.chevron.pack(side="right", padx=(4, 4)) - elif node.kind == "file" and node.size: - # Size label (right side, files only) - ctk.CTkLabel( - self.frame, - text=node.size, - font=ctk.CTkFont(size=12), - text_color=("gray50", "gray60"), - width=80, - anchor="e", - ).pack(side="right", padx=(0, 8)) - - # Bind hover only on outer frame (avoids flicker from child entry) - self.frame.bind("", self._on_enter) - self.frame.bind("", self._on_leave) - # Bind click on frame and all non-chevron children - self._bind_clicks(self.frame) - - def _bind_clicks(self, widget) -> None: - """Bind click handler on widget and all children except the chevron.""" - if hasattr(self, "chevron") and widget is self.chevron: - return # chevron has its own command; don't intercept clicks - widget.bind("", self._on_click) - for child in widget.winfo_children(): - self._bind_clicks(child) - - def _on_enter(self, _event=None) -> None: - self._hovered = True - self._update_bg() - - def _on_leave(self, _event=None) -> None: - self._hovered = False - self._update_bg() - - def _on_click(self, event) -> None: - self.app._on_row_click(self.node_id, event) - - def set_checked(self, checked: bool) -> None: - self._checked = checked - self._accent.configure( - fg_color="#2563EB" if checked else "transparent" - ) - self._update_bg() - - def set_chevron(self, expanded: bool) -> None: - if hasattr(self, "chevron"): - self.chevron.configure(text="▼" if expanded else "▶") - - def _update_bg(self) -> None: - if self._checked and self._hovered: - color = _BG_CHECKED_HOVER - elif self._checked: - color = _BG_CHECKED - elif self._hovered: - color = _BG_HOVER - else: - color = _BG_NORMAL - self.frame.configure(fg_color=color) - - def destroy(self) -> None: - self.frame.destroy() - - -def should_skip_file_row(existing_entry) -> bool: - """Return True when a file row is already fully registered.""" - return existing_entry is not None - -class WebsiteCopierCtk: +class WebsiteCopierCtk(FileTypeFilterMixin): USER_AGENT = "IndexRipper/2.0" def __init__(self, ui_smoke: bool = False): @@ -273,11 +126,8 @@ def __init__(self, ui_smoke: bool = False): # so native clipboard shortcuts work in every CTkEntry / CTkTextbox. for seq, virt in ( ("", "<>"), - ("", "<>"), ("", "<>"), - ("", "<>"), ("", "<>"), - ("", "<>"), ("", "<>"), ("", "<>"), ): @@ -362,7 +212,7 @@ def _build_toolbar(self) -> None: ) self.url_entry.grid(row=0, column=0, sticky="ew", padx=(0, 8)) - # Scan button (toggles "Scan" ↔ "Stop Scan") + # Scan button (toggles "Scan" / "Stop Scan") self.scan_btn = ctk.CTkButton(toolbar, text="Scan", command=self.start_scan, width=90) self.scan_btn.grid(row=0, column=1, padx=(0, 4)) @@ -552,8 +402,6 @@ def _poll_file_queue(self) -> None: except tk.TclError: self.is_processing_files = False - # --- Stub methods (implemented in later Tasks) --- - def _build_filters_row(self) -> None: # ── Row 1: action buttons ────────────────────────────────────────── self._ctrl_row_frame = ctk.CTkFrame(self.window, fg_color="transparent") @@ -580,8 +428,8 @@ def _build_filters_row(self) -> None: type_btns = ctk.CTkFrame(types_row, fg_color="transparent") type_btns.grid(row=0, column=1, sticky="w", padx=(0, 8)) - ctk.CTkButton(type_btns, text="✓ All", command=self.select_all_types, width=68, **_s).pack(side="left", padx=(0, 4)) - ctk.CTkButton(type_btns, text="✗ All", command=self.deselect_all_types, width=68, **_s).pack(side="left") + ctk.CTkButton(type_btns, text="\u2713 All", command=self.select_all_types, width=68, **_s).pack(side="left", padx=(0, 4)) + ctk.CTkButton(type_btns, text="\u2717 All", command=self.deselect_all_types, width=68, **_s).pack(side="left") self.filters_container = ctk.CTkScrollableFrame( types_row, @@ -657,7 +505,7 @@ def _sync_rows(self) -> None: prev = self._last_visible # Append-only optimisation: if the existing rows are still a prefix of # the new list (no reordering), just pack the new rows at the end. - # This prevents the pack_forget→repack flash during scanning. + # This prevents the pack_forget->repack flash during scanning. if ( len(self._visible_nodes) >= len(prev) and self._visible_nodes[: len(prev)] == prev @@ -686,7 +534,7 @@ def _sync_rows(self) -> None: self._last_visible = list(self._visible_nodes) def _schedule_tree_update(self) -> None: - """Debounce _rebuild_visible + _sync_rows to avoid O(n²) during bulk add.""" + """Debounce _rebuild_visible + _sync_rows to avoid O(n^2) during bulk add.""" if not self._tree_update_pending: self._tree_update_pending = True self.window.after(50, self._do_tree_update) @@ -748,13 +596,13 @@ def _build_progress_section(self) -> None: def _build_panels(self) -> None: self.panels_notebook = ctk.CTkTabview(self.window, height=180) self.panels_notebook.grid(row=5, column=0, sticky="ew", padx=10, pady=(4, 10)) - self._panels_widget = self.panels_notebook # toggle_panels 用 + self._panels_widget = self.panels_notebook # toggle_panels uses this downloads_tab = self.panels_notebook.add("Downloads") logs_tab = self.panels_notebook.add("Logs") - self.panels_notebook.set("Logs") # 預設顯示 Logs + self.panels_notebook.set("Logs") # default to Logs - # Downloads tab:可捲動容器 + DownloadsPanel + # Downloads tab: scrollable container + DownloadsPanel downloads_scroll = ctk.CTkScrollableFrame(downloads_tab, height=120) downloads_scroll.pack(fill="both", expand=True) self.downloads_panel = DownloadsPanel( @@ -764,7 +612,7 @@ def _build_panels(self) -> None: tokens=self.ui_tokens, ) - # Logs tab:CTkTextbox + # Logs tab: CTkTextbox self.log_text = ctk.CTkTextbox(logs_tab, height=120, wrap="word") self.log_text.pack(fill="both", expand=True) @@ -787,13 +635,13 @@ def _build_download_controls(self) -> None: controls.grid(row=0, column=1, sticky="e", padx=(10, 0)) self.download_btn = ctk.CTkButton( - controls, text="⬇ Download Selected", command=self.download_selected, + controls, text="\u2b07 Download Selected", command=self.download_selected, height=30, ) self.download_btn.grid(row=0, column=0, padx=(0, 4)) self.pause_btn = ctk.CTkButton( - controls, text="⏸ Pause", + controls, text="\u23f8 Pause", fg_color=("gray70", "gray30"), hover_color=("gray60", "gray40"), command=self.toggle_pause, @@ -803,7 +651,7 @@ def _build_download_controls(self) -> None: self.pause_btn.grid(row=0, column=1, padx=(0, 4)) self.path_btn = ctk.CTkButton( - controls, text="📁 Folder", + controls, text="\U0001f4c1 Folder", fg_color=("gray70", "gray30"), hover_color=("gray60", "gray40"), command=self.choose_download_path, @@ -1114,7 +962,7 @@ def _start(): self.scan_pause_btn.grid() # show self.scan_pause_btn.configure(state="normal") self.progress_bar.set(0) - self.progress_label.configure(text="Scanning…") + self.progress_label.configure(text="Scanning\u2026") self._set_status("Scanning", "#B45309") self.window.after(0, _start) @@ -1133,12 +981,12 @@ def _finish(): else: self.progress_bar.set(1) n = len(self.files_dict) - self.progress_label.configure(text=f"Done — {n} files found") + self.progress_label.configure(text=f"Done \u2014 {n} files found") self._set_status("Ready", "#059669") self.window.after(0, _finish) def update_progress(self, file_path: str, file_name: str, progress: float) -> None: - """Backend hook — called from download thread with per-file progress (0–100).""" + """Backend hook — called from download thread with per-file progress (0-100).""" self.window.after(0, lambda: self._update_download_progress(file_path, progress)) def update_download_status(self, file_path: str, status: str) -> None: @@ -1146,13 +994,13 @@ def update_download_status(self, file_path: str, status: str) -> None: self.window.after(0, lambda: self._set_download_status(file_path, status)) def _update_scan_progress(self, scanned: int, total: int) -> None: - """Update the scan progress bar (0–100%).""" + """Update the scan progress bar (0-100%).""" if total <= 0: self.progress_bar.set(0) return pct = scanned / total self.progress_bar.set(pct) - self.progress_label.configure(text=f"Scanning… {scanned}/{total} ({pct:.0%})") + self.progress_label.configure(text=f"Scanning\u2026 {scanned}/{total} ({pct:.0%})") def add_folder(self, dir_path: str, url: str) -> str: """Ensure all path segments exist as folder nodes; return leaf node_id.""" @@ -1177,6 +1025,7 @@ def add_folder(self, dir_path: str, url: str) -> str: size="", file_type="", icon_group="folder", + expanded=True, ) self.tree_nodes[node_id] = node if parent_id: @@ -1257,25 +1106,25 @@ def _file_icon_and_group(self, file_name: str, file_type: str | None): ext = normalize_extension(file_name) mime = (file_type or "").lower() if ext in (".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp", ".ico"): - return "🖼", "image" + return "\U0001f5bc", "image" if ext in (".md", ".txt", ".pdf", ".doc", ".docx", ".rtf"): - return "📄", "document" + return "\U0001f4c4", "document" if ext in (".zip", ".rar", ".7z", ".tar", ".gz", ".bz2", ".xz"): - return "🗜", "archive" + return "\U0001f5dc", "archive" if ext in ( ".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs", ".java", ".c", ".cpp", ".h", ".hpp", ".json", ".yaml", ".yml", ".toml", ".xml", ".html", ".css", ".sh", ): - return "💻", "code" + return "\U0001f4bb", "code" if "text/" in mime: - return "📄", "text" + return "\U0001f4c4", "text" if "image/" in mime: - return "🖼", "image" + return "\U0001f5bc", "image" if "audio/" in mime: - return "🎵", "audio" + return "\U0001f3b5", "audio" if "video/" in mime: - return "🎬", "video" - return "📦", "binary" + return "\U0001f3ac", "video" + return "\U0001f4e6", "binary" # --- Backend bridge: downloads --- @@ -1446,70 +1295,6 @@ def toggle_panels(self) -> None: self.panels_visible = True self.toggle_panels_btn.configure(text="Hide Panels") - def select_all_types(self) -> None: - for var in self.file_types.values(): - var.set(True) - - def deselect_all_types(self) -> None: - for var in self.file_types.values(): - var.set(False) - - def _bind_hscroll_wheel(self, widget) -> None: - """讓 widget 的滾輪事件橫向捲動 filters_container(平滑版)。""" - try: - canvas = self.filters_container._parent_canvas - except AttributeError: - return - - # 每個 scroll unit = 20px,讓滾動距離合理 - canvas.configure(xscrollincrement=20) - - def _scroll(event): - delta = getattr(event, "delta", 0) - if not delta: - return - # macOS trackpad delta 很小 (1–5);滑鼠滾輪約 120 - # 統一換算為 unit 數,最少 1 - units = max(1, abs(delta) // 3) - canvas.xview_scroll(-units if delta > 0 else units, "units") - - widget.bind("", _scroll, add="+") - widget.bind("", _scroll, add="+") - - def _add_file_type_filter(self, ext: str) -> None: - if not hasattr(self, "filters_container"): - return - if ext in self.file_types: - return - var = tk.BooleanVar(value=True) - self.file_types[ext] = var - self.file_type_counts[ext] = 0 - cb = ctk.CTkCheckBox( - self.filters_container, - text=ext if ext else "(no ext)", - variable=var, - onvalue=True, - offvalue=False, - ) - cb.pack(side="left", padx=4, pady=4) - self._bind_hscroll_wheel(cb) - var.trace_add("write", lambda *_: self._on_type_filter_changed(ext)) - self.file_type_widgets[ext] = cb - - def _on_type_filter_changed(self, ext: str) -> None: - """Show or hide existing tree rows when a file-type checkbox is toggled.""" - if self.full_tree_backup: - return # search filter active; handled by _filter_tree_by_term - var = self.file_types.get(ext) - if var is None: - return - visible = var.get() - for node in self.tree_nodes.values(): - if node.kind == "file" and normalize_extension(node.name) == ext: - node.hidden = not visible - self._rebuild_visible() - self._sync_rows() - def main(): app = WebsiteCopierCtk() diff --git a/backend.py b/src/index_ripper/backend.py similarity index 99% rename from backend.py rename to src/index_ripper/backend.py index b8261d0..246714b 100644 --- a/backend.py +++ b/src/index_ripper/backend.py @@ -12,7 +12,7 @@ import requests from bs4 import BeautifulSoup -from app_utils import cleanup_partial_file, is_url_in_scope +from index_ripper.utils import cleanup_partial_file, is_url_in_scope diff --git a/self_test.py b/src/index_ripper/self_test.py similarity index 100% rename from self_test.py rename to src/index_ripper/self_test.py diff --git a/settings_store.py b/src/index_ripper/settings.py similarity index 100% rename from settings_store.py rename to src/index_ripper/settings.py diff --git a/src/index_ripper/ui/__init__.py b/src/index_ripper/ui/__init__.py new file mode 100644 index 0000000..08e7b92 --- /dev/null +++ b/src/index_ripper/ui/__init__.py @@ -0,0 +1 @@ +"""Index Ripper UI components.""" diff --git a/ui_downloads.py b/src/index_ripper/ui/downloads.py similarity index 100% rename from ui_downloads.py rename to src/index_ripper/ui/downloads.py diff --git a/src/index_ripper/ui/filetree.py b/src/index_ripper/ui/filetree.py new file mode 100644 index 0000000..46f5e24 --- /dev/null +++ b/src/index_ripper/ui/filetree.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +import customtkinter as ctk + + +@dataclass +class TreeNode: + node_id: str + parent_id: str # "" for root-level nodes + name: str + kind: str # "folder" | "file" + full_path: str # "" for folders + size: str + file_type: str + icon_group: str # "folder"|"image"|"document"|"archive"|"code"|"audio"|"video"|"text"|"binary" + checked: bool = False + expanded: bool = False + hidden: bool = False # True when filtered out by search + children: list[str] = field(default_factory=list) # ordered list of child node_ids + + +_EMOJI_ICONS = { + "folder": "📁", + "image": "🖼️", + "document": "📄", + "archive": "🗜️", + "code": "💻", + "audio": "🎵", + "video": "🎬", + "text": "📝", + "binary": "⚙️", +} + +_BG_NORMAL = "transparent" +_BG_HOVER = ("#F1F5F9", "#1E293B") +_BG_CHECKED = ("#EFF6FF", "#172554") +_BG_CHECKED_HOVER = ("#DBEAFE", "#1E3A5F") + + +class RowWidget: + """One visible row in the FileTree.""" + + INDENT_PX = 20 + ROW_HEIGHT = 36 + + def __init__(self, parent, app, node: TreeNode, depth: int): + self.app = app + self.node_id = node.node_id + self._checked = node.checked + self._hovered = False + + self.frame = ctk.CTkFrame(parent, height=self.ROW_HEIGHT, corner_radius=4) + self.frame.pack(fill="x", padx=4, pady=0) + self.frame.pack_propagate(False) + + # 3-px accent bar (left edge, blue when checked) + self._accent = ctk.CTkFrame( + self.frame, width=3, corner_radius=0, + fg_color="#2563EB" if node.checked else "transparent", + ) + self._accent.pack(side="left", fill="y") + + # Indent spacer + if depth > 0: + ctk.CTkFrame( + self.frame, width=depth * self.INDENT_PX, + fg_color="transparent", height=self.ROW_HEIGHT, + ).pack(side="left") + + # Emoji icon + ctk.CTkLabel( + self.frame, + text=_EMOJI_ICONS.get(node.icon_group, "📄"), + font=ctk.CTkFont(size=18), + width=28, + ).pack(side="left", padx=(4, 4)) + + # Name label + self.name_label = ctk.CTkLabel( + self.frame, + text=node.name, + anchor="w", + font=ctk.CTkFont(size=14, weight="bold" if node.kind == "folder" else "normal"), + ) + self.name_label.pack(side="left", fill="x", expand=True) + + # Chevron on RIGHT (folders only) + if node.kind == "folder": + self.chevron = ctk.CTkButton( + self.frame, text="▼" if node.expanded else "▶", + width=22, height=22, fg_color="transparent", + hover_color=("gray85", "gray30"), text_color=("gray40", "gray60"), + font=ctk.CTkFont(size=10), + command=lambda: app._on_chevron_click(self.node_id), + ) + self.chevron.pack(side="right", padx=(4, 4)) + elif node.kind == "file" and node.size: + # Size label (right side, files only) + ctk.CTkLabel( + self.frame, + text=node.size, + font=ctk.CTkFont(size=12), + text_color=("gray50", "gray60"), + width=80, + anchor="e", + ).pack(side="right", padx=(0, 8)) + + # Bind hover only on outer frame (avoids flicker from child entry) + self.frame.bind("", self._on_enter) + self.frame.bind("", self._on_leave) + # Bind click on frame and all non-chevron children + self._bind_clicks(self.frame) + + def _bind_clicks(self, widget) -> None: + """Bind click handler on widget and all children except the chevron.""" + if hasattr(self, "chevron") and widget is self.chevron: + return # chevron has its own command; don't intercept clicks + widget.bind("", self._on_click) + for child in widget.winfo_children(): + self._bind_clicks(child) + + def _on_enter(self, _event=None) -> None: + self._hovered = True + self._update_bg() + + def _on_leave(self, _event=None) -> None: + self._hovered = False + self._update_bg() + + def _on_click(self, event) -> None: + self.app._on_row_click(self.node_id, event) + + def set_checked(self, checked: bool) -> None: + self._checked = checked + self._accent.configure( + fg_color="#2563EB" if checked else "transparent" + ) + self._update_bg() + + def set_chevron(self, expanded: bool) -> None: + if hasattr(self, "chevron"): + self.chevron.configure(text="▼" if expanded else "▶") + + def _update_bg(self) -> None: + if self._checked and self._hovered: + color = _BG_CHECKED_HOVER + elif self._checked: + color = _BG_CHECKED + elif self._hovered: + color = _BG_HOVER + else: + color = _BG_NORMAL + self.frame.configure(fg_color=color) + + def destroy(self) -> None: + self.frame.destroy() + + +def should_skip_file_row(existing_entry) -> bool: + """Return True when a file row is already fully registered.""" + return existing_entry is not None diff --git a/src/index_ripper/ui/filters.py b/src/index_ripper/ui/filters.py new file mode 100644 index 0000000..8196e45 --- /dev/null +++ b/src/index_ripper/ui/filters.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import tkinter as tk + +import customtkinter as ctk + +from index_ripper.utils import normalize_extension + + +class FileTypeFilterMixin: + """Mixin providing file-type filter checkbox management for the main app class.""" + + def select_all_types(self) -> None: + for var in self.file_types.values(): + var.set(True) + + def deselect_all_types(self) -> None: + for var in self.file_types.values(): + var.set(False) + + def _bind_hscroll_wheel(self, widget) -> None: + """讓 widget 的滾輪事件橫向捲動 filters_container(平滑版)。""" + try: + canvas = self.filters_container._parent_canvas + except AttributeError: + return + + # 每個 scroll unit = 20px,讓滾動距離合理 + canvas.configure(xscrollincrement=20) + + def _scroll(event): + delta = getattr(event, "delta", 0) + if not delta: + return + # macOS trackpad delta 很小 (1–5);滑鼠滾輪約 120 + # 統一換算為 unit 數,最少 1 + units = max(1, abs(delta) // 3) + canvas.xview_scroll(-units if delta > 0 else units, "units") + + widget.bind("", _scroll, add="+") + widget.bind("", _scroll, add="+") + + def _add_file_type_filter(self, ext: str) -> None: + if not hasattr(self, "filters_container"): + return + if ext in self.file_types: + return + var = tk.BooleanVar(value=True) + self.file_types[ext] = var + self.file_type_counts[ext] = 0 + cb = ctk.CTkCheckBox( + self.filters_container, + text=ext if ext else "(no ext)", + variable=var, + onvalue=True, + offvalue=False, + ) + cb.pack(side="left", padx=4, pady=4) + self._bind_hscroll_wheel(cb) + var.trace_add("write", lambda *_: self._on_type_filter_changed(ext)) + self.file_type_widgets[ext] = cb + + def _on_type_filter_changed(self, ext: str) -> None: + """Show or hide existing tree rows when a file-type checkbox is toggled.""" + if self.full_tree_backup: + return # search filter active; handled by _filter_tree_by_term + var = self.file_types.get(ext) + if var is None: + return + visible = var.get() + for node in self.tree_nodes.values(): + if node.kind == "file" and normalize_extension(node.name) == ext: + node.hidden = not visible + self._rebuild_visible() + self._sync_rows() diff --git a/ui_theme.py b/src/index_ripper/ui/theme.py similarity index 100% rename from ui_theme.py rename to src/index_ripper/ui/theme.py diff --git a/app_utils.py b/src/index_ripper/utils.py similarity index 100% rename from app_utils.py rename to src/index_ripper/utils.py diff --git a/test_app_utils.py b/test_app_utils.py deleted file mode 100644 index 0d22df4..0000000 --- a/test_app_utils.py +++ /dev/null @@ -1,23 +0,0 @@ -import unittest - -from app_utils import default_download_folder, normalize_extension, shorten_path - - -class TestAppUtils(unittest.TestCase): - def test_normalize_extension_normal(self): - self.assertEqual(normalize_extension("a.TXT"), ".txt") - - def test_normalize_extension_none(self): - self.assertEqual(normalize_extension("README"), ".(No Extension)") - - def test_shorten_path(self): - self.assertEqual(shorten_path("/a/b", keep=10), "/a/b") - self.assertEqual(shorten_path("/" + "x" * 50, keep=10), "..." + "x" * 10) - - def test_default_download_folder(self): - out = default_download_folder("https://example.com/a/b", "/tmp") - self.assertTrue(out.endswith("example.com")) - - -if __name__ == "__main__": - unittest.main() diff --git a/test_ui_downloads.py b/test_ui_downloads.py deleted file mode 100644 index 01bdf04..0000000 --- a/test_ui_downloads.py +++ /dev/null @@ -1,17 +0,0 @@ -import unittest - -import ui_downloads - - -class TestUIDownloadsSemantics(unittest.TestCase): - def test_download_status_state_mapping(self): - self.assertEqual(ui_downloads.download_status_state("Queued"), "queued") - self.assertEqual(ui_downloads.download_status_state("Downloading 12.0%"), "active") - self.assertEqual(ui_downloads.download_status_state("Completed"), "success") - self.assertEqual(ui_downloads.download_status_state("Failed"), "error") - self.assertEqual(ui_downloads.download_status_state("Canceling..."), "warning") - self.assertEqual(ui_downloads.download_status_state("something else"), "queued") - - -if __name__ == "__main__": - unittest.main() diff --git a/test_ui_ttkb.py b/test_ui_ttkb.py deleted file mode 100644 index 6e8e2d4..0000000 --- a/test_ui_ttkb.py +++ /dev/null @@ -1,21 +0,0 @@ -import unittest - -from ui_ttkb import should_skip_file_row - - -class TestUiTtkbScanEntryGate(unittest.TestCase): - def test_should_not_skip_placeholder_none(self): - self.assertFalse(should_skip_file_row(None)) - - def test_should_skip_real_file_entry(self): - self.assertTrue(should_skip_file_row({"url": "https://example.com/a.txt"})) - - def test_should_skip_only_for_real_entries_not_placeholders(self): - placeholders = [None] - real_entries = [{"url": "x"}, {"file_name": "a.txt"}] - self.assertEqual([should_skip_file_row(v) for v in placeholders], [False]) - self.assertEqual([should_skip_file_row(v) for v in real_entries], [True, True]) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test_ui_ctk.py b/tests/test_app_smoke.py similarity index 84% rename from test_ui_ctk.py rename to tests/test_app_smoke.py index b247411..7819b00 100644 --- a/test_ui_ctk.py +++ b/tests/test_app_smoke.py @@ -1,4 +1,4 @@ -"""Smoke tests for ui_ctk.WebsiteCopierCtk.""" +"""Smoke tests for index_ripper.app (WebsiteCopierCtk).""" import os import sys import unittest @@ -18,12 +18,12 @@ class TestWebsiteCopierCtkSmoke(unittest.TestCase): def _make_smoke(self): import importlib - mod = importlib.import_module("ui_ctk") + mod = importlib.import_module("index_ripper.app") importlib.reload(mod) return mod.WebsiteCopierCtk(ui_smoke=True) def test_import(self): - import ui_ctk # noqa: F401 + from index_ripper import app as ui_ctk # noqa: F401 def test_smoke_init_and_destroy(self): app = self._make_smoke() @@ -43,7 +43,8 @@ def test_has_log_text(self): self.assertTrue(hasattr(app, "log_text")) def test_full_ui_has_scan_btn(self): - import ui_ctk, importlib + from index_ripper import app as ui_ctk + import importlib importlib.reload(ui_ctk) app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) app.window.after(0, app.window.destroy) @@ -52,9 +53,9 @@ def test_full_ui_has_scan_btn(self): self.assertTrue(hasattr(app, "url_entry")) self.assertTrue(hasattr(app, "status_label")) - def test_has_filters_container(self): - import ui_ctk, importlib + from index_ripper import app as ui_ctk + import importlib importlib.reload(ui_ctk) app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) app.window.after(0, app.window.destroy) @@ -62,7 +63,8 @@ def test_has_filters_container(self): self.assertTrue(hasattr(app, "filters_container")) def test_add_file_type_filter(self): - import ui_ctk, importlib + from index_ripper import app as ui_ctk + import importlib importlib.reload(ui_ctk) app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) app._add_file_type_filter(".mp4") @@ -71,9 +73,9 @@ def test_add_file_type_filter(self): self.assertIn(".mp4", app.file_types) self.assertIn(".mp4", app.file_type_widgets) - def test_has_download_controls(self): - import ui_ctk, importlib + from index_ripper import app as ui_ctk + import importlib importlib.reload(ui_ctk) app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) app.window.after(0, app.window.destroy) @@ -84,7 +86,8 @@ def test_has_download_controls(self): self.assertTrue(hasattr(app, "toggle_panels_btn")) def test_has_file_tree(self): - import ui_ctk, importlib + from index_ripper import app as ui_ctk + import importlib importlib.reload(ui_ctk) app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) app.window.after(0, app.window.destroy) @@ -95,7 +98,8 @@ def test_has_file_tree(self): self.assertTrue(hasattr(app, "search_var")) def test_search_var_traces(self): - import ui_ctk, importlib + from index_ripper import app as ui_ctk + import importlib importlib.reload(ui_ctk) app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) app.window.after(0, app.window.destroy) @@ -104,7 +108,8 @@ def test_search_var_traces(self): self.assertTrue(len(app.search_var.trace_info()) > 0) def test_has_progress_bar(self): - import ui_ctk, importlib + from index_ripper import app as ui_ctk + import importlib importlib.reload(ui_ctk) app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) app.window.after(0, app.window.destroy) @@ -113,7 +118,8 @@ def test_has_progress_bar(self): self.assertTrue(hasattr(app, "progress_label")) def test_has_panels_notebook(self): - import ui_ctk, importlib + from index_ripper import app as ui_ctk + import importlib importlib.reload(ui_ctk) app = ui_ctk.WebsiteCopierCtk(ui_smoke=False) app.window.after(0, app.window.destroy) @@ -123,9 +129,9 @@ def test_has_panels_notebook(self): self.assertTrue(hasattr(app, "log_text")) self.assertTrue(hasattr(app, "downloads_panel")) - def test_poll_methods_exist(self): - import ui_ctk, importlib + from index_ripper import app as ui_ctk + import importlib importlib.reload(ui_ctk) app = ui_ctk.WebsiteCopierCtk(ui_smoke=True) for method in ("_poll_scan_queue", "_poll_file_queue", @@ -135,7 +141,7 @@ def test_poll_methods_exist(self): app.run() def test_scan_methods_exist(self): - import ui_ctk + from index_ripper import app as ui_ctk for method in ("start_scan", "toggle_scan_pause", "clear_scan_results", "add_folder", "add_file", "update_progress", "download_selected", "toggle_pause", diff --git a/test_backend.py b/tests/test_backend.py similarity index 99% rename from test_backend.py rename to tests/test_backend.py index 6f2cc6f..27bce72 100644 --- a/test_backend.py +++ b/tests/test_backend.py @@ -5,7 +5,7 @@ import unittest from unittest.mock import MagicMock, patch -from backend import Backend +from index_ripper.backend import Backend class MockUIManager: diff --git a/tests/test_downloads.py b/tests/test_downloads.py new file mode 100644 index 0000000..fb0c9f5 --- /dev/null +++ b/tests/test_downloads.py @@ -0,0 +1,17 @@ +import unittest + +from index_ripper.ui import downloads + + +class TestUIDownloadsSemantics(unittest.TestCase): + def test_download_status_state_mapping(self): + self.assertEqual(downloads.download_status_state("Queued"), "queued") + self.assertEqual(downloads.download_status_state("Downloading 12.0%"), "active") + self.assertEqual(downloads.download_status_state("Completed"), "success") + self.assertEqual(downloads.download_status_state("Failed"), "error") + self.assertEqual(downloads.download_status_state("Canceling..."), "warning") + self.assertEqual(downloads.download_status_state("something else"), "queued") + + +if __name__ == "__main__": + unittest.main() diff --git a/test_settings_store.py b/tests/test_settings.py similarity index 93% rename from test_settings_store.py rename to tests/test_settings.py index 7ca95e8..44b24ac 100644 --- a/test_settings_store.py +++ b/tests/test_settings.py @@ -2,7 +2,7 @@ import tempfile import unittest -from settings_store import load_settings, save_settings +from index_ripper.settings import load_settings, save_settings class TestSettingsStore(unittest.TestCase): diff --git a/test_ui_theme.py b/tests/test_theme.py similarity index 54% rename from test_ui_theme.py rename to tests/test_theme.py index e5adf75..80cfba2 100644 --- a/test_ui_theme.py +++ b/tests/test_theme.py @@ -1,22 +1,22 @@ import unittest -import ui_theme +from index_ripper.ui import theme class TestUITheme(unittest.TestCase): def test_action_button_style_name(self): - self.assertEqual(ui_theme.action_button_style_name("primary"), "Primary.TButton") + self.assertEqual(theme.action_button_style_name("primary"), "Primary.TButton") self.assertEqual( - ui_theme.action_button_style_name("secondary"), "Secondary.TButton" + theme.action_button_style_name("secondary"), "Secondary.TButton" ) - self.assertEqual(ui_theme.action_button_style_name("danger"), "Danger.TButton") + self.assertEqual(theme.action_button_style_name("danger"), "Danger.TButton") self.assertEqual( - ui_theme.action_button_style_name("success"), "Success.TButton" + theme.action_button_style_name("success"), "Success.TButton" ) - self.assertEqual(ui_theme.action_button_style_name("unknown"), "Secondary.TButton") + self.assertEqual(theme.action_button_style_name("unknown"), "Secondary.TButton") def test_ui_tokens_contains_modern_widget_styles(self): - tokens = ui_theme.ui_tokens() + tokens = theme.ui_tokens() self.assertIn("option_menu", tokens) self.assertIn("tabview", tokens) self.assertIn("checkbox", tokens) @@ -25,7 +25,7 @@ def test_ui_tokens_contains_modern_widget_styles(self): self.assertIn("border_color", tokens["checkbox"]) def test_apply_bootstrap_theme_not_present(self): - self.assertFalse(hasattr(ui_theme, "apply_bootstrap_theme")) + self.assertFalse(hasattr(theme, "apply_bootstrap_theme")) if __name__ == "__main__": diff --git a/test_security_utils.py b/tests/test_utils.py similarity index 71% rename from test_security_utils.py rename to tests/test_utils.py index 01b4364..f06a85f 100644 --- a/test_security_utils.py +++ b/tests/test_utils.py @@ -2,14 +2,33 @@ import tempfile import unittest -from app_utils import ( +from index_ripper.utils import ( + default_download_folder, is_url_in_scope, + normalize_extension, safe_join, sanitize_filename, sanitize_path_segment, + shorten_path, ) +class TestAppUtils(unittest.TestCase): + def test_normalize_extension_normal(self): + self.assertEqual(normalize_extension("a.TXT"), ".txt") + + def test_normalize_extension_none(self): + self.assertEqual(normalize_extension("README"), ".(No Extension)") + + def test_shorten_path(self): + self.assertEqual(shorten_path("/a/b", keep=10), "/a/b") + self.assertEqual(shorten_path("/" + "x" * 50, keep=10), "..." + "x" * 10) + + def test_default_download_folder(self): + out = default_download_folder("https://example.com/a/b", "/tmp") + self.assertTrue(out.endswith("example.com")) + + class TestSecurityUtils(unittest.TestCase): def test_sanitize_path_segment_blocks_traversal_patterns(self): self.assertEqual(sanitize_path_segment(".."), "_") diff --git a/ui_ttkb.py b/ui_ttkb.py deleted file mode 100644 index bbf3d8d..0000000 --- a/ui_ttkb.py +++ /dev/null @@ -1,1387 +0,0 @@ -from __future__ import annotations - -import os -import threading -from concurrent.futures import ThreadPoolExecutor -from queue import Empty, Queue - -import tkinter as tk -from tkinter import filedialog, messagebox - -import requests -from requests.adapters import HTTPAdapter -from urllib3.util.retry import Retry - -import ttkbootstrap as ttkb -from ttkbootstrap.constants import BOTH, END, LEFT, RIGHT, TOP, X, Y - -from app_utils import default_download_folder, normalize_extension, safe_join, sanitize_filename -from backend import Backend - - -def should_skip_file_row(existing_entry) -> bool: - """Return True when a file row is already fully registered.""" - return existing_entry is not None - - -class WebsiteCopierTtkb: - USER_AGENT = "IndexRipper/2.0" - - def __init__(self, ui_smoke: bool = False): - self._ui_smoke = bool(ui_smoke) - self.debug_enabled = os.environ.get("INDEX_RIPPER_DEBUG", "0") != "0" - self.use_modal_dialogs = os.environ.get("INDEX_RIPPER_MODAL_DIALOGS", "0") == "1" - - self.window = ttkb.Window(title="Index Ripper", themename="flatly") - self.window.geometry("1200x900") - self.window.minsize(900, 650) - - self.backend = Backend(self) - - self.pause_event = threading.Event() - self.pause_event.set() - self.scan_pause_event = threading.Event() - self.scan_pause_event.set() - - self.is_scanning = False - self.scanned_urls = 0 - self.total_urls = 0 - - self.files_dict_lock = threading.Lock() - self.folders_dict_lock = threading.Lock() - self.files_dict: dict[str, dict] = {} - self.folders: dict[str, str] = {} - self.checked_items: set[str] = set() - self.checkbox_checked = "✔ " - - self.dir_queue = Queue() - self.file_queue = Queue() - self.scan_item_buffer = Queue() - self.is_processing_dirs = False - self.is_processing_files = False - self.scan_flush_interval_ms = 16 - self.scan_flush_batch_size = 200 - self.scan_flush_job = None - self._last_logged_queue_size = None - - self.file_types: dict[str, tk.BooleanVar] = {} - self.file_type_counts: dict[str, int] = {} - self.file_type_widgets: dict[str, tk.Checkbutton] = {} - - self.download_path = "" - self.download_queue = Queue() - self.max_workers = 5 - self.executor = ThreadPoolExecutor(max_workers=self.max_workers) - self.active_downloads = [] - - retry_strategy = Retry( - total=3, - backoff_factor=0.1, - status_forcelist=[500, 502, 503, 504], - ) - self.session = requests.Session() - adapter = HTTPAdapter( - max_retries=retry_strategy, - pool_connections=10, - pool_maxsize=10, - pool_block=False, - ) - self.session.mount("http://", adapter) - self.session.mount("https://", adapter) - self.timeout = (10, 20) - - self._build_ui() - - self.window.protocol("WM_DELETE_WINDOW", self.on_closing) - if not self._ui_smoke: - self.window.bind("", self.focus_search) - self.window.bind("", self.focus_logs) - self.window.bind("", self.clear_search) - self.window.bind_all("", self._on_global_url_paste, add="+") - self.window.bind_all("", self._on_global_url_paste, add="+") - self.window.bind_all("", self._on_global_url_paste, add="+") - self.window.bind_all("", self._on_global_url_paste, add="+") - self.window.bind_all("<>", self._on_global_url_paste, add="+") - self._debug("WebsiteCopierTtkb initialized") - - def _build_ui(self) -> None: - if self._ui_smoke: - self._build_ui_smoke_only() - return - - self.window.columnconfigure(0, weight=1) - self.window.rowconfigure(2, weight=1) - - header = ttkb.Frame(self.window, padding=10) - header.grid(row=0, column=0, sticky="ew") - header.columnconfigure(1, weight=1) - - ttkb.Label(header, text="URL").grid(row=0, column=0, sticky="w") - self.url_var = tk.StringVar() - self.url_entry = tk.Entry(header, textvariable=self.url_var, relief="flat") - self.url_entry.grid(row=0, column=1, sticky="ew", padx=(8, 8)) - self.url_entry.bind("", self._on_url_paste) - self.url_entry.bind("", self._on_url_paste) - self.url_entry.bind("", self._on_url_paste) - self.url_entry.bind("", self._on_url_paste) - self.url_entry.bind("", self._on_url_paste) - self.url_entry.bind("", self._show_url_context_menu) - self.url_entry.bind("", self._show_url_context_menu) - - self.url_context_menu = tk.Menu(self.window, tearoff=0) - self.url_context_menu.add_command(label="Paste", command=self._paste_into_url_entry) - - self.status_label = ttkb.Label(header, text="Ready", bootstyle="success") - self.status_label.grid(row=0, column=2, sticky="e", padx=(0, 8)) - - actions = ttkb.Frame(header) - actions.grid(row=0, column=3, sticky="e") - self.scan_btn = ttkb.Button(actions, text="Scan", bootstyle="primary", command=self.start_scan) - self.scan_btn.pack(side=LEFT, padx=3) - self.scan_pause_btn = ttkb.Button( - actions, - text="Pause Scan", - bootstyle="secondary", - command=self.toggle_scan_pause, - state="disabled", - ) - self.scan_pause_btn.pack(side=LEFT, padx=3) - self.clear_scan_btn = ttkb.Button(actions, text="Clear", command=self.clear_scan_results) - self.clear_scan_btn.pack(side=LEFT, padx=3) - - filters_and_controls = ttkb.Frame(self.window, padding=(10, 0, 10, 6)) - filters_and_controls.grid(row=1, column=0, sticky="ew") - filters_and_controls.columnconfigure(0, weight=1) - filters_and_controls.columnconfigure(1, weight=0) - - self.filters_frame = ttkb.Frame(filters_and_controls) - self.filters_frame.grid(row=0, column=0, sticky="ew") - self.filters_frame.columnconfigure(0, weight=1) - self.filters_canvas, self.filters_container = self._create_scrollable_container( - self.filters_frame, - height=85, - ) - type_actions = ttkb.Frame(self.filters_frame) - type_actions.pack(fill=X, pady=(4, 0)) - ttkb.Button( - type_actions, - text="Select All Types", - bootstyle="secondary-outline", - command=self.select_all_types, - ).pack(side=LEFT, padx=(0, 6)) - ttkb.Button( - type_actions, - text="Deselect All Types", - bootstyle="secondary-outline", - command=self.deselect_all_types, - ).pack(side=LEFT) - - controls = ttkb.Frame(filters_and_controls) - controls.grid(row=0, column=1, sticky="e", padx=(10, 0)) - self.download_btn = ttkb.Button( - controls, - text="Download Selected", - bootstyle="success", - command=self.download_selected, - ) - self.download_btn.grid(row=0, column=0, padx=4) - self.pause_btn = ttkb.Button( - controls, - text="Pause", - bootstyle="secondary", - command=self.toggle_pause, - state="disabled", - ) - self.pause_btn.grid(row=0, column=1, padx=4) - self.path_btn = ttkb.Button( - controls, - text="Choose Folder", - bootstyle="secondary", - command=self.choose_download_path, - ) - self.path_btn.grid(row=0, column=2, padx=4) - self.panels_visible = True - self.toggle_panels_btn = ttkb.Button( - controls, - text="Hide Panels", - bootstyle="secondary-outline", - command=self.toggle_panels, - ) - self.toggle_panels_btn.grid(row=0, column=5, padx=(10, 0)) - - ttkb.Label(controls, text="Threads").grid(row=0, column=3, padx=(12, 4)) - self.threads_var = tk.StringVar(value="5") - try: - self.threads_combo = ttkb.Combobox( - controls, - values=[str(i) for i in range(1, 11)], - textvariable=self.threads_var, - width=4, - state="readonly", - ) - self.threads_combo.bind("<>", self.update_thread_count) - except (tk.TclError, TypeError): - # Fallback when ttkbootstrap arrow assets fail to initialize. - self.threads_combo = tk.Spinbox( - controls, - from_=1, - to=10, - textvariable=self.threads_var, - width=4, - command=self.update_thread_count, - relief="flat", - ) - self.threads_combo.bind("", self.update_thread_count) - self.threads_combo.bind("", self.update_thread_count) - self.threads_combo.grid(row=0, column=4) - - self.tree_frame = ttkb.Frame(self.window, padding=(10, 0, 10, 8)) - self.tree_frame.grid(row=2, column=0, sticky="nsew") - self.tree_frame.columnconfigure(0, weight=1) - self.tree_frame.rowconfigure(1, weight=1) - - search_bar = ttkb.Frame(self.tree_frame) - search_bar.grid(row=0, column=0, sticky="ew") - search_bar.columnconfigure(1, weight=1) - ttkb.Label(search_bar, text="Search").grid(row=0, column=0, padx=(0, 6)) - self.search_var = tk.StringVar() - self.search_entry = ttkb.Entry(search_bar, textvariable=self.search_var) - self.search_entry.grid(row=0, column=1, sticky="ew") - self.search_var.trace_add("write", self.on_search_filter_changed) - - self.tree = ttkb.Treeview( - self.tree_frame, - columns=("size", "type", "full_path"), - show="tree headings", - bootstyle="info", - selectmode="extended", - ) - self.tree.heading("#0", text="Path", command=lambda: self.sort_tree("#0")) - self.tree.heading("size", text="Size", command=lambda: self.sort_tree("size")) - self.tree.heading("type", text="Type", command=lambda: self.sort_tree("type")) - self.tree.column("#0", width=600, stretch=True) - self.tree.column("size", width=120, stretch=False, anchor="e") - self.tree.column("type", width=240, stretch=False) - self.tree.column("full_path", width=0, stretch=False, anchor="w") - self.tree.grid(row=1, column=0, sticky="nsew") - style = ttkb.Style() - style.configure("Treeview", font=("SF Pro Text", 14), rowheight=34) - style.configure("Treeview.Heading", font=("SF Pro Text", 13, "bold")) - self.tree.tag_configure("checked", foreground="#0F766E") - self.tree.bind("", self._on_tree_select_all) - self.tree.bind("", self._on_tree_select_all) - self.tree.bind("", self.on_tree_click) - self.tree.bind("", self.show_context_menu) - self.tree.bind("", self.show_context_menu) - self.tree.bind("", self.show_context_menu) - self.tree.bind("", self.on_tree_drag_select) - self.tree.bind("", self.on_tree_space) - self.tree.bind("", self.on_tree_enter) - - self.tree_scroll = tk.Scrollbar(self.tree_frame, orient="vertical", command=self.tree.yview) - self.tree.configure(yscrollcommand=self.tree_scroll.set) - self.tree_scroll.grid(row=1, column=1, sticky="ns") - - self.progress_frame = ttkb.Frame(self.window, padding=(10, 0, 10, 8)) - self.progress_frame.grid(row=3, column=0, sticky="ew") - self.progress_frame.columnconfigure(0, weight=1) - self.progress_var = tk.DoubleVar(value=0) - self.progress_bar = ttkb.Progressbar( - self.progress_frame, - variable=self.progress_var, - maximum=100, - bootstyle="success", - ) - self.progress_bar.grid(row=0, column=0, sticky="ew") - self.progress_label = ttkb.Label(self.progress_frame, text="") - self.progress_label.grid(row=1, column=0, sticky="w") - - self.panels_notebook = ttkb.Notebook(self.window, bootstyle="secondary") - self.panels_notebook.grid(row=4, column=0, sticky="ew", padx=10, pady=(0, 10)) - - self.downloads_tab = ttkb.Frame(self.panels_notebook, padding=10) - self.logs_tab = ttkb.Frame(self.panels_notebook, padding=10) - self.panels_notebook.add(self.downloads_tab, text="Downloads") - self.panels_notebook.add(self.logs_tab, text="Logs") - self.panels_notebook.select(self.logs_tab) - - downloads_frame = ttkb.Frame(self.downloads_tab) - downloads_frame.pack(fill=X) - self.downloads_canvas, self.downloads_container = self._create_scrollable_container( - downloads_frame, - height=140, - ) - - logs_wrap = ttkb.Frame(self.logs_tab) - logs_wrap.pack(fill=X) - self.log_text = tk.Text(logs_wrap, height=8, wrap="word") - self.log_text.pack(side=LEFT, fill=X, expand=True) - self.log_scroll = tk.Scrollbar(logs_wrap, orient="vertical", command=self.log_text.yview) - self.log_scroll.pack(side=RIGHT, fill=Y) - self.log_text.configure(yscrollcommand=self.log_scroll.set) - - self.download_items: dict[str, dict] = {} - self.sort_reverse = False - self.full_tree_backup = {} - self.drag_anchor_item = "" - self.context_menu = tk.Menu(self.window, tearoff=0) - self.context_menu.add_command(label="Select All", command=self.select_all) - self.context_menu.add_command(label="Deselect All", command=self.deselect_all) - self.context_menu.add_separator() - self.context_menu.add_command(label="Expand All", command=self.expand_all) - self.context_menu.add_command(label="Collapse All", command=self.collapse_all) - - def _build_ui_smoke_only(self) -> None: - self.window.columnconfigure(0, weight=1) - self.window.rowconfigure(1, weight=1) - - header = tk.Frame(self.window) - header.grid(row=0, column=0, sticky="ew", padx=10, pady=10) - tk.Label(header, text="Index Ripper ttkbootstrap UI Smoke").pack(anchor="w") - - self.url_var = tk.StringVar(value="https://example.com/") - tk.Entry(header, textvariable=self.url_var).pack(fill=X, pady=(6, 0)) - - body = tk.Frame(self.window) - body.grid(row=1, column=0, sticky="nsew", padx=10, pady=(0, 10)) - body.columnconfigure(0, weight=1) - body.rowconfigure(1, weight=1) - - self.search_var = tk.StringVar() - self.search_entry = tk.Entry(body, textvariable=self.search_var) - self.search_entry.grid(row=0, column=0, sticky="ew") - - self.log_text = tk.Text(body, height=8, wrap="word") - self.log_text.grid(row=1, column=0, sticky="nsew", pady=(6, 0)) - self.log_text.insert("end", "UI smoke minimal view initialized.\n") - - self.panels_notebook = None - self.logs_tab = None - - def notify_info(self, title: str, message: str) -> None: - if not self.use_modal_dialogs: - self.log_message(f"[INFO] {title}: {message}") - return - try: - messagebox.showinfo(title, message) - except Exception: - self.log_message(f"[INFO] {title}: {message}") - - def notify_warning(self, title: str, message: str) -> None: - if not self.use_modal_dialogs: - self.log_message(f"[WARN] {title}: {message}") - return - try: - messagebox.showwarning(title, message) - except Exception: - self.log_message(f"[WARN] {title}: {message}") - - def notify_error(self, title: str, message: str) -> None: - if not self.use_modal_dialogs: - self.log_message(f"[ERROR] {title}: {message}") - return - try: - messagebox.showerror(title, message) - except Exception: - self.log_message(f"[ERROR] {title}: {message}") - - def log_message(self, message: str) -> None: - try: - self.log_text.insert(END, message + "\n") - self.log_text.see(END) - except tk.TclError: - pass - - def _debug(self, message: str) -> None: - if not self.debug_enabled: - return - line = f"[DEBUG] {message}" - try: - print(line) - except Exception: - pass - try: - if hasattr(self, "log_text"): - self.log_text.insert(END, line + "\n") - self.log_text.see(END) - except Exception: - pass - - def _set_status(self, text: str, style: str = "secondary") -> None: - try: - self.status_label.configure(text=text, bootstyle=style) - except tk.TclError: - pass - - def choose_download_path(self) -> None: - path = filedialog.askdirectory(title="Choose Download Location") - if path: - self.download_path = path - - def _on_url_paste(self, _event=None): - """Paste clipboard into URL field exactly once.""" - self._debug("URL paste handler invoked") - self._paste_into_url_entry() - return "break" - - def _on_global_url_paste(self, _event=None): - try: - focused = self.window.focus_get() - except tk.TclError: - return None - if focused is self.url_entry: - self._debug("Global paste routed to URL entry") - self._paste_into_url_entry() - return "break" - return None - - def _paste_into_url_entry(self): - try: - text = self.window.clipboard_get() - except tk.TclError: - return - if not text: - return - try: - if self.url_entry.selection_present(): - self.url_entry.delete("sel.first", "sel.last") - except tk.TclError: - pass - self.url_entry.insert("insert", text) - self._debug(f"Pasted URL text length={len(text)}") - - def _show_url_context_menu(self, event): - try: - self.url_entry.focus_set() - self.url_context_menu.tk_popup(event.x_root, event.y_root) - finally: - self.url_context_menu.grab_release() - return "break" - - def update_thread_count(self, _event=None) -> None: - try: - self.max_workers = max(1, min(10, int(self.threads_var.get()))) - except ValueError: - self.max_workers = 5 - self.threads_var.set(str(self.max_workers)) - self.executor.shutdown(wait=False, cancel_futures=True) - self.executor = ThreadPoolExecutor(max_workers=self.max_workers) - - def toggle_pause(self) -> None: - if self.pause_event.is_set(): - self.pause_event.clear() - self.pause_btn.configure(text="Resume") - self.progress_label.configure(text="Downloads paused") - else: - self.pause_event.set() - self.pause_btn.configure(text="Pause") - - def toggle_scan_pause(self) -> None: - if self.scan_pause_event.is_set(): - self.scan_pause_event.clear() - self.scan_pause_btn.configure(text="Resume Scan") - self._set_status("Scan Paused", "warning") - else: - self.scan_pause_event.set() - self.scan_pause_btn.configure(text="Pause Scan") - self._set_status("Scanning", "info") - - def _run_on_ui_thread(self, callback, *args, wait: bool = False): - if not wait: - self.window.after(0, lambda: callback(*args)) - return - done = threading.Event() - error_box = {} - - def wrapped(): - try: - callback(*args) - except Exception as ex: - error_box["error"] = ex - finally: - done.set() - - self.window.after(0, wrapped) - done.wait() - if "error" in error_box: - raise error_box["error"] - - def _create_scrollable_container(self, parent, height: int): - canvas = tk.Canvas(parent, height=height, highlightthickness=0, borderwidth=0) - scrollbar = tk.Scrollbar(parent, orient="vertical", command=canvas.yview) - container = ttkb.Frame(canvas) - - window_id = canvas.create_window((0, 0), window=container, anchor="nw") - - def _on_container_configure(_event): - canvas.configure(scrollregion=canvas.bbox("all")) - - def _on_canvas_configure(event): - canvas.itemconfigure(window_id, width=event.width) - - container.bind("", _on_container_configure) - canvas.bind("", _on_canvas_configure) - canvas.configure(yscrollcommand=scrollbar.set) - - canvas.pack(side=LEFT, fill=X, expand=True) - scrollbar.pack(side=RIGHT, fill=Y) - return canvas, container - - def on_scan_started(self, url): - self._run_on_ui_thread(self._on_scan_started_ui, url, wait=True) - - def _on_scan_started_ui(self, _url): - self.is_scanning = True - self.scanned_urls = 0 - self.total_urls = 0 - self.scan_pause_btn.configure(state="normal", text="Pause Scan") - self.scan_btn.configure(text="Stop Scan") - self._set_status("Scanning", "info") - - self._cancel_scan_item_flush() - while not self.scan_item_buffer.empty(): - self.scan_item_buffer.get() - while not self.dir_queue.empty(): - self.dir_queue.get() - while not self.file_queue.empty(): - self.file_queue.get() - self.is_processing_dirs = False - self.is_processing_files = False - - with self.files_dict_lock: - self.files_dict.clear() - with self.folders_dict_lock: - self.folders.clear() - - for item in self.tree.get_children(""): - self.tree.delete(item) - self.checked_items.clear() - - self.file_types.clear() - self.file_type_counts.clear() - for widget in self.filters_container.winfo_children(): - widget.destroy() - - self.progress_var.set(0) - self.progress_label.configure(text="Preparing...") - - def on_scan_progress(self, scanned_urls, total_urls): - self._run_on_ui_thread(self._on_scan_progress_ui, scanned_urls, total_urls) - - def _on_scan_progress_ui(self, scanned_urls, total_urls): - self.scanned_urls = scanned_urls - self.total_urls = total_urls - self.update_scan_progress() - - def on_scan_item(self, **payload): - self._run_on_ui_thread(self._on_scan_item_ui, payload) - - def _on_scan_item_ui(self, payload: dict): - self.scan_item_buffer.put( - ( - bool(payload.get("is_directory")), - payload.get("path"), - payload.get("url"), - payload.get("file_name"), - payload.get("size"), - payload.get("file_type"), - payload.get("full_path"), - ) - ) - self._schedule_scan_item_flush() - - def _schedule_scan_item_flush(self): - if self.scan_flush_job is not None: - return - if not self.is_scanning and self.scan_item_buffer.empty(): - return - self.scan_flush_job = self.window.after( - self.scan_flush_interval_ms, self._flush_scan_item_buffer - ) - - def _cancel_scan_item_flush(self): - if self.scan_flush_job is None: - return - try: - self.window.after_cancel(self.scan_flush_job) - except tk.TclError: - pass - finally: - self.scan_flush_job = None - - def _flush_scan_item_buffer(self, max_items=None, reschedule=True): - self.scan_flush_job = None - if max_items is None: - max_items = self.scan_flush_batch_size - - processed = 0 - added_dir = False - added_file = False - - while processed < max_items: - try: - ( - is_directory, - path, - url, - file_name, - size, - file_type, - full_path, - ) = self.scan_item_buffer.get_nowait() - except Empty: - break - - if is_directory: - with self.folders_dict_lock: - self.dir_queue.put((path, url)) - added_dir = True - else: - self.file_queue.put((path, url, file_name, size, file_type, full_path)) - added_file = True - processed += 1 - - if added_dir and not self.is_processing_dirs: - self.window.after(0, self.process_dir_queue) - if added_file and not self.is_processing_files: - self.window.after(0, self.process_file_queue) - - if reschedule and not self.scan_item_buffer.empty(): - self._schedule_scan_item_flush() - - def on_scan_finished(self, stopped): - self._run_on_ui_thread(self._on_scan_finished_ui, stopped) - - def _on_scan_finished_ui(self, stopped): - self.is_scanning = False - self._cancel_scan_item_flush() - while not self.scan_item_buffer.empty(): - self._flush_scan_item_buffer(max_items=self.scan_flush_batch_size, reschedule=False) - while not self.dir_queue.empty(): - self.process_dir_queue() - while not self.file_queue.empty(): - self.process_file_queue() - - self.scan_btn.configure(text="Scan") - self.scan_pause_btn.configure(state="disabled", text="Pause Scan") - self.scan_pause_event.set() - - if stopped: - self.progress_label.configure(text="Scan stopped.") - self._set_status("Ready", "success") - self.log_message("[Scan] Stopped by user.") - else: - self.progress_label.configure(text="Scan finished.") - self._set_status("Ready", "success") - self.log_message("[Scan] Finished.") - self.scan_completed() - tree_items = len(self._all_tree_items()) - with self.files_dict_lock: - file_entries = len(self.files_dict) - self._debug( - f"scan_finished stopped={stopped} files_dict={file_entries} tree_items={tree_items}" - ) - - def update_scan_progress(self): - if self.total_urls <= 0: - self.progress_var.set(0) - return - pct = (self.scanned_urls / self.total_urls) * 100 - self.progress_var.set(pct) - self.progress_label.configure(text=f"Scan: {self.scanned_urls}/{self.total_urls}") - - def scan_completed(self): - self.full_tree_backup.clear() - self._backup_full_tree() - - def create_folder_structure(self, dir_path: str, url: str) -> str: - if not dir_path: - dir_path = "/" - parts = [p for p in dir_path.split("/") if p] - - current = "" - parent_id = "" - for part in parts: - current = current + "/" + part - with self.folders_dict_lock: - existing = self.folders.get(current) - if existing: - parent_id = existing - continue - - node_id = self.tree.insert( - parent_id, - "end", - text=f"📁 {part}", - values=("", "dir", ""), - tags=("folder",), - open=True, - ) - with self.folders_dict_lock: - self.folders[current] = node_id - parent_id = node_id - - return parent_id - - def process_dir_queue(self): - try: - self.is_processing_dirs = True - if not self.dir_queue.empty(): - dir_path, url = self.dir_queue.get() - self.create_folder_structure(dir_path, url) - self.window.after(10, self.process_dir_queue) - else: - self.is_processing_dirs = False - except tk.TclError: - self.is_processing_dirs = False - - def process_file_queue(self): - try: - self.is_processing_files = True - qsize = self.file_queue.qsize() - if qsize != self._last_logged_queue_size and (qsize <= 5 or qsize % 100 == 0): - self._debug(f"process_file_queue queue_size={qsize}") - self._last_logged_queue_size = qsize - if not self.file_queue.empty(): - ( - dir_path, - url, - file_name, - size, - file_type, - full_path, - ) = self.file_queue.get() - self.update_file_types(file_name) - self._add_file_to_tree(dir_path, url, file_name, size, file_type, full_path) - self.window.after(5, self.process_file_queue) - else: - self.is_processing_files = False - except tk.TclError: - self.is_processing_files = False - - def _add_file_to_tree(self, dir_path, url, file_name, size, file_type, full_path, from_cache=False): - if not file_name: - return - is_html_dir_like = ( - isinstance(file_type, str) - and "text/html" in file_type.lower() - and "." not in (file_name or "") - ) - parent_id = self.create_folder_structure(dir_path, url) - if not from_cache: - with self.files_dict_lock: - existing_entry = self.files_dict.get(full_path) - if should_skip_file_row(existing_entry): - return - if is_html_dir_like: - self.files_dict.pop(full_path, None) - else: - self.files_dict[full_path] = { - "url": url, - "file_name": file_name, - "size": size, - "file_type": file_type, - "path": dir_path, - } - - if is_html_dir_like: - folder_path = f"{dir_path.rstrip('/')}/{file_name}".replace("//", "/") - self.create_folder_structure(folder_path, url) - self._debug(f"add_folder_like path={folder_path} from_url={url}") - return - - ext = normalize_extension(file_name) - if not self.filter_file_type(file_name): - return - icon, group = self._file_icon_and_group(file_name, file_type) - display_type = group if not file_type else f"{group} | {file_type}" - - node_id = self.tree.insert( - parent_id, - "end", - text=f"{icon} {file_name}", - values=(size or "", display_type, full_path or ""), - tags=("file", ext), - ) - if full_path: - self.tree.set(node_id, "#0", f"{icon} {file_name}") - if full_path and full_path in self.checked_items: - self._set_item_checked_visual(node_id, True) - tags = list(self.tree.item(node_id, "tags")) - if "checked" not in tags: - tags.append("checked") - self.tree.item(node_id, tags=tuple(tags)) - self._debug( - f"add_file name={file_name} full_path={full_path} parent={parent_id or '/'} from_cache={from_cache}" - ) - - def filter_file_type(self, file_name: str) -> bool: - ext = normalize_extension(file_name) - var = self.file_types.get(ext) - if var is None: - return True - return bool(var.get()) - - def update_file_types(self, file_name: str) -> None: - ext = normalize_extension(file_name) - self.file_type_counts[ext] = self.file_type_counts.get(ext, 0) + 1 - if ext not in self.file_types: - self.file_types[ext] = tk.BooleanVar(value=True) - self.redraw_file_type_filters() - return - widget = self.file_type_widgets.get(ext) - if widget is not None: - widget.configure(text=f"{ext} ({self.file_type_counts.get(ext, 0)})") - - def redraw_file_type_filters(self): - for widget in self.filters_container.winfo_children(): - widget.destroy() - self.file_type_widgets.clear() - for ext in sorted(self.file_types.keys()): - cb = tk.Checkbutton( - self.filters_container, - text=f"{ext} ({self.file_type_counts.get(ext, 0)})", - variable=self.file_types[ext], - command=self.apply_filters, - anchor="w", - relief="flat", - highlightthickness=0, - padx=6, - pady=2, - ) - cb.pack(side=LEFT, padx=6) - self.file_type_widgets[ext] = cb - - def apply_filters(self) -> None: - before = len(self._all_tree_items()) - for item in self.tree.get_children(""): - self.tree.delete(item) - with self.folders_dict_lock: - self.folders.clear() - - with self.files_dict_lock: - items = list(self.files_dict.items()) - for full_path, info in items: - file_name = info.get("file_name") - if not file_name: - continue - if not self.filter_file_type(file_name): - continue - self._add_file_to_tree( - info.get("path") or "", - info.get("url") or "", - file_name, - info.get("size"), - info.get("file_type"), - full_path, - from_cache=True, - ) - after = len(self._all_tree_items()) - self._debug( - f"apply_filters files_dict={len(items)} visible_before={before} visible_after={after}" - ) - - def clear_scan_results(self) -> None: - if self.is_scanning: - return - for item in self.tree.get_children(""): - self.tree.delete(item) - with self.files_dict_lock: - self.files_dict.clear() - with self.folders_dict_lock: - self.folders.clear() - for widget in self.filters_container.winfo_children(): - widget.destroy() - self.file_types.clear() - self.file_type_counts.clear() - self.file_type_widgets.clear() - self.checked_items.clear() - self.progress_var.set(0) - self.progress_label.configure(text="") - self._set_status("Ready", "success") - - def start_scan(self) -> None: - url = self.url_var.get().strip() - if self.is_scanning: - self.backend.should_stop = True - self.backend.should_stop = True - self._set_status("Stopping", "warning") - return - if not url: - self.notify_error("Error", "Please enter a URL") - return - - self.backend.should_stop = False - self.backend.should_stop = False - self.search_var.set("") - self.full_tree_backup.clear() - try: - self.download_path = default_download_folder(url, os.getcwd()) - except Exception: - self.download_path = os.path.join(os.getcwd(), "downloads") - - t = threading.Thread(target=self.backend.scan_website, args=(url,), daemon=True) - t.start() - - def _ensure_download_item(self, file_path: str, display_name: str): - item = self.download_items.get(file_path) - if item: - return item["cancel_event"] - - row = ttkb.Frame(self.downloads_container) - row.pack(fill=X, pady=3) - name = ttkb.Label(row, text=display_name) - name.pack(side=LEFT, padx=(0, 8)) - bar_var = tk.DoubleVar(value=0) - bar = ttkb.Progressbar(row, variable=bar_var, maximum=100, bootstyle="info") - bar.pack(side=LEFT, fill=X, expand=True) - status = ttkb.Label(row, text="Queued") - status.pack(side=LEFT, padx=8) - cancel_event = threading.Event() - - def do_cancel(): - cancel_event.set() - try: - status.configure(text="Canceling...") - except tk.TclError: - pass - - cancel_btn = ttkb.Button(row, text="Cancel", bootstyle="danger", width=8, command=do_cancel) - cancel_btn.pack(side=RIGHT, padx=(8, 0)) - - self.download_items[file_path] = { - "frame": row, - "bar_var": bar_var, - "status": status, - "cancel_event": cancel_event, - } - return cancel_event - - def update_progress(self, file_path, file_name, progress): - self._run_on_ui_thread(self._update_progress_ui, file_path, file_name, progress) - - def _update_progress_ui(self, file_path, file_name, progress): - item = self.download_items.get(file_path) - if item: - try: - item["bar_var"].set(progress) - item["status"].configure(text=f"Downloading {progress:.1f}%") - except tk.TclError: - pass - return - - self.progress_var.set(progress) - self.progress_label.configure(text=f"Downloading: {file_name} ({progress:.1f}%)") - - def update_download_status(self, file_path, status_text): - self._run_on_ui_thread(self._update_download_status_ui, file_path, status_text) - - def _update_download_status_ui(self, file_path, status_text): - item = self.download_items.get(file_path) - if not item: - return - try: - item["status"].configure(text=status_text) - except tk.TclError: - pass - - def on_downloads_finished(self, completed, total): - self._run_on_ui_thread(self.downloads_completed, completed, total) - - def downloads_completed(self, completed, total): - self.pause_btn.configure(state="disabled") - self.progress_var.set(0) - self.progress_label.configure(text=f"Downloads: {completed}/{total}") - self._set_status("Ready", "success") - - def download_selected(self) -> None: - if not self.download_path: - url = self.url_var.get().strip() - if url: - try: - self.download_path = default_download_folder(url, os.getcwd()) - except Exception: - self.download_path = os.path.join(os.getcwd(), "downloads") - else: - self.download_path = os.path.join(os.getcwd(), "downloads") - - os.makedirs(self.download_path, exist_ok=True) - - selected = [] - with self.files_dict_lock: - for full_path in sorted(self.checked_items): - info = self.files_dict.get(full_path) - if info: - selected.append((full_path, info)) - if not selected: - selected_item_ids = list(self.tree.selection()) - for item_id in selected_item_ids: - try: - tags = self.tree.item(item_id, "tags") - except tk.TclError: - continue - if "file" not in tags: - continue - full_path = self.tree.set(item_id, "full_path") - if not full_path: - continue - info = self.files_dict.get(full_path) - if info: - selected.append((full_path, info)) - - if not selected: - self.notify_info("Info", "No selected files to download") - return - - self.pause_btn.configure(state="normal") - futures = [] - for full_path, info in selected: - url = info.get("url") - file_name = info.get("file_name") - if not url or not file_name: - continue - - safe_name = sanitize_filename(file_name) - file_path = safe_join(self.download_path, [safe_name]) - - cancel_event = self._ensure_download_item(file_path, safe_name) - futures.append(self.executor.submit(self.backend.download_file, url, file_path, safe_name, cancel_event)) - - monitor = threading.Thread(target=self.backend.monitor_downloads, args=(futures,), daemon=True) - monitor.start() - - def apply_search_filter(self) -> None: - term = (self.search_var.get() or "").strip().lower() - if not term: - if self.full_tree_backup: - self._restore_full_tree() - return - - if not self.full_tree_backup: - self._backup_full_tree() - self._filter_tree_by_term(term) - - def on_search_filter_changed(self, *_): - self.apply_search_filter() - - def _backup_full_tree(self) -> None: - self.full_tree_backup.clear() - for item in self._all_tree_items(): - self.full_tree_backup[item] = { - "parent": self.tree.parent(item), - "index": self.tree.index(item), - "text": self.tree.item(item, "text"), - "values": self.tree.item(item, "values"), - "tags": self.tree.item(item, "tags"), - "open": self.tree.item(item, "open"), - } - - def _restore_full_tree(self) -> None: - backup = dict(self.full_tree_backup) - self.full_tree_backup.clear() - for item in self.tree.get_children(""): - self.tree.delete(item) - with self.folders_dict_lock: - self.folders.clear() - - items_sorted = sorted(backup.items(), key=lambda kv: (kv[1]["parent"], kv[1]["index"])) - id_map = {} - for old_id, data in items_sorted: - parent_old = data["parent"] - parent_new = id_map.get(parent_old, "") if parent_old else "" - new_id = self.tree.insert(parent_new, "end", text=data["text"], values=data["values"], tags=data["tags"]) - self.tree.item(new_id, open=data["open"]) - id_map[old_id] = new_id - - def _filter_tree_by_term(self, term: str) -> None: - for item in self._all_tree_items(): - text = (self.tree.item(item, "text") or "").lower() - if term in text: - continue - try: - self.tree.detach(item) - except tk.TclError: - pass - - def _all_tree_items(self): - out = [] - stack = list(self.tree.get_children("")) - while stack: - item = stack.pop() - out.append(item) - stack.extend(self.tree.get_children(item)) - return out - - def get_all_tree_items(self, parent=""): - if parent == "": - return self._all_tree_items() - out = [] - for child in self.tree.get_children(parent): - out.append(child) - out.extend(self.get_all_tree_items(child)) - return out - - def _get_ancestors(self, item): - ancestors = [] - try: - parent = self.tree.parent(item) - while parent: - ancestors.append(parent) - parent = self.tree.parent(parent) - except tk.TclError: - return [] - return ancestors - - def _strip_checkmark(self, text: str) -> str: - if text.startswith(self.checkbox_checked): - return text[len(self.checkbox_checked) :] - return text - - def _file_icon_and_group(self, file_name: str, file_type: str | None): - ext = normalize_extension(file_name) - mime = (file_type or "").lower() - if ext in (".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp", ".ico"): - return "🖼", "image" - if ext in (".md", ".txt", ".pdf", ".doc", ".docx", ".rtf"): - return "📄", "document" - if ext in (".zip", ".rar", ".7z", ".tar", ".gz", ".bz2", ".xz"): - return "🗜", "archive" - if ext in ( - ".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs", ".java", ".c", ".cpp", - ".h", ".hpp", ".json", ".yaml", ".yml", ".toml", ".xml", ".html", ".css", ".sh", - ): - return "💻", "code" - if "text/" in mime: - return "📄", "text" - if "image/" in mime: - return "🖼", "image" - if "audio/" in mime: - return "🎵", "audio" - if "video/" in mime: - return "🎬", "video" - return "📦", "binary" - - def _set_item_checked_visual(self, item: str, checked: bool) -> None: - try: - text = self.tree.item(item, "text") - except tk.TclError: - return - raw = self._strip_checkmark(text or "") - display = f"{self.checkbox_checked}{raw}" if checked else raw - try: - self.tree.item(item, text=display) - except tk.TclError: - return - - def _focused_tree_item(self): - try: - item = self.tree.focus() - if item and self.tree.exists(item): - return item - selected = self.tree.selection() - if selected: - return selected[0] - except tk.TclError: - return "" - return "" - - def toggle_check(self, item, force_check=None): - try: - if not self.tree.exists(item): - return - tags = list(self.tree.item(item, "tags")) - except tk.TclError: - return - - is_checked = "checked" in tags - new_checked = (not is_checked) if force_check is None else bool(force_check) - - if "file" in tags: - full_path = self.tree.set(item, "full_path") - if full_path: - if new_checked: - self.checked_items.add(full_path) - else: - self.checked_items.discard(full_path) - - if new_checked and "checked" not in tags: - tags.append("checked") - if (not new_checked) and "checked" in tags: - tags.remove("checked") - self.tree.item(item, tags=tuple(tags)) - self._set_item_checked_visual(item, new_checked) - - if "folder" in tags: - for child in self.tree.get_children(item): - self.toggle_check(child, force_check=new_checked) - - def on_tree_click(self, event): - try: - item = self.tree.identify("item", event.x, event.y) - if not item: - return - self.drag_anchor_item = item - # Let Treeview default bindings handle selection semantics (shift/cmd drag). - self.tree.focus(item) - element = self.tree.identify_element(event.x, event.y) - if "indicator" in element: - return - # Do not toggle check state when modifier-assisted multi-selection is active. - modifier_mask = 0x0001 | 0x0004 | 0x0008 | 0x0010 | 0x0080 - if event.state & modifier_mask: - return - if self.tree.identify_column(event.x) == "#0": - self.toggle_check(item) - except tk.TclError: - return - - def on_tree_drag_select(self, event): - try: - if not self.drag_anchor_item: - return - target = self.tree.identify_row(event.y) - if not target: - return - items = self._all_tree_items() - if self.drag_anchor_item not in items or target not in items: - return - a = items.index(self.drag_anchor_item) - b = items.index(target) - lo, hi = (a, b) if a <= b else (b, a) - self.tree.selection_set(items[lo : hi + 1]) - except tk.TclError: - return - - def on_tree_space(self, _event=None): - item = self._focused_tree_item() - if item: - self.toggle_check(item) - return "break" - return None - - def on_tree_enter(self, _event=None): - item = self._focused_tree_item() - if not item: - return None - try: - tags = self.tree.item(item, "tags") - if "folder" in tags: - self.tree.item(item, open=not bool(self.tree.item(item, "open"))) - else: - self.toggle_check(item) - except tk.TclError: - return None - return "break" - - def show_context_menu(self, event): - try: - item = self.tree.identify_row(event.y) - if item: - self.tree.focus(item) - self.tree.selection_set(item) - self.context_menu.tk_popup(event.x_root, event.y_root) - finally: - self.context_menu.grab_release() - return "break" - - def select_all(self): - for item in self._all_tree_items(): - self.toggle_check(item, force_check=True) - - def deselect_all(self): - for item in self._all_tree_items(): - self.toggle_check(item, force_check=False) - - def expand_all(self, parent=""): - for item in self.tree.get_children(parent): - self.tree.item(item, open=True) - self.expand_all(item) - - def collapse_all(self, parent=""): - for item in self.tree.get_children(parent): - self.tree.item(item, open=False) - self.collapse_all(item) - - def select_all_types(self): - for var in self.file_types.values(): - var.set(True) - self.apply_filters() - - def deselect_all_types(self): - for var in self.file_types.values(): - var.set(False) - self.apply_filters() - - def toggle_panels(self): - if self.panels_visible: - self.panels_notebook.grid_remove() - self.panels_visible = False - self.toggle_panels_btn.configure(text="Show Panels") - else: - self.panels_notebook.grid() - self.panels_visible = True - self.toggle_panels_btn.configure(text="Hide Panels") - - def _on_tree_select_all(self, _event=None): - self.select_all() - return "break" - - def on_tree_select_all(self, _event=None): - return self._on_tree_select_all(_event) - - def _safe_create_folder_and_add_file( - self, dir_path, url, file_name, size, file_type, full_path - ): - self._add_file_to_tree(dir_path, url, file_name, size, file_type, full_path) - - def focus_search(self, _event=None): - try: - self.search_entry.focus_set() - except tk.TclError: - pass - - def focus_logs(self, _event=None): - try: - if self.panels_notebook is not None and self.logs_tab is not None: - self.panels_notebook.select(self.logs_tab) - self.log_text.focus_set() - except (AttributeError, tk.TclError): - pass - - def clear_search(self, _event=None): - self.search_var.set("") - - def sort_tree(self, col): - try: - items = [(self.tree.set(i, col), i) for i in self.tree.get_children("")] - items.sort(reverse=self.sort_reverse) - for index, (_, i) in enumerate(items): - self.tree.move(i, "", index) - self.sort_reverse = not self.sort_reverse - except tk.TclError: - pass - - def on_closing(self): - try: - self.backend.should_stop = True - self.executor.shutdown(wait=False, cancel_futures=True) - except Exception: - pass - try: - self.window.destroy() - except tk.TclError: - pass - - def run(self) -> None: - if self._ui_smoke: - try: - for _ in range(3): - self.window.update_idletasks() - self.window.update() - finally: - try: - self.window.destroy() - except tk.TclError: - pass - return - self.window.mainloop() diff --git a/uv.lock b/uv.lock index ec43126..bad854e 100644 --- a/uv.lock +++ b/uv.lock @@ -140,12 +140,11 @@ wheels = [ [[package]] name = "index-ripper" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "beautifulsoup4" }, { name = "customtkinter" }, { name = "requests" }, - { name = "ttkbootstrap" }, { name = "urllib3" }, ] @@ -160,7 +159,6 @@ requires-dist = [ { name = "customtkinter", specifier = ">=5.2.0,<6.0.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, { name = "requests", specifier = ">=2.31.0,<3.0.0" }, - { name = "ttkbootstrap", specifier = ">=1.10.1,<2.0.0" }, { name = "urllib3", specifier = ">=2.0.0,<3.0.0" }, ] provides-extras = ["dev"] @@ -183,93 +181,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366 }, ] -[[package]] -name = "pillow" -version = "12.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084 }, - { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866 }, - { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148 }, - { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007 }, - { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418 }, - { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590 }, - { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655 }, - { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286 }, - { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663 }, - { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448 }, - { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651 }, - { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803 }, - { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601 }, - { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995 }, - { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012 }, - { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638 }, - { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540 }, - { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613 }, - { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745 }, - { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823 }, - { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367 }, - { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811 }, - { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689 }, - { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535 }, - { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364 }, - { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561 }, - { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460 }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698 }, - { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706 }, - { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621 }, - { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069 }, - { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040 }, - { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523 }, - { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552 }, - { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108 }, - { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712 }, - { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880 }, - { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616 }, - { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008 }, - { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226 }, - { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136 }, - { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129 }, - { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807 }, - { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954 }, - { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441 }, - { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383 }, - { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104 }, - { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652 }, - { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823 }, - { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143 }, - { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254 }, - { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499 }, - { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137 }, - { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721 }, - { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798 }, - { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315 }, - { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360 }, - { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438 }, - { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503 }, - { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748 }, - { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314 }, - { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612 }, - { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567 }, - { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951 }, - { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769 }, - { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358 }, - { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558 }, - { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028 }, - { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940 }, - { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736 }, - { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894 }, - { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446 }, - { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606 }, - { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321 }, - { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579 }, - { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094 }, - { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850 }, - { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343 }, - { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880 }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -328,18 +239,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016 }, ] -[[package]] -name = "ttkbootstrap" -version = "1.20.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pillow" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/fd/b2c7378cc73989d0a9da5e9c99428cab20cdd6c2f34fd54c08caea1b1bdc/ttkbootstrap-1.20.2.tar.gz", hash = "sha256:e8408ff4cffd7eb2767d7b33c3882d59c1bdf37cfeba29d9a3d45f486d370c7a", size = 175346 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/cf/812a67729004c68ae58621c5a75ea242cd016826d033bbbd66e6e051553b/ttkbootstrap-1.20.2-py3-none-any.whl", hash = "sha256:6b3f25aa173c09423474ac31eeb2d766cf0f9514c8debe24da8d1f062e4afdcf", size = 190439 }, -] - [[package]] name = "typing-extensions" version = "4.15.0" From 3b8ee006b549193cae09c31f61b23ecf1c8f732d Mon Sep 17 00:00:00 2001 From: guan4tou2 Date: Sun, 22 Mar 2026 18:44:29 +0800 Subject: [PATCH 55/55] feat: add Electron rewrite with React, TypeScript, and modern UI New Electron desktop app in electron-app/ with full feature parity: Architecture: - Main process: scanner (cheerio), downloader (Node http/https + p-queue), task queue - Renderer: React 19 + TypeScript + Zustand + Tailwind CSS + shadcn/ui - IPC bridge with typed channels, preload contextBridge Features: - Split-panel UI: file tree (left) + downloads/logs/preview (right) - Virtual-scrolled file tree with @tanstack/react-virtual - Multi-site task queue with tab switching - File type filtering with checkbox chips - Search filtering across file names and paths - Shift+click range selection - Sort by name, size, or type - Per-file download progress with cancel/retry - Scan/download pause/resume - File preview (images + text) via double-click - Indeterminate progress during URL discovery - Toast notifications (sonner) - Dark mode with Slate palette - Thin overlay scrollbars - Custom right-click context menu - Keyboard shortcuts: Ctrl+A, Ctrl+F, Escape Build: electron-vite + electron-builder (Windows/macOS/Linux) Tests: 41 tests pass (vitest) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plans/2026-03-22-electron-rewrite.md | 1298 ++ .../plans/2026-03-22-project-restructure.md | 665 + .../2026-03-22-electron-rewrite-design.md | 327 + electron-app/.gitignore | 5 + electron-app/components.json | 21 + electron-app/electron-builder.yml | 21 + electron-app/electron.vite.config.ts | 23 + electron-app/package-lock.json | 11895 ++++++++++++++++ electron-app/package.json | 60 + electron-app/resources/icon.png | Bin 0 -> 932286 bytes .../src/main/__tests__/scanner.test.ts | 149 + electron-app/src/main/__tests__/utils.test.ts | 155 + electron-app/src/main/downloader.ts | 244 + electron-app/src/main/index.ts | 58 + electron-app/src/main/ipc.ts | 242 + electron-app/src/main/scanner.ts | 384 + electron-app/src/main/task-queue.ts | 43 + electron-app/src/main/utils.ts | 82 + electron-app/src/preload/index.d.ts | 8 + electron-app/src/preload/index.ts | 63 + electron-app/src/renderer/index.html | 16 + electron-app/src/renderer/src/App.tsx | 96 + electron-app/src/renderer/src/assets/main.css | 92 + .../renderer/src/components/DownloadCard.tsx | 84 + .../renderer/src/components/DownloadsList.tsx | 91 + .../src/renderer/src/components/FileTree.tsx | 197 + .../renderer/src/components/FileTreeRow.tsx | 83 + .../src/renderer/src/components/LogsPanel.tsx | 58 + .../renderer/src/components/PreviewPanel.tsx | 151 + .../src/components/ProgressSection.tsx | 86 + .../renderer/src/components/RightPanel.tsx | 39 + .../src/renderer/src/components/SearchBar.tsx | 67 + .../renderer/src/components/SplitPanel.tsx | 78 + .../src/renderer/src/components/StatusBar.tsx | 25 + .../src/renderer/src/components/TaskTabs.tsx | 70 + .../src/renderer/src/components/Toolbar.tsx | 216 + .../renderer/src/components/TypeFilters.tsx | 83 + .../src/renderer/src/components/ui/button.tsx | 64 + .../renderer/src/components/ui/checkbox.tsx | 32 + .../src/components/ui/context-menu.tsx | 252 + .../renderer/src/components/ui/progress.tsx | 29 + .../src/components/ui/scroll-area.tsx | 56 + .../src/renderer/src/components/ui/sonner.tsx | 8 + .../src/renderer/src/components/ui/tabs.tsx | 89 + .../src/renderer/src/hooks/useFileTree.ts | 326 + electron-app/src/renderer/src/hooks/useIpc.ts | 48 + .../src/renderer/src/hooks/useIpcListeners.ts | 168 + electron-app/src/renderer/src/lib/utils.ts | 6 + electron-app/src/renderer/src/main.tsx | 11 + .../src/renderer/src/stores/task-store.ts | 232 + electron-app/src/shared/icons.ts | 31 + electron-app/src/shared/types.ts | 42 + electron-app/tsconfig.json | 11 + electron-app/tsconfig.node.json | 16 + electron-app/tsconfig.web.json | 23 + electron-app/vitest.config.ts | 8 + 56 files changed, 18727 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-22-electron-rewrite.md create mode 100644 docs/superpowers/plans/2026-03-22-project-restructure.md create mode 100644 docs/superpowers/specs/2026-03-22-electron-rewrite-design.md create mode 100644 electron-app/.gitignore create mode 100644 electron-app/components.json create mode 100644 electron-app/electron-builder.yml create mode 100644 electron-app/electron.vite.config.ts create mode 100644 electron-app/package-lock.json create mode 100644 electron-app/package.json create mode 100644 electron-app/resources/icon.png create mode 100644 electron-app/src/main/__tests__/scanner.test.ts create mode 100644 electron-app/src/main/__tests__/utils.test.ts create mode 100644 electron-app/src/main/downloader.ts create mode 100644 electron-app/src/main/index.ts create mode 100644 electron-app/src/main/ipc.ts create mode 100644 electron-app/src/main/scanner.ts create mode 100644 electron-app/src/main/task-queue.ts create mode 100644 electron-app/src/main/utils.ts create mode 100644 electron-app/src/preload/index.d.ts create mode 100644 electron-app/src/preload/index.ts create mode 100644 electron-app/src/renderer/index.html create mode 100644 electron-app/src/renderer/src/App.tsx create mode 100644 electron-app/src/renderer/src/assets/main.css create mode 100644 electron-app/src/renderer/src/components/DownloadCard.tsx create mode 100644 electron-app/src/renderer/src/components/DownloadsList.tsx create mode 100644 electron-app/src/renderer/src/components/FileTree.tsx create mode 100644 electron-app/src/renderer/src/components/FileTreeRow.tsx create mode 100644 electron-app/src/renderer/src/components/LogsPanel.tsx create mode 100644 electron-app/src/renderer/src/components/PreviewPanel.tsx create mode 100644 electron-app/src/renderer/src/components/ProgressSection.tsx create mode 100644 electron-app/src/renderer/src/components/RightPanel.tsx create mode 100644 electron-app/src/renderer/src/components/SearchBar.tsx create mode 100644 electron-app/src/renderer/src/components/SplitPanel.tsx create mode 100644 electron-app/src/renderer/src/components/StatusBar.tsx create mode 100644 electron-app/src/renderer/src/components/TaskTabs.tsx create mode 100644 electron-app/src/renderer/src/components/Toolbar.tsx create mode 100644 electron-app/src/renderer/src/components/TypeFilters.tsx create mode 100644 electron-app/src/renderer/src/components/ui/button.tsx create mode 100644 electron-app/src/renderer/src/components/ui/checkbox.tsx create mode 100644 electron-app/src/renderer/src/components/ui/context-menu.tsx create mode 100644 electron-app/src/renderer/src/components/ui/progress.tsx create mode 100644 electron-app/src/renderer/src/components/ui/scroll-area.tsx create mode 100644 electron-app/src/renderer/src/components/ui/sonner.tsx create mode 100644 electron-app/src/renderer/src/components/ui/tabs.tsx create mode 100644 electron-app/src/renderer/src/hooks/useFileTree.ts create mode 100644 electron-app/src/renderer/src/hooks/useIpc.ts create mode 100644 electron-app/src/renderer/src/hooks/useIpcListeners.ts create mode 100644 electron-app/src/renderer/src/lib/utils.ts create mode 100644 electron-app/src/renderer/src/main.tsx create mode 100644 electron-app/src/renderer/src/stores/task-store.ts create mode 100644 electron-app/src/shared/icons.ts create mode 100644 electron-app/src/shared/types.ts create mode 100644 electron-app/tsconfig.json create mode 100644 electron-app/tsconfig.node.json create mode 100644 electron-app/tsconfig.web.json create mode 100644 electron-app/vitest.config.ts diff --git a/docs/superpowers/plans/2026-03-22-electron-rewrite.md b/docs/superpowers/plans/2026-03-22-electron-rewrite.md new file mode 100644 index 0000000..1b29b84 --- /dev/null +++ b/docs/superpowers/plans/2026-03-22-electron-rewrite.md @@ -0,0 +1,1298 @@ +# Index Ripper Electron Rewrite — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a new Electron desktop app (React + TypeScript + Node.js) that replaces the Python version with full feature parity plus multi-site task queue support. + +**Architecture:** Electron main process handles scanning and downloads (cheerio + Electron net + p-queue). React renderer with Zustand stores displays a split-panel UI. IPC bridge connects them via typed channels. All shared types in `src/shared/`. + +**Tech Stack:** Electron 35+, electron-vite, React 19, TypeScript, Tailwind CSS v4, shadcn/ui, Zustand, cheerio, p-queue, @tanstack/react-virtual, electron-store, electron-builder + +**Spec:** `docs/superpowers/specs/2026-03-22-electron-rewrite-design.md` + +--- + +## Task 1: Project Scaffolding + +**Files:** +- Create: `electron-app/package.json` +- Create: `electron-app/electron.vite.config.ts` +- Create: `electron-app/tsconfig.json` +- Create: `electron-app/tsconfig.node.json` +- Create: `electron-app/tsconfig.web.json` +- Create: `electron-app/src/main/index.ts` +- Create: `electron-app/src/preload/index.ts` +- Create: `electron-app/src/renderer/index.html` +- Create: `electron-app/src/renderer/main.tsx` +- Create: `electron-app/src/renderer/App.tsx` +- Create: `electron-app/electron-builder.yml` +- Create: `electron-app/resources/icon.png` + +- [ ] **Step 1: Scaffold project with electron-vite** + +```bash +cd /Users/guantou/Desktop/Index-Ripper +npm create @quick-start/electron@latest electron-app -- --template react-ts +``` + +If the interactive scaffolding fails, create manually. The result should be a working electron-vite project with React + TypeScript. + +- [ ] **Step 2: Install core dependencies** + +```bash +cd /Users/guantou/Desktop/Index-Ripper/electron-app +npm install zustand cheerio p-queue electron-store +npm install -D tailwindcss @tailwindcss/vite @tanstack/react-virtual vitest +``` + +- [ ] **Step 3: Configure Tailwind CSS v4** + +In `electron-app/src/renderer/main.css`: +```css +@import "tailwindcss"; +``` + +In `electron-app/electron.vite.config.ts`, add Tailwind plugin to renderer config: +```typescript +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + // ... + renderer: { + plugins: [react(), tailwindcss()], + }, +}) +``` + +- [ ] **Step 4: Set up shadcn/ui** + +```bash +cd /Users/guantou/Desktop/Index-Ripper/electron-app +npx shadcn@latest init +``` + +Configure with: TypeScript, New York style, Slate base color, CSS variables. + +Add commonly needed components: +```bash +npx shadcn@latest add button tabs checkbox scroll-area context-menu progress sonner +``` + +- [ ] **Step 5: Create minimal App.tsx** + +```tsx +export default function App(): React.ReactElement { + return ( +
+

Index Ripper

+
+ ) +} +``` + +- [ ] **Step 6: Verify app launches** + +```bash +cd /Users/guantou/Desktop/Index-Ripper/electron-app +npm run dev +``` + +Expected: Electron window opens showing "Index Ripper" text on dark background. + +- [ ] **Step 7: Copy app icon** + +```bash +cp /Users/guantou/Desktop/Index-Ripper/app.png /Users/guantou/Desktop/Index-Ripper/electron-app/resources/icon.png +``` + +- [ ] **Step 8: Commit** + +```bash +cd /Users/guantou/Desktop/Index-Ripper +git add electron-app/ +git commit -m "feat(electron): scaffold project with electron-vite, React, TypeScript, Tailwind, shadcn/ui" +``` + +--- + +## Task 2: Shared Types & Utilities + +**Files:** +- Create: `electron-app/src/shared/types.ts` +- Create: `electron-app/src/shared/icons.ts` +- Create: `electron-app/src/main/utils.ts` +- Test: `electron-app/src/main/__tests__/utils.test.ts` + +- [ ] **Step 1: Create shared types** + +```typescript +// electron-app/src/shared/types.ts + +export interface TreeNode { + id: string + parentId: string + name: string + kind: 'folder' | 'file' + url: string + fullPath: string + size: string + fileType: string + iconGroup: string + checked: boolean + expanded: boolean + hidden: boolean + children: string[] +} + +export interface Task { + id: string + url: string + status: 'idle' | 'scanning' | 'scanned' | 'downloading' | 'done' | 'error' | 'cancelled' + nodes: Record + roots: string[] + checkedFiles: string[] + downloads: Record + scanProgress: { scanned: number; total: number } + downloadPath: string +} + +export interface DownloadItem { + id: string + fileName: string + url: string + destPath: string + status: 'queued' | 'downloading' | 'paused' | 'completed' | 'failed' | 'cancelled' + progress: number + speed: number + totalSize: number + downloadedSize: number +} + +export type ScanStatus = Task['status'] +export type DownloadStatus = DownloadItem['status'] +``` + +- [ ] **Step 2: Create icon mapping** + +```typescript +// electron-app/src/shared/icons.ts +import { extname } from 'path' + +const IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.ico'] +const DOC_EXTS = ['.md', '.txt', '.pdf', '.doc', '.docx', '.rtf'] +const ARCHIVE_EXTS = ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz'] +const CODE_EXTS = [ + '.py', '.js', '.ts', '.tsx', '.jsx', '.go', '.rs', '.java', '.c', '.cpp', + '.h', '.hpp', '.json', '.yaml', '.yml', '.toml', '.xml', '.html', '.css', '.sh', +] + +export function getIconGroup(fileName: string, mimeType: string): string { + const ext = extname(fileName).toLowerCase() + if (IMAGE_EXTS.includes(ext)) return 'image' + if (DOC_EXTS.includes(ext)) return 'document' + if (ARCHIVE_EXTS.includes(ext)) return 'archive' + if (CODE_EXTS.includes(ext)) return 'code' + if (mimeType.includes('audio/')) return 'audio' + if (mimeType.includes('video/')) return 'video' + if (mimeType.includes('text/')) return 'text' + if (mimeType.includes('image/')) return 'image' + return 'binary' +} + +export const EMOJI_ICONS: Record = { + folder: '📁', image: '🖼️', document: '📄', archive: '🗜️', + code: '💻', audio: '🎵', video: '🎬', text: '📝', binary: '⚙️', +} +``` + +- [ ] **Step 3: Write utils tests** + +```typescript +// electron-app/src/main/__tests__/utils.test.ts +import { describe, it, expect } from 'vitest' +import { sanitizePathSegment, sanitizeFilename, isUrlInScope, safeJoin, defaultDownloadFolder } from '../utils' + +describe('sanitizePathSegment', () => { + it('blocks traversal patterns', () => { + expect(sanitizePathSegment('..')).toBe('_') + expect(sanitizePathSegment('a/b')).toBe('a_b') + expect(sanitizePathSegment('a\\b')).toBe('a_b') + }) + it('strips Windows-illegal chars', () => { + expect(sanitizePathSegment('<>:"|?*')).toBe('_______') + }) + it('returns _ for empty', () => { + expect(sanitizePathSegment('')).toBe('_') + }) +}) + +describe('isUrlInScope', () => { + it('accepts same origin and path prefix', () => { + expect(isUrlInScope('https://example.com/a/b/', 'https://example.com/a/b/file.txt')).toBe(true) + }) + it('rejects different host', () => { + expect(isUrlInScope('https://example.com/a/', 'https://evil.com/a/file.txt')).toBe(false) + }) + it('rejects different scheme', () => { + expect(isUrlInScope('https://example.com/a/', 'http://example.com/a/file.txt')).toBe(false) + }) +}) + +describe('safeJoin', () => { + it('rejects path escape', () => { + expect(() => safeJoin('/tmp/root', ['..', 'outside.txt'])).toThrow() + }) + it('accepts normal path', () => { + const result = safeJoin('/tmp/root', ['sub', 'file.txt']) + expect(result).toContain('root') + expect(result).toContain('file.txt') + }) +}) + +describe('defaultDownloadFolder', () => { + it('derives folder from hostname', () => { + const result = defaultDownloadFolder('https://example.com/files/') + expect(result).toContain('example.com') + }) +}) +``` + +- [ ] **Step 4: Run tests to verify they fail** + +```bash +cd /Users/guantou/Desktop/Index-Ripper/electron-app +npx vitest run src/main/__tests__/utils.test.ts +``` + +Expected: FAIL — module not found + +- [ ] **Step 5: Implement utils** + +```typescript +// electron-app/src/main/utils.ts +import { resolve, normalize, join } from 'path' + +const WINDOWS_INVALID = /[<>:"/\\|?*]/g + +export function sanitizePathSegment(name: string): string { + let value = decodeURIComponent(String(name || '')) + value = value.replace(/[/\\]/g, '_') + value = value.replace(WINDOWS_INVALID, '_') + value = value.trim().replace(/[. ]+$/, '') + if (!value || value === '.' || value === '..') return '_' + return value +} + +export function sanitizeFilename(name: string): string { + return sanitizePathSegment(name) +} + +export function isUrlInScope(baseUrl: string, candidateUrl: string): boolean { + let base: URL, candidate: URL + try { + base = new URL(baseUrl) + candidate = new URL(candidateUrl) + } catch { + return false + } + if (base.protocol !== candidate.protocol) return false + if (base.hostname.toLowerCase() !== candidate.hostname.toLowerCase()) return false + if (base.port !== candidate.port) return false + const basePath = normalize(base.pathname).replace(/\/$/, '') + const candPath = normalize(candidate.pathname) + if (basePath === '/' || basePath === '.') return true + return candPath === basePath || candPath.startsWith(basePath + '/') +} + +export function safeJoin(root: string, parts: string[]): string { + const rootReal = resolve(root) + const target = resolve(join(rootReal, ...parts)) + if (!target.startsWith(rootReal)) { + throw new Error('Path escapes root') + } + return target +} + +export function defaultDownloadFolder(url: string): string { + try { + const hostname = new URL(url).hostname.replace(/:/g, '_') + return join(require('os').homedir(), 'Downloads', hostname || 'downloads') + } catch { + return join(require('os').homedir(), 'Downloads', 'downloads') + } +} + +export function normalizeExtension(fileName: string): string { + const dot = fileName.lastIndexOf('.') + if (dot < 0) return '.(No Extension)' + return fileName.slice(dot).toLowerCase() +} +``` + +- [ ] **Step 6: Run tests to verify they pass** + +```bash +cd /Users/guantou/Desktop/Index-Ripper/electron-app +npx vitest run src/main/__tests__/utils.test.ts +``` + +Expected: All tests PASS + +- [ ] **Step 7: Commit** + +```bash +cd /Users/guantou/Desktop/Index-Ripper +git add electron-app/src/shared/ electron-app/src/main/utils.ts electron-app/src/main/__tests__/ +git commit -m "feat(electron): add shared types, icons, and utility functions with tests" +``` + +--- + +## Task 3: Scanner Service + +**Files:** +- Create: `electron-app/src/main/scanner.ts` +- Test: `electron-app/src/main/__tests__/scanner.test.ts` + +- [ ] **Step 1: Write scanner tests** + +```typescript +// electron-app/src/main/__tests__/scanner.test.ts +import { describe, it, expect, vi } from 'vitest' +import { parseDirectoryListing, isDirectoryHref } from '../scanner' + +describe('parseDirectoryListing', () => { + it('extracts links from Index of page', () => { + const html = ` + + sub/ + file.txt + Parent Directory + Name + + ` + const links = parseDirectoryListing(html, 'https://example.com/files/') + expect(links).toContainEqual({ url: 'https://example.com/files/sub/', isDirectory: true }) + expect(links).toContainEqual({ url: 'https://example.com/files/file.txt', isDirectory: false }) + expect(links.find(l => l.url.includes('..'))).toBeUndefined() + expect(links.find(l => l.url.includes('?'))).toBeUndefined() + }) + + it('ignores out-of-scope links', () => { + const html = 'hack' + const links = parseDirectoryListing(html, 'https://example.com/') + expect(links).toHaveLength(0) + }) +}) + +describe('isDirectoryHref', () => { + it('returns true for trailing slash', () => { + expect(isDirectoryHref('sub/')).toBe(true) + }) + it('returns false for files', () => { + expect(isDirectoryHref('file.txt')).toBe(false) + }) +}) +``` + +- [ ] **Step 2: Run tests to verify they fail** + +```bash +cd /Users/guantou/Desktop/Index-Ripper/electron-app +npx vitest run src/main/__tests__/scanner.test.ts +``` + +Expected: FAIL + +- [ ] **Step 3: Implement scanner** + +```typescript +// electron-app/src/main/scanner.ts +import * as cheerio from 'cheerio' +import { net } from 'electron' +import { basename, dirname } from 'path' +import { EventEmitter } from 'events' +import type { TreeNode } from '../shared/types' +import { isUrlInScope } from './utils' +import { getIconGroup } from '../shared/icons' + +export interface ParsedLink { + url: string + isDirectory: boolean +} + +export function isDirectoryHref(href: string): boolean { + return href.endsWith('/') +} + +export function parseDirectoryListing(html: string, pageUrl: string): ParsedLink[] { + const $ = cheerio.load(html) + const links: ParsedLink[] = [] + const seen = new Set() + + $('a').each((_, el) => { + const href = $(el).attr('href') + if (!href || href === '.' || href === '..' || href === '/' || href.startsWith('?')) return + if (href.includes('..')) return + + let fullUrl: string + try { + fullUrl = new URL(href, pageUrl).href + } catch { + return + } + + // Strip query/fragment + const parsed = new URL(fullUrl) + fullUrl = `${parsed.protocol}//${parsed.host}${parsed.pathname}` + + if (!isUrlInScope(pageUrl, fullUrl)) return + if (seen.has(fullUrl)) return + seen.add(fullUrl) + + links.push({ url: fullUrl, isDirectory: isDirectoryHref(href) }) + }) + + return links +} + +export interface ScanCallbacks { + onItem: (node: Partial & { isDirectory: boolean }) => void + onProgress: (scanned: number, total: number) => void + onError: (message: string) => void + onFinished: (stopped: boolean) => void +} + +export class Scanner extends EventEmitter { + private aborted = false + private paused = false + private pausePromise: Promise | null = null + private pauseResolve: (() => void) | null = null + + abort(): void { this.aborted = true } + + pause(): void { + if (this.paused) return + this.paused = true + this.pausePromise = new Promise(r => { this.pauseResolve = r }) + } + + resume(): void { + this.paused = false + this.pauseResolve?.() + this.pausePromise = null + } + + private async waitIfPaused(): Promise { + if (this.pausePromise) await this.pausePromise + } + + async scan(url: string, callbacks: ScanCallbacks): Promise { + try { + // Phase 1: Discover all URLs recursively + const allUrls = await this.discoverUrls(url, callbacks) + if (this.aborted) { callbacks.onFinished(true); return } + + // Phase 2: Process each URL (HEAD requests for files) + let scanned = 0 + const total = allUrls.length + callbacks.onProgress(0, total) + + for (const item of allUrls) { + if (this.aborted) break + await this.waitIfPaused() + + if (item.isDirectory) { + const dirPath = new URL(item.url).pathname.replace(/\/$/, '') + callbacks.onItem({ isDirectory: true, fullPath: dirPath, name: basename(dirPath) || '/', kind: 'folder' }) + } else { + await this.processFile(item.url, callbacks) + } + scanned++ + callbacks.onProgress(scanned, total) + } + + callbacks.onFinished(this.aborted) + } catch (err) { + callbacks.onError(String(err)) + callbacks.onFinished(false) + } + } + + private async discoverUrls( + url: string, + callbacks: ScanCallbacks, + visited = new Set() + ): Promise { + const normalized = url.endsWith('/') ? url : url + '/' + if (visited.has(normalized) || this.aborted) return [] + visited.add(normalized) + + const html = await this.fetchHtml(url) + if (!html) return [] + + const links = parseDirectoryListing(html, url) + const result: ParsedLink[] = [...links] + + for (const link of links) { + if (this.aborted) break + if (link.isDirectory) { + const subLinks = await this.discoverUrls(link.url, callbacks, visited) + result.push(...subLinks) + } + } + + return result + } + + private async fetchHtml(url: string): Promise { + return new Promise((resolve) => { + const request = net.request(url) + let data = '' + request.on('response', (response) => { + response.on('data', (chunk) => { data += chunk.toString() }) + response.on('end', () => resolve(data)) + response.on('error', () => resolve(null)) + }) + request.on('error', () => resolve(null)) + request.end() + }) + } + + private async processFile(url: string, callbacks: ScanCallbacks): Promise { + const parsed = new URL(url) + const fileName = decodeURIComponent(basename(parsed.pathname)) + const dirPath = decodeURIComponent(dirname(parsed.pathname)) + if (!fileName) return + + const fullPath = (dirPath + '/' + fileName).replace(/^\//, '') + + try { + const head = await this.headRequest(url) + const size = head.contentLength + ? `${(head.contentLength / 1024).toFixed(2)} KB` + : 'Unknown' + const fileType = head.contentType || 'Unknown' + const iconGroup = getIconGroup(fileName, fileType) + + callbacks.onItem({ + isDirectory: false, + name: fileName, + kind: 'file', + url, + fullPath, + size, + fileType, + iconGroup, + }) + } catch { + callbacks.onError(`Could not process file ${url}`) + } + } + + private headRequest(url: string): Promise<{ contentLength: number | null; contentType: string }> { + return new Promise((resolve, reject) => { + const request = net.request({ url, method: 'HEAD' }) + request.on('response', (response) => { + resolve({ + contentLength: response.headers['content-length'] + ? parseInt(String(response.headers['content-length']), 10) + : null, + contentType: String(response.headers['content-type'] || ''), + }) + response.on('data', () => {}) // drain + response.on('end', () => {}) + }) + request.on('error', reject) + request.end() + }) + } +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +```bash +cd /Users/guantou/Desktop/Index-Ripper/electron-app +npx vitest run src/main/__tests__/scanner.test.ts +``` + +Expected: PASS (pure parsing tests; Scanner class needs Electron runtime for full test) + +- [ ] **Step 5: Commit** + +```bash +cd /Users/guantou/Desktop/Index-Ripper +git add electron-app/src/main/scanner.ts electron-app/src/main/__tests__/scanner.test.ts +git commit -m "feat(electron): implement scanner service with directory listing parser" +``` + +--- + +## Task 4: Download Manager + +**Files:** +- Create: `electron-app/src/main/downloader.ts` + +- [ ] **Step 1: Implement downloader** + +```typescript +// electron-app/src/main/downloader.ts +import { net } from 'electron' +import { createWriteStream, mkdirSync, unlinkSync, existsSync } from 'fs' +import { dirname } from 'path' +import PQueue from 'p-queue' +import { EventEmitter } from 'events' +import type { DownloadItem } from '../shared/types' + +export interface DownloadCallbacks { + onProgress: (fileId: string, progress: number, speed: number) => void + onStatus: (fileId: string, status: DownloadItem['status']) => void + onFinished: (completed: number, total: number) => void + onLog: (message: string) => void +} + +export class Downloader extends EventEmitter { + private queue: PQueue + private aborted = false + private paused = false + private pausePromise: Promise | null = null + private pauseResolve: (() => void) | null = null + private cancelledFiles = new Set() + + constructor(concurrency = 5) { + super() + this.queue = new PQueue({ concurrency }) + } + + setConcurrency(n: number): void { + this.queue.concurrency = Math.max(1, Math.min(10, n)) + } + + pause(): void { + if (this.paused) return + this.paused = true + this.queue.pause() + this.pausePromise = new Promise(r => { this.pauseResolve = r }) + } + + resume(): void { + this.paused = false + this.queue.start() + this.pauseResolve?.() + this.pausePromise = null + } + + cancelFile(fileId: string): void { + this.cancelledFiles.add(fileId) + } + + cancelAll(): void { + this.aborted = true + this.queue.clear() + } + + async downloadFiles( + files: Array<{ id: string; url: string; destPath: string; fileName: string }>, + callbacks: DownloadCallbacks + ): Promise { + this.aborted = false + this.cancelledFiles.clear() + let completed = 0 + const total = files.length + + const tasks = files.map(file => + this.queue.add(async () => { + if (this.aborted || this.cancelledFiles.has(file.id)) { + callbacks.onStatus(file.id, 'cancelled') + return + } + const success = await this.downloadOne(file, callbacks) + if (success) completed++ + }) + ) + + await Promise.allSettled(tasks) + callbacks.onFinished(completed, total) + } + + private async downloadOne( + file: { id: string; url: string; destPath: string; fileName: string }, + callbacks: DownloadCallbacks, + attempt = 1 + ): Promise { + callbacks.onStatus(file.id, 'downloading') + + return new Promise((resolve) => { + mkdirSync(dirname(file.destPath), { recursive: true }) + + const request = net.request(file.url) + request.on('response', (response) => { + if (response.statusCode && response.statusCode >= 500 && attempt <= 3) { + const delay = Math.pow(2, attempt) * 100 + setTimeout(() => { + this.downloadOne(file, callbacks, attempt + 1).then(resolve) + }, delay) + return + } + + const totalSize = response.headers['content-length'] + ? parseInt(String(response.headers['content-length']), 10) + : 0 + let downloaded = 0 + let lastTime = Date.now() + let lastBytes = 0 + + const stream = createWriteStream(file.destPath) + + response.on('data', (chunk) => { + if (this.aborted || this.cancelledFiles.has(file.id)) { + request.abort() + stream.close() + this.cleanupFile(file.destPath) + callbacks.onStatus(file.id, 'cancelled') + callbacks.onLog(`[Download] Cancelled: ${file.fileName}`) + resolve(false) + return + } + + stream.write(chunk) + downloaded += chunk.length + + const now = Date.now() + const elapsed = (now - lastTime) / 1000 + if (elapsed >= 0.5) { + const speed = (downloaded - lastBytes) / elapsed + lastTime = now + lastBytes = downloaded + const progress = totalSize > 0 ? (downloaded / totalSize) * 100 : 0 + callbacks.onProgress(file.id, progress, speed) + } + }) + + response.on('end', () => { + stream.close() + callbacks.onProgress(file.id, 100, 0) + callbacks.onStatus(file.id, 'completed') + callbacks.onLog(`[Download] Completed: ${file.fileName}`) + resolve(true) + }) + + response.on('error', (err) => { + stream.close() + this.cleanupFile(file.destPath) + callbacks.onStatus(file.id, 'failed') + callbacks.onLog(`[Download] Failed: ${file.fileName} — ${err}`) + resolve(false) + }) + }) + + request.on('error', (err) => { + this.cleanupFile(file.destPath) + callbacks.onStatus(file.id, 'failed') + callbacks.onLog(`[Download] Error: ${file.fileName} — ${err}`) + resolve(false) + }) + + request.end() + }) + } + + private cleanupFile(path: string): void { + try { + if (existsSync(path)) unlinkSync(path) + } catch { /* ignore */ } + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +cd /Users/guantou/Desktop/Index-Ripper +git add electron-app/src/main/downloader.ts +git commit -m "feat(electron): implement download manager with retry, cancel, pause/resume" +``` + +--- + +## Task 5: Task Queue & IPC Bridge + +**Files:** +- Create: `electron-app/src/main/task-queue.ts` +- Create: `electron-app/src/main/ipc.ts` +- Modify: `electron-app/src/main/index.ts` +- Modify: `electron-app/src/preload/index.ts` + +- [ ] **Step 1: Implement task queue** + +```typescript +// electron-app/src/main/task-queue.ts +import { randomUUID } from 'crypto' +import type { Task } from '../shared/types' +import { Scanner } from './scanner' +import { Downloader } from './downloader' +import { defaultDownloadFolder } from './utils' + +export class TaskQueue { + tasks = new Map() + + createTask(url: string): Task { + const id = randomUUID() + const task: Task = { + id, + url, + status: 'idle', + nodes: {}, + roots: [], + checkedFiles: [], + downloads: {}, + scanProgress: { scanned: 0, total: 0 }, + downloadPath: defaultDownloadFolder(url), + } + this.tasks.set(id, { task, scanner: new Scanner(), downloader: new Downloader() }) + return task + } + + getTask(taskId: string) { return this.tasks.get(taskId) } + removeTask(taskId: string) { + const entry = this.tasks.get(taskId) + if (entry) { + entry.scanner.abort() + entry.downloader.cancelAll() + this.tasks.delete(taskId) + } + } + allTasks(): Task[] { return [...this.tasks.values()].map(e => e.task) } +} +``` + +- [ ] **Step 2: Implement IPC handler registration** + +Create `electron-app/src/main/ipc.ts` that registers all IPC handlers listed in the spec. Each handler delegates to TaskQueue, Scanner, or Downloader. Main-to-renderer events use `webContents.send()`. + +Key handlers: +- `scan:start` → creates Scanner, calls `scanner.scan()`, pipes callbacks to renderer via IPC events +- `download:start` → prepares file list, calls `downloader.downloadFiles()` +- `task:create` → creates new task, returns task data +- `dialog:selectFolder` → uses `dialog.showOpenDialog()` +- `download:setWorkers` → calls `downloader.setConcurrency()` + +- [ ] **Step 3: Update preload to expose typed API** + +```typescript +// electron-app/src/preload/index.ts +import { contextBridge, ipcRenderer } from 'electron' + +const api = { + // Renderer → Main + scan: { + start: (taskId: string, url: string) => ipcRenderer.invoke('scan:start', taskId, url), + stop: (taskId: string) => ipcRenderer.invoke('scan:stop', taskId), + pause: (taskId: string) => ipcRenderer.invoke('scan:pause', taskId), + resume: (taskId: string) => ipcRenderer.invoke('scan:resume', taskId), + }, + download: { + start: (taskId: string, files: any[], destPath: string) => + ipcRenderer.invoke('download:start', taskId, files, destPath), + pause: (taskId: string) => ipcRenderer.invoke('download:pause', taskId), + resume: (taskId: string) => ipcRenderer.invoke('download:resume', taskId), + cancel: (taskId: string, fileId: string) => ipcRenderer.invoke('download:cancel', taskId, fileId), + cancelAll: (taskId: string) => ipcRenderer.invoke('download:cancelAll', taskId), + retry: (taskId: string, fileId: string) => ipcRenderer.invoke('download:retry', taskId, fileId), + setWorkers: (count: number) => ipcRenderer.invoke('download:setWorkers', count), + }, + task: { + create: (url: string) => ipcRenderer.invoke('task:create', url), + remove: (taskId: string) => ipcRenderer.invoke('task:remove', taskId), + }, + settings: { + get: () => ipcRenderer.invoke('settings:get'), + set: (data: any) => ipcRenderer.invoke('settings:set', data), + }, + dialog: { + selectFolder: () => ipcRenderer.invoke('dialog:selectFolder'), + }, + // Main → Renderer (event subscriptions) + on: (channel: string, callback: (...args: any[]) => void) => { + const listener = (_event: any, ...args: any[]) => callback(...args) + ipcRenderer.on(channel, listener) + return () => ipcRenderer.removeListener(channel, listener) + }, +} + +contextBridge.exposeInMainWorld('api', api) +``` + +- [ ] **Step 4: Update main/index.ts to register IPC handlers on app ready** + +Wire up `registerIpcHandlers(taskQueue)` call in the `app.whenReady()` block. + +- [ ] **Step 5: Verify app still launches** + +```bash +cd /Users/guantou/Desktop/Index-Ripper/electron-app +npm run dev +``` + +Expected: Electron window opens without errors + +- [ ] **Step 6: Commit** + +```bash +cd /Users/guantou/Desktop/Index-Ripper +git add electron-app/src/main/ electron-app/src/preload/ +git commit -m "feat(electron): implement task queue, IPC bridge, and preload API" +``` + +--- + +## Task 6: Zustand Stores + +**Files:** +- Create: `electron-app/src/renderer/stores/task-store.ts` +- Create: `electron-app/src/renderer/stores/tree-store.ts` +- Create: `electron-app/src/renderer/stores/download-store.ts` +- Create: `electron-app/src/renderer/hooks/useIpc.ts` + +- [ ] **Step 1: Create IPC subscription hook** + +```typescript +// electron-app/src/renderer/hooks/useIpc.ts +import { useEffect } from 'react' + +declare global { + interface Window { api: any } +} + +export function useIpcEvent(channel: string, callback: (...args: any[]) => void): void { + useEffect(() => { + const unsubscribe = window.api.on(channel, callback) + return unsubscribe + }, [channel, callback]) +} +``` + +- [ ] **Step 2: Create task store** + +```typescript +// electron-app/src/renderer/stores/task-store.ts +import { create } from 'zustand' +import type { Task } from '../../shared/types' + +interface TaskStore { + tasks: Record + activeTaskId: string | null + createTask: (task: Task) => void + removeTask: (taskId: string) => void + setActiveTask: (taskId: string) => void + updateTask: (taskId: string, partial: Partial) => void +} + +export const useTaskStore = create((set) => ({ + tasks: {}, + activeTaskId: null, + createTask: (task) => set((state) => ({ + tasks: { ...state.tasks, [task.id]: task }, + activeTaskId: state.activeTaskId ?? task.id, + })), + removeTask: (taskId) => set((state) => { + const { [taskId]: _, ...rest } = state.tasks + const ids = Object.keys(rest) + return { + tasks: rest, + activeTaskId: state.activeTaskId === taskId ? (ids[0] ?? null) : state.activeTaskId, + } + }), + setActiveTask: (taskId) => set({ activeTaskId: taskId }), + updateTask: (taskId, partial) => set((state) => ({ + tasks: { + ...state.tasks, + [taskId]: { ...state.tasks[taskId], ...partial }, + }, + })), +})) +``` + +- [ ] **Step 3: Create tree and download stores** + +Similar pattern — `useTreeStore` manages nodes/roots/checked per task, `useDownloadStore` manages download items per task. Both update from IPC events. + +- [ ] **Step 4: Commit** + +```bash +cd /Users/guantou/Desktop/Index-Ripper +git add electron-app/src/renderer/stores/ electron-app/src/renderer/hooks/ +git commit -m "feat(electron): add Zustand stores for tasks, tree, and downloads" +``` + +--- + +## Task 7: UI Shell — TaskTabs + Toolbar + SplitPanel + +**Files:** +- Create: `electron-app/src/renderer/components/TaskTabs.tsx` +- Create: `electron-app/src/renderer/components/Toolbar.tsx` +- Create: `electron-app/src/renderer/components/SplitPanel.tsx` +- Modify: `electron-app/src/renderer/App.tsx` + +- [ ] **Step 1: Build TaskTabs component** + +Tab bar showing all tasks. Each tab displays shortened hostname. `+` button to create new task. Active tab highlighted. + +- [ ] **Step 2: Build Toolbar component** + +URL input, Scan/Stop button, Download button, thread count dropdown, status indicator (dot + text). + +- [ ] **Step 3: Build SplitPanel component** + +Resizable left/right panels using CSS `resize` or a drag handle. Left panel takes ~40% default width. + +- [ ] **Step 4: Assemble in App.tsx** + +```tsx +export default function App() { + return ( +
+ + + File Tree (coming next)
} + right={
Downloads/Logs (coming next)
} + /> + + ) +} +``` + +- [ ] **Step 5: Verify layout renders correctly** + +```bash +cd /Users/guantou/Desktop/Index-Ripper/electron-app && npm run dev +``` + +Expected: Window shows task tabs, toolbar with URL input, and split panel layout. + +- [ ] **Step 6: Commit** + +```bash +cd /Users/guantou/Desktop/Index-Ripper +git add electron-app/src/renderer/ +git commit -m "feat(electron): build UI shell with TaskTabs, Toolbar, SplitPanel" +``` + +--- + +## Task 8: FileTree Component + +**Files:** +- Create: `electron-app/src/renderer/components/FileTree.tsx` +- Create: `electron-app/src/renderer/components/FileTreeRow.tsx` +- Create: `electron-app/src/renderer/hooks/useFileTree.ts` +- Create: `electron-app/src/renderer/components/SearchBar.tsx` +- Create: `electron-app/src/renderer/components/TypeFilters.tsx` +- Create: `electron-app/src/renderer/components/StatusBar.tsx` + +- [ ] **Step 1: Create useFileTree hook** + +Logic for: flatten tree to visible list (respecting expanded/hidden), toggle check (with folder cascade), expand/collapse, search filter, type filter. + +- [ ] **Step 2: Create FileTreeRow component** + +Single row: 3px accent bar + indent spacer + emoji icon + name + size (files) or chevron (folders). Hover effect, click to toggle check. + +- [ ] **Step 3: Create FileTree component** + +Uses `@tanstack/react-virtual` to virtualize the flattened visible node list. Right-click context menu using shadcn ContextMenu. + +- [ ] **Step 4: Create SearchBar, TypeFilters, StatusBar** + +- SearchBar: input field, Ctrl+F focuses it, Escape clears +- TypeFilters: horizontal scrollable row of checkbox chips with counts +- StatusBar: "20 files, 3 folders | 2 selected" + +- [ ] **Step 5: Wire into SplitPanel left side** + +- [ ] **Step 6: Verify with mock data** + +Manually add some TreeNode data to the store and verify the tree renders, expands, collapses, and checkboxes work. + +- [ ] **Step 7: Commit** + +```bash +cd /Users/guantou/Desktop/Index-Ripper +git add electron-app/src/renderer/ +git commit -m "feat(electron): implement FileTree with virtual scrolling, search, type filters" +``` + +--- + +## Task 9: Downloads Panel + Logs Panel + +**Files:** +- Create: `electron-app/src/renderer/components/DownloadsList.tsx` +- Create: `electron-app/src/renderer/components/DownloadCard.tsx` +- Create: `electron-app/src/renderer/components/LogsPanel.tsx` + +- [ ] **Step 1: Build DownloadCard** + +Card showing: file name, progress bar (shadcn Progress), status text (color-coded), speed display, cancel/retry button. + +- [ ] **Step 2: Build DownloadsList** + +Scrollable list of DownloadCards. Overall progress bar + ETA at bottom. + +- [ ] **Step 3: Build LogsPanel** + +Scrollable monospace text area, auto-scrolls to bottom. Receives `log:message` IPC events. + +- [ ] **Step 4: Wire into SplitPanel right side with tab switching** + +Use shadcn Tabs to switch between Downloads and Logs. + +- [ ] **Step 5: Commit** + +```bash +cd /Users/guantou/Desktop/Index-Ripper +git add electron-app/src/renderer/ +git commit -m "feat(electron): implement DownloadsList, DownloadCard, LogsPanel" +``` + +--- + +## Task 10: IPC Integration — Connect UI to Backend + +**Files:** +- Modify: `electron-app/src/renderer/components/Toolbar.tsx` +- Modify: `electron-app/src/renderer/components/TaskTabs.tsx` +- Modify: `electron-app/src/renderer/App.tsx` +- Modify: `electron-app/src/renderer/stores/*.ts` + +- [ ] **Step 1: Wire scan flow** + +Toolbar "Scan" button → `window.api.scan.start(taskId, url)`. Subscribe to `scan:item`, `scan:progress`, `scan:finished`, `scan:error` events → update tree store and task store. + +- [ ] **Step 2: Wire download flow** + +Toolbar "Download" button → collect checked files → `window.api.download.start(taskId, files, destPath)`. Subscribe to `download:progress`, `download:status`, `download:finished` → update download store. + +- [ ] **Step 3: Wire task management** + +TaskTabs `+` button → `window.api.task.create(url)`. Tab close → `window.api.task.remove(taskId)`. Tab click → switch active task in store. + +- [ ] **Step 4: Wire settings persistence** + +On window close → save panel sizes, thread count, last download path via `window.api.settings.set()`. On app start → restore via `window.api.settings.get()`. + +- [ ] **Step 5: Add toast notifications** + +Use shadcn Sonner for: scan complete, download complete, errors. + +- [ ] **Step 6: Full integration test** + +Launch app, enter a real "Index of" URL, scan, select files, download. Verify complete flow. + +- [ ] **Step 7: Commit** + +```bash +cd /Users/guantou/Desktop/Index-Ripper +git add electron-app/ +git commit -m "feat(electron): integrate IPC between UI and backend, full scan/download flow" +``` + +--- + +## Task 11: Packaging & Build Configuration + +**Files:** +- Modify: `electron-app/electron-builder.yml` +- Modify: `electron-app/package.json` + +- [ ] **Step 1: Configure electron-builder.yml** + +```yaml +appId: com.indexripper.app +productName: Index Ripper +directories: + buildResources: resources + output: dist +files: + - '!**/.vscode/*' + - '!src/*' + - '!electron.vite.config.*' + - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' +win: + target: nsis + icon: resources/icon.ico +mac: + target: dmg + icon: resources/icon.png + category: public.app-category.utilities +linux: + target: AppImage + icon: resources/icon.png + category: Utility +``` + +- [ ] **Step 2: Add build scripts to package.json** + +```json +{ + "scripts": { + "build": "electron-vite build", + "build:win": "npm run build && electron-builder --win", + "build:mac": "npm run build && electron-builder --mac", + "build:linux": "npm run build && electron-builder --linux" + } +} +``` + +- [ ] **Step 3: Test build** + +```bash +cd /Users/guantou/Desktop/Index-Ripper/electron-app +npm run build +``` + +Expected: Build succeeds, output in `dist/` + +- [ ] **Step 4: Commit** + +```bash +cd /Users/guantou/Desktop/Index-Ripper +git add electron-app/ +git commit -m "build(electron): configure electron-builder for cross-platform packaging" +``` + +--- + +## Task 12: Final Verification & Cleanup + +- [ ] **Step 1: Run all tests** + +```bash +cd /Users/guantou/Desktop/Index-Ripper/electron-app +npx vitest run +``` + +- [ ] **Step 2: Verify complete flow** + +1. `npm run dev` → app launches +2. Enter URL → Scan → file tree populates +3. Check files → Download → progress shows +4. Pause/resume scan and download +5. Add second task tab → switch between tasks +6. Cancel individual download +7. Change thread count + +- [ ] **Step 3: Update .gitignore** + +Add `electron-app/node_modules/`, `electron-app/dist/`, `electron-app/out/` to root `.gitignore`. + +- [ ] **Step 4: Final commit** + +```bash +cd /Users/guantou/Desktop/Index-Ripper +git add -A +git commit -m "feat(electron): complete Electron rewrite with full feature parity" +``` diff --git a/docs/superpowers/plans/2026-03-22-project-restructure.md b/docs/superpowers/plans/2026-03-22-project-restructure.md new file mode 100644 index 0000000..030caab --- /dev/null +++ b/docs/superpowers/plans/2026-03-22-project-restructure.md @@ -0,0 +1,665 @@ +# Index Ripper 專案重構計畫 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 將平放在根目錄的所有 .py 檔案重組為 `src/index_ripper/` 套件結構,移除遺留程式碼,拆分大型模組,統一命名風格,並更新所有相關設定。 + +**Architecture:** 建立 `src/index_ripper/` Python 套件,將核心邏輯、UI 元件、工具函式分離到子模組中。`index_ripper.py` 中的舊版 `WebsiteCopier` 類別(使用 ttk.Treeview)已被 `ui_ctk.py` 中的 `WebsiteCopierCtk`(使用自訂 RowWidget)完全取代,將移除舊版。`ui_ttkb.py`、`pyside_poc.py`、`index_ripper_ttkb.py` 為遺留/PoC 程式碼,一併移除。 + +**Tech Stack:** Python 3.11+, CustomTkinter, requests, BeautifulSoup4, pytest, uv, PyInstaller + +--- + +## 檔案結構規劃 + +### 移除的檔案 +- `index_ripper.py` — 包含舊版 `WebsiteCopier` 類別(1434 行,使用 ttk.Treeview),已被 `ui_ctk.py` 的 `WebsiteCopierCtk` 完全取代 +- `ui_ttkb.py` — 舊版 ttkbootstrap UI(1387 行) +- `index_ripper_ttkb.py` — ttkbootstrap 進入點 +- `pyside_poc.py` — PySide 概念驗證 +- `POC_MATRIX.md` — PoC 比較文件 + +### 新套件結構 +``` +src/ +└── index_ripper/ + ├── __init__.py # 套件初始化,版本號 + ├── __main__.py # python -m index_ripper 進入點(--smoke, --self-test, --ui-smoke) + ├── app.py # WebsiteCopierCtk 主應用類別(初始化、run、on_closing、backend bridge) + ├── backend.py # Backend 類別(掃描、下載邏輯)— 從原 backend.py 移入 + ├── settings.py # load_settings / save_settings — 從原 settings_store.py 移入 + ├── utils.py # 工具函式 — 從原 app_utils.py 移入 + ├── self_test.py # 自測邏輯 — 從原 self_test.py 移入 + └── ui/ + ├── __init__.py # UI 子套件 + ├── theme.py # 主題、顏色 tokens、樣式設定 — 從原 ui_theme.py 移入 + ├── downloads.py # DownloadsPanel — 從原 ui_downloads.py 移入 + ├── filetree.py # TreeNode、RowWidget、FileTree 相關常量 + └── filters.py # 檔案類型篩選邏輯 +tests/ + ├── __init__.py + ├── test_backend.py + ├── test_utils.py # 合併原 test_app_utils.py + test_security_utils.py + ├── test_settings.py # 從原 test_settings_store.py 重新命名 + ├── test_theme.py # 從原 test_ui_theme.py 重新命名 + ├── test_downloads.py # 從原 test_ui_downloads.py 重新命名 + └── test_app_smoke.py # 從原 test_ui_ctk.py 重新命名 +``` + +### 修改的設定檔 +- `pyproject.toml` — 更新 entry point、testpaths +- `macos.spec` — 更新 Analysis 路徑 +- `.github/workflows/ci.yml` — 更新測試指令路徑 +- `.gitignore` — 確認 src/ 結構相容 + +--- + +## Task 1: 建立套件骨架並移動工具模組 + +**Files:** +- Create: `src/index_ripper/__init__.py` +- Create: `src/index_ripper/utils.py` +- Create: `src/index_ripper/settings.py` +- Create: `src/index_ripper/self_test.py` +- Modify: `pyproject.toml` + +- [ ] **Step 1: 建立目錄結構** + +```bash +mkdir -p src/index_ripper/ui +mkdir -p tests +``` + +- [ ] **Step 2: 建立 `src/index_ripper/__init__.py`** + +```python +"""Index Ripper — download files from directory listing pages.""" + +__version__ = "0.1.0" +``` + +- [ ] **Step 3: 移動 `app_utils.py` → `src/index_ripper/utils.py`** + +將 `app_utils.py` 的所有內容複製到 `src/index_ripper/utils.py`,保持所有函式不變。 + +- [ ] **Step 4: 移動 `settings_store.py` → `src/index_ripper/settings.py`** + +將 `settings_store.py` 的所有內容複製到 `src/index_ripper/settings.py`。 + +- [ ] **Step 5: 移動 `self_test.py` → `src/index_ripper/self_test.py`** + +將 `self_test.py` 的所有內容複製到 `src/index_ripper/self_test.py`。 + +- [ ] **Step 6: 更新 `pyproject.toml`** + +新增 build-system 設定(src layout 必需),更新 entry point,移除 ttkbootstrap 依賴: + +```toml +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/index_ripper"] + +[project.scripts] +index-ripper = "index_ripper.app:main" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +``` + +移除 `ttkbootstrap` 依賴: +```toml +dependencies = [ + "requests>=2.31.0,<3.0.0", + "beautifulsoup4>=4.12.0,<5.0.0", + "urllib3>=2.0.0,<3.0.0", + "customtkinter>=5.2.0,<6.0.0", +] +``` + +- [ ] **Step 6b: 執行 `uv sync` 安裝新套件結構** + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv sync --extra dev` +Expected: 成功安裝,`index_ripper` 套件可被 import + +- [ ] **Step 7: 執行測試確認工具模組可獨立 import** + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -c "from index_ripper.utils import sanitize_filename; print('OK')"` +Expected: `OK` + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -c "from index_ripper.settings import load_settings; print('OK')"` +Expected: `OK` + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -c "from index_ripper.self_test import run_self_test; print('OK')"` +Expected: `OK` + +- [ ] **Step 8: Commit** + +```bash +git add src/ pyproject.toml +git commit -m "refactor: create src/index_ripper package skeleton with utils, settings, self_test" +``` + +--- + +## Task 2: 移動 backend 模組 + +**Files:** +- Create: `src/index_ripper/backend.py` +- Modify: `src/index_ripper/utils.py` (確認 import 路徑) + +- [ ] **Step 1: 複製 `backend.py` → `src/index_ripper/backend.py`** + +更新 import 語句: +```python +# 原本 +from app_utils import cleanup_partial_file, is_url_in_scope + +# 改為 +from index_ripper.utils import cleanup_partial_file, is_url_in_scope +``` + +其餘 Backend 類別內容完全不變。 + +- [ ] **Step 2: 驗證 import** + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -c "from index_ripper.backend import Backend; print('OK')"` +Expected: `OK` + +- [ ] **Step 3: Commit** + +```bash +git add src/index_ripper/backend.py +git commit -m "refactor: move Backend class to src/index_ripper/backend.py" +``` + +--- + +## Task 3: 移動 UI 子模組(theme、downloads) + +**Files:** +- Create: `src/index_ripper/ui/__init__.py` +- Create: `src/index_ripper/ui/theme.py` +- Create: `src/index_ripper/ui/downloads.py` + +- [ ] **Step 1: 建立 `src/index_ripper/ui/__init__.py`** + +```python +"""Index Ripper UI components.""" +``` + +- [ ] **Step 2: 複製 `ui_theme.py` → `src/index_ripper/ui/theme.py`** + +內容完全不變(沒有 internal import 需要更新)。 + +- [ ] **Step 3: 複製 `ui_downloads.py` → `src/index_ripper/ui/downloads.py`** + +內容完全不變(沒有 internal import 需要更新)。 + +- [ ] **Step 4: 驗證 import** + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -c "from index_ripper.ui.theme import ui_tokens; print('OK')"` +Expected: `OK` + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -c "from index_ripper.ui.downloads import DownloadsPanel; print('OK')"` +Expected: `OK` + +- [ ] **Step 5: Commit** + +```bash +git add src/index_ripper/ui/ +git commit -m "refactor: move theme and downloads UI modules to src/index_ripper/ui/" +``` + +--- + +## Task 4: 拆分 ui_ctk.py — 抽取 filetree 模組 + +**Files:** +- Create: `src/index_ripper/ui/filetree.py` + +`ui_ctk.py` 的前 180 行包含 `TreeNode` dataclass、`_EMOJI_ICONS` 常量、顏色常量、`RowWidget` 類別和 `should_skip_file_row` 函式。這些是自成一體的檔案樹資料模型和視覺元件。 + +- [ ] **Step 1: 建立 `src/index_ripper/ui/filetree.py`** + +從 `ui_ctk.py` 第 1-184 行提取以下內容: +- `TreeNode` dataclass +- `_EMOJI_ICONS` 字典 +- `_BG_NORMAL`, `_BG_HOVER`, `_BG_CHECKED`, `_BG_CHECKED_HOVER` 顏色常量 +- `RowWidget` 類別 +- `should_skip_file_row` 函式 + +加上必要的 import: +```python +from __future__ import annotations + +from dataclasses import dataclass, field + +import customtkinter as ctk +``` + +- [ ] **Step 2: 驗證 import** + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -c "from index_ripper.ui.filetree import TreeNode, RowWidget; print('OK')"` +Expected: `OK` + +- [ ] **Step 3: Commit** + +```bash +git add src/index_ripper/ui/filetree.py +git commit -m "refactor: extract TreeNode, RowWidget into ui/filetree.py" +``` + +--- + +## Task 5: 拆分 ui_ctk.py — 抽取 filters 模組 + +**Files:** +- Create: `src/index_ripper/ui/filters.py` + +`WebsiteCopierCtk` 中以下方法與檔案類型篩選有關,可獨立為 mixin 或輔助類別: +- `_add_file_type_filter` +- `_on_type_filter_changed` +- `select_all_types` +- `deselect_all_types` +- `_bind_hscroll_wheel` + +- [ ] **Step 1: 建立 `src/index_ripper/ui/filters.py`** + +建立 `FileTypeFilterMixin` 類別,包含上述方法。這些方法依賴 `self.file_types`, `self.file_type_counts`, `self.file_type_widgets`, `self.filters_container`, `self.tree_nodes`, `self.full_tree_backup` 等屬性,作為 mixin 使用時由主應用類別提供。 + +```python +"""File type filter UI logic (mixin for the main app).""" +from __future__ import annotations + +import tkinter as tk +import customtkinter as ctk + +from index_ripper.utils import normalize_extension + + +class FileTypeFilterMixin: + """Mixin providing file-type checkbox filter behaviour.""" + + def _add_file_type_filter(self, ext: str) -> None: + ... # 從 ui_ctk.py 第 1479-1497 行搬入 + + def _on_type_filter_changed(self, ext: str) -> None: + ... # 從 ui_ctk.py 第 1499-1511 行搬入 + # 注意:此方法呼叫 self._rebuild_visible() 和 self._sync_rows() + # 這些方法定義在 WebsiteCopierCtk (app.py) 中,由 mixin 透過 self 動態解析 + + def select_all_types(self) -> None: + ... # 從 ui_ctk.py 第 1449-1451 行搬入 + + def deselect_all_types(self) -> None: + ... # 從 ui_ctk.py 第 1453-1455 行搬入 + + def _bind_hscroll_wheel(self, widget) -> None: + ... # 從 ui_ctk.py 第 1457-1477 行搬入 +``` + +- [ ] **Step 2: 驗證 import** + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -c "from index_ripper.ui.filters import FileTypeFilterMixin; print('OK')"` +Expected: `OK` + +- [ ] **Step 3: Commit** + +```bash +git add src/index_ripper/ui/filters.py +git commit -m "refactor: extract FileTypeFilterMixin into ui/filters.py" +``` + +--- + +## Task 6: 建立主應用模組 app.py + +**Files:** +- Create: `src/index_ripper/app.py` + +這是最關鍵的一步。從 `ui_ctk.py` 的 `WebsiteCopierCtk` 類別搬入 `app.py`,同時: +- 從 `ui.filetree` import `TreeNode`, `RowWidget`, `should_skip_file_row` +- 從 `ui.filters` import `FileTypeFilterMixin` 並作為 mixin +- 更新所有 internal import 為套件路徑 + +- [ ] **Step 1: 建立 `src/index_ripper/app.py`** + +```python +"""Main application window.""" +from __future__ import annotations + +import copy +import os +import threading +import time +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass, field +from queue import Empty, Queue + +import tkinter as tk +from tkinter import filedialog, messagebox, ttk + +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +import customtkinter as ctk + +from index_ripper.utils import ( + build_download_path, + default_download_folder, + normalize_extension, + rebuild_executor, + safe_join, + sanitize_filename, +) +from index_ripper.backend import Backend +from index_ripper.ui.downloads import DownloadsPanel +from index_ripper.ui.theme import ( + apply_app_theme, + configure_action_button_styles, + ui_tokens, +) +from index_ripper.ui.filetree import TreeNode, RowWidget, should_skip_file_row +from index_ripper.ui.filters import FileTypeFilterMixin + + +class WebsiteCopierCtk(FileTypeFilterMixin): + ... # 從 ui_ctk.py 第 187-1517 行搬入(排除 TreeNode、RowWidget、should_skip_file_row、 + # FileTypeFilterMixin 已抽出的方法) + + +def main(): + app = WebsiteCopierCtk() + app.run() +``` + +- [ ] **Step 2: 建立 `src/index_ripper/__main__.py`** + +```python +"""Entry point for `python -m index_ripper`.""" +import sys + + +def _main() -> None: + if "--smoke" in sys.argv: + print("SMOKE_OK") + raise SystemExit(0) + + if "--self-test" in sys.argv: + from index_ripper.self_test import run_self_test + + result = run_self_test() + print( + f"SELF_TEST_OK total={result.total} files={result.files} dirs={result.directories}" + ) + raise SystemExit(0) + + from index_ripper.utils import configure_tk_libraries + configure_tk_libraries() + + if "--ui-smoke" in sys.argv: + from index_ripper.app import WebsiteCopierCtk + + app = WebsiteCopierCtk(ui_smoke=True) + app.window.after(0, app.window.destroy) + app.run() + print("UI_SMOKE_OK") + raise SystemExit(0) + + from index_ripper.app import main + main() + + +if __name__ == "__main__": + _main() +``` + +- [ ] **Step 3: 驗證應用可啟動** + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -m index_ripper --smoke` +Expected: `SMOKE_OK` + +- [ ] **Step 4: Commit** + +```bash +git add src/index_ripper/app.py src/index_ripper/__main__.py +git commit -m "refactor: create main app module with WebsiteCopierCtk and __main__ entry point" +``` + +--- + +## Task 7: 遷移並合併測試檔案 + +**Files:** +- Create: `tests/__init__.py` +- Create: `tests/test_backend.py` +- Create: `tests/test_utils.py` +- Create: `tests/test_settings.py` +- Create: `tests/test_theme.py` +- Create: `tests/test_downloads.py` +- Create: `tests/test_app_smoke.py` + +- [ ] **Step 1: 建立 `tests/__init__.py`** + +空檔案。 + +- [ ] **Step 2: 遷移 `test_backend.py` → `tests/test_backend.py`** + +更新 import: +```python +# 原本 +from backend import Backend + +# 改為 +from index_ripper.backend import Backend +``` + +- [ ] **Step 3: 合併 `test_app_utils.py` + `test_security_utils.py` → `tests/test_utils.py`** + +更新 import: +```python +# 原本 +from app_utils import ... + +# 改為 +from index_ripper.utils import ... +``` + +將兩個檔案的測試類別合併到同一檔案中。 + +- [ ] **Step 4: 遷移其餘測試** + +- `test_settings_store.py` → `tests/test_settings.py`(import 改為 `from index_ripper.settings import ...`) +- `test_ui_theme.py` → `tests/test_theme.py` + - 原本用 `import ui_theme` 再 `ui_theme.action_button_style_name(...)` 的寫法,改為 `from index_ripper.ui import theme` 再 `theme.action_button_style_name(...)` +- `test_ui_downloads.py` → `tests/test_downloads.py` + - 同上,`import ui_downloads` 改為 `from index_ripper.ui import downloads`,呼叫改為 `downloads.download_status_state(...)` +- `test_ui_ctk.py` → `tests/test_app_smoke.py` + - **重點**:此檔案大量使用 `import ui_ctk`、`importlib.import_module("ui_ctk")` 和 `importlib.reload(mod)` + - 所有 `"ui_ctk"` 字串需改為 `"index_ripper.app"` + - `import ui_ctk` 改為 `from index_ripper import app as ui_ctk` + - `importlib.import_module("ui_ctk")` 改為 `importlib.import_module("index_ripper.app")` + - `import ui_ctk, importlib` 改為 `from index_ripper import app as ui_ctk; import importlib` + +- [ ] **Step 5: 執行所有測試** + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -m pytest tests/ -v` +Expected: 所有測試通過 + +- [ ] **Step 6: Commit** + +```bash +git add tests/ +git commit -m "refactor: migrate and consolidate tests to tests/ directory" +``` + +--- + +## Task 8: 刪除舊檔案 + +**Files:** +- Delete: `index_ripper.py` +- Delete: `ui_ctk.py` +- Delete: `ui_ttkb.py` +- Delete: `index_ripper_ttkb.py` +- Delete: `pyside_poc.py` +- Delete: `POC_MATRIX.md` +- Delete: `backend.py` +- Delete: `app_utils.py` +- Delete: `settings_store.py` +- Delete: `self_test.py` +- Delete: `ui_theme.py` +- Delete: `ui_downloads.py` +- Delete: `test_backend.py` +- Delete: `test_app_utils.py` +- Delete: `test_security_utils.py` +- Delete: `test_settings_store.py` +- Delete: `test_ui_theme.py` +- Delete: `test_ui_downloads.py` +- Delete: `test_ui_ttkb.py` +- Delete: `test_ui_ctk.py` + +- [ ] **Step 1: 刪除遺留程式碼(UI 和 PoC)** + +```bash +git rm index_ripper.py ui_ttkb.py index_ripper_ttkb.py pyside_poc.py POC_MATRIX.md +``` + +- [ ] **Step 2: 刪除已遷移到 src/ 的舊模組** + +```bash +git rm ui_ctk.py backend.py app_utils.py settings_store.py self_test.py ui_theme.py ui_downloads.py +``` + +- [ ] **Step 3: 刪除已遷移到 tests/ 的舊測試** + +```bash +git rm test_backend.py test_app_utils.py test_security_utils.py test_settings_store.py test_ui_theme.py test_ui_downloads.py test_ui_ttkb.py test_ui_ctk.py +``` + +- [ ] **Step 4: 執行所有測試確認無殘留依賴** + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -m pytest tests/ -v` +Expected: 所有測試通過 + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -m index_ripper --smoke` +Expected: `SMOKE_OK` + +- [ ] **Step 5: Commit** + +```bash +git add -A +git commit -m "refactor: remove legacy files and old root-level modules" +``` + +--- + +## Task 9: 更新建置與 CI 設定 + +**Files:** +- Modify: `macos.spec` +- Modify: `.github/workflows/ci.yml` +- Modify: `.github/workflows/build.yml` (如有需要) + +- [ ] **Step 1: 更新 `macos.spec`** + +```python +a = Analysis( + ["src/index_ripper/__main__.py"], + pathex=["src"], # <-- 必須加上,讓 PyInstaller 找到 index_ripper 套件 + ... + hiddenimports=["tkinter", "tkinter.ttk", "index_ripper"], + ... +) +``` + +- [ ] **Step 2: 更新 `.github/workflows/ci.yml`** + +測試指令改為: +```yaml +- name: Run unit tests + run: uv run python -m pytest tests/ -v +``` + +更新所有三個平台的 PyInstaller build 指令(Windows、macOS、Linux),全部加上 `--paths src`: + +Windows build: +```yaml +run: | + uv run pyinstaller --onefile --windowed --icon=app.png --name=IndexRipper \ + --paths src --collect-all customtkinter src/index_ripper/__main__.py +``` + +macOS build: +```yaml +run: | + uv run pyinstaller -F --windowed --name=IndexRipper \ + --paths src --collect-all customtkinter --icon=app.png src/index_ripper/__main__.py +``` + +Linux build: +```yaml +run: | + uv run pyinstaller --onefile --windowed --name=IndexRipper \ + --paths src --collect-all customtkinter --icon=app.png src/index_ripper/__main__.py +``` + +- [ ] **Step 3: 更新 `pyproject.toml`(如果 Task 1 還沒做完)** + +確認 ttkbootstrap 已從 dependencies 移除。 + +- [ ] **Step 4: 驗證 smoke test** + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -m index_ripper --smoke` +Expected: `SMOKE_OK` + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -m index_ripper --self-test` +Expected: `SELF_TEST_OK total=... files=... dirs=...` + +- [ ] **Step 5: Commit** + +```bash +git add macos.spec .github/workflows/ pyproject.toml +git commit -m "build: update CI, PyInstaller, and pyproject for new package structure" +``` + +--- + +## Task 10: 最終驗證與清理 + +**Files:** +- Modify: `.gitignore` (如需要) +- Modify: `README.md` (更新安裝/運行指令) + +- [ ] **Step 1: 執行完整測試套件** + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -m pytest tests/ -v` +Expected: 所有測試通過 + +- [ ] **Step 2: 驗證所有進入點** + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -m index_ripper --smoke` +Expected: `SMOKE_OK` + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv run python -m index_ripper --self-test` +Expected: `SELF_TEST_OK ...` + +- [ ] **Step 3: 更新 README.md 中的使用說明** + +將 `python index_ripper.py` 改為 `python -m index_ripper` 或 `index-ripper`。 + +- [ ] **Step 4: 確認 `uv.lock` 同步(移除 ttkbootstrap)** + +Run: `cd /Users/guantou/Desktop/Index-Ripper && uv lock` + +- [ ] **Step 5: 最終 Commit** + +```bash +git add -A +git commit -m "docs: update README for new package structure" +``` diff --git a/docs/superpowers/specs/2026-03-22-electron-rewrite-design.md b/docs/superpowers/specs/2026-03-22-electron-rewrite-design.md new file mode 100644 index 0000000..3ab37be --- /dev/null +++ b/docs/superpowers/specs/2026-03-22-electron-rewrite-design.md @@ -0,0 +1,327 @@ +# Index Ripper Electron Rewrite — Design Spec + +## Goal + +Rewrite Index Ripper from Python + CustomTkinter to Electron + React + TypeScript + Node.js. This is a full rewrite — no Python code is retained. The new app delivers all existing features (website scanning, file tree browsing, filtering, multi-threaded download with pause/resume/cancel) plus a new multi-site task queue feature, wrapped in a modern split-panel UI built with shadcn/ui. + +## Architecture + +### Process Model + +**Main Process (Node.js):** +- Scanner service — two-phase: first recursively discover all URLs, then issue HEAD requests per file for metadata (size, MIME type). Scan concurrency: 10 parallel workers. +- Download manager — concurrent streaming downloads with progress tracking, automatic retry (3 attempts with exponential backoff for 5xx errors) +- Task queue — manages multiple scan/download jobs across different sites, each task is independent +- File system — safe path construction, settings persistence + +**Renderer Process (React + TypeScript):** +- Single-window app with split-panel layout, native title bar (not frameless — avoids cross-platform drag/resize issues) +- Communicates with main process exclusively via typed IPC channels +- No direct file system or network access + +**Preload Script:** +- Exposes a typed `window.api` object via `contextBridge` +- All IPC channels explicitly whitelisted — no `remote` module usage + +### IPC Channel Design + +All task-scoped channels include `taskId` as the first parameter. + +``` +renderer → main: + scan:start(taskId, url) → begins recursive crawl + scan:stop(taskId) → aborts in-progress scan + scan:pause(taskId) / scan:resume(taskId) + download:start(taskId, files[], destPath) + download:pause(taskId) / download:resume(taskId) + download:cancel(taskId, fileId) + download:cancelAll(taskId) → cancel all active downloads + download:retry(taskId, fileId) → retry a failed download + download:setWorkers(count) → change concurrency (1-10) + task:create(url) → add new site to task queue + task:remove(taskId) + settings:get() / settings:set(data) + dialog:selectFolder() → open native folder picker + +main → renderer: + scan:item(taskId, node) → discovered file or directory + scan:progress(taskId, scanned, total) + scan:finished(taskId, stopped) + scan:error(taskId, message) → structured scan error + download:progress(taskId, fileId, percent, speed) + download:status(taskId, fileId, status) + download:finished(taskId, completed, total) + log:message(taskId, text) +``` + +## Tech Stack + +| Layer | Technology | Why | +|-------|-----------|-----| +| Framework | Electron 35+ | Desktop app with full OS integration | +| Build tool | electron-vite | Vite-based, fast HMR, TypeScript native | +| Frontend | React 19 + TypeScript | Complex state (tree, downloads, tasks) | +| Styling | Tailwind CSS v4 + shadcn/ui | Modern component library, dark mode built-in | +| State | Zustand | Lightweight, works well with React, no boilerplate | +| HTTP client | Electron `net` module | Respects system proxy, native to Electron | +| HTML parser | cheerio | jQuery-like API for parsing directory listings | +| Concurrency | p-queue | Configurable concurrent download/scan limit | +| Settings | electron-store | JSON persistence with schema validation | +| Tree virtualization | @tanstack/react-virtual | Better variable-height support than react-window | +| Packaging | electron-builder | Cross-platform builds (Windows, macOS, Linux) | + +## UI Design + +### Layout: Split Panel with Native Title Bar + +``` +┌─────────────────────────────────────────────────┐ +│ Index Ripper — □ × │ ← native title bar +├─────────────────────────────────────────────────┤ +│ [example.com] [mirror.site] [+] (task tabs) │ +├─────────────────────────────────────────────────┤ +│ [https://example.com/files/ ] [Scan] [⬇] ● OK │ +├────────────────────┬────────────────────────────┤ +│ 🔍 Search... │ [Downloads] [Logs] │ +│ .pdf(5) .jpg(12) │ │ +│────────────────────│ ┌────────────────────────┐ │ +│ 📁 documents ▼ │ │ report.pdf ✓ Done │ │ +│ ▌📄 report.pdf │ │ ████████████████ 100% │ │ +│ 📄 readme.txt │ ├────────────────────────┤ │ +│ 📁 images ▶ │ │ photo.jpg 67% 1.2MB │ │ +│ 📁 archives ▶ │ │ ████████░░░░░░░ 67% │ │ +│ │ ├────────────────────────┤ │ +│ │ │ backup.zip Queued │ │ +│ │ │ ░░░░░░░░░░░░░░░ 0% │ │ +│ 20 files, 3 dirs │ │ +│ 2 selected │ 1/3 done — ~2 min left │ +└────────────────────┴────────────────────────────┘ +``` + +### Component Hierarchy + +``` +App +├── TaskTabs (multi-site tab bar with + button) +├── Toolbar (URL input, Scan button, Download button, thread count, status) +└── SplitPanel (resizable left/right) + ├── LeftPanel + │ ├── SearchBar (text filter input) + │ ├── TypeFilters (scrollable checkbox chips) + │ ├── FileTree (virtual-scrolled tree with checkboxes) + │ └── StatusBar (file/folder count, selected count) + └── RightPanel + ├── TabSwitcher (Downloads / Logs toggle) + ├── DownloadsList (per-file progress cards with cancel/retry) + └── OverallProgress (aggregate progress bar + ETA) +``` + +### File Tree + +- **Virtual scrolling** via `@tanstack/react-virtual` — flattened list approach, handles 10,000+ nodes +- Each row: accent bar (blue when checked) + icon + name + size + chevron +- Icons: same emoji set as current (📁📄🖼️🗜️💻🎵🎬📝⚙️) +- Checkbox toggling: click row to toggle, folders cascade to children +- Right-click context menu: Select All, Deselect All, Expand All, Collapse All +- Keyboard: Ctrl/Cmd+A select all, Ctrl/Cmd+F focus search, Ctrl/Cmd+L focus logs, Escape clear search +- macOS: standard edit shortcuts (Cmd+V/C/X/Z) work natively in text inputs (Electron handles this with native title bar) + +### Notifications + +- Toast notifications (shadcn/ui `Sonner`) for: scan complete, no files found, download complete, network errors +- Log panel for detailed messages + +### Theme + +- Dark mode only initially (matching current app aesthetic) +- Slate color palette from Tailwind (gray-900 backgrounds, blue-500 accents) +- shadcn/ui dark theme defaults for all interactive components + +## Features + +### Existing Features (preserved) + +1. **Website scanning** — two-phase: recursive URL discovery (10 concurrent), then HEAD requests for file metadata (size, MIME type). Supports pause/resume/stop +2. **File tree** — hierarchical display with expand/collapse, checkbox selection, cascade +3. **File type filtering** — dynamic checkbox chips with counts, select/deselect all +4. **Search filtering** — real-time text search across file names and paths +5. **Configurable concurrent downloads** — 1-10 workers, adjustable at runtime via toolbar dropdown +6. **Per-file progress** — individual progress bars, speed display, status labels, cancel buttons +7. **Pause/resume downloads** — global pause affects all active downloads +8. **Directory structure preservation** — downloads mirror source hierarchy +9. **Partial file cleanup** — removes incomplete files on cancel/error +10. **Settings persistence** — remembers window size, panel proportions, thread count, last download path +11. **Automatic retry** — 3 retries with exponential backoff for transient HTTP errors (5xx) +12. **Default download folder** — auto-derived from site hostname (e.g., `~/Downloads/example.com/`) +13. **URL right-click context menu** — paste support in URL input + +### New Feature: Multi-Site Task Queue + +Each "task" represents one website scan/download session. Users can: +- Add new tasks via the `+` tab button (enters URL, starts scan) +- Switch between tasks — each task has its own file tree, download state, and progress +- Remove completed/cancelled tasks +- Tasks run independently — scanning site A while downloading from site B + +Task lifecycle (state machine): +``` +idle → scanning → scanned → downloading → done + ↓ ↓ + error error + +Any state → cancelled (via task removal) +Scanning can be paused/resumed independently of downloads. +A single task cannot scan and download simultaneously. +``` + +Tasks are **not persisted** across app restarts. Closing the app discards all tasks. + +## Data Models + +All types live in `src/shared/types.ts` — imported by both main and renderer. + +### TreeNode + +```typescript +interface TreeNode { + id: string + parentId: string // "" for roots + name: string + kind: 'folder' | 'file' + url: string // download URL (files only, "" for folders) + fullPath: string // relative path (e.g., "docs/sub" for folders, "docs/sub/file.txt" for files) + size: string // "245 KB" or "Unknown" + fileType: string // MIME type from Content-Type header + iconGroup: string // folder|image|document|archive|code|audio|video|text|binary + checked: boolean + expanded: boolean + hidden: boolean // filtered out by search/type + children: string[] // child node IDs +} +``` + +### Task + +```typescript +interface Task { + id: string + url: string + status: 'idle' | 'scanning' | 'scanned' | 'downloading' | 'done' | 'error' | 'cancelled' + nodes: Record // id → TreeNode (flat map for O(1) lookup) + roots: string[] // top-level node IDs in insertion order + checkedFiles: string[] // fullPath[] of checked files (serializable, derived from nodes) + downloads: Record // fileId → DownloadItem + scanProgress: { scanned: number; total: number } + downloadPath: string // per-task download destination +} +``` + +`checkedFiles` is derived from `nodes` (all file nodes where `checked === true`). It exists as a convenience for `download:start` IPC calls. The source of truth for checked state is `TreeNode.checked`. + +### DownloadItem + +```typescript +interface DownloadItem { + id: string // same as TreeNode.id for correlation + fileName: string + url: string + destPath: string + status: 'queued' | 'downloading' | 'paused' | 'completed' | 'failed' | 'cancelled' + progress: number // 0-100 + speed: number // bytes/sec + totalSize: number + downloadedSize: number +} +``` + +`DownloadItem.id` equals the corresponding `TreeNode.id`, enabling direct correlation between tree selection and download progress. + +### Icon Mapping + +MIME type to `iconGroup` mapping (in `src/shared/icons.ts`): + +```typescript +function getIconGroup(fileName: string, mimeType: string): string { + const ext = extname(fileName).toLowerCase() + if (['.jpg','.jpeg','.png','.gif','.webp','.svg','.bmp','.ico'].includes(ext)) return 'image' + if (['.md','.txt','.pdf','.doc','.docx','.rtf'].includes(ext)) return 'document' + if (['.zip','.rar','.7z','.tar','.gz','.bz2','.xz'].includes(ext)) return 'archive' + if (['.py','.js','.ts','.tsx','.jsx','.go','.rs','.java','.c','.cpp','.h','.json','.yaml','.yml','.toml','.xml','.html','.css','.sh'].includes(ext)) return 'code' + if (mimeType.includes('audio/')) return 'audio' + if (mimeType.includes('video/')) return 'video' + if (mimeType.includes('text/')) return 'text' + if (mimeType.includes('image/')) return 'image' + return 'binary' +} +``` + +## Project Structure + +``` +electron-app/ +├── package.json +├── electron.vite.config.ts +├── tsconfig.json +├── src/ +│ ├── shared/ +│ │ ├── types.ts # TreeNode, Task, DownloadItem interfaces +│ │ └── icons.ts # Icon/emoji mapping +│ ├── main/ # Electron main process +│ │ ├── index.ts # App entry, window creation +│ │ ├── ipc.ts # IPC handler registration +│ │ ├── scanner.ts # Two-phase website scanning +│ │ ├── downloader.ts # Download manager with retry +│ │ ├── task-queue.ts # Multi-site task management +│ │ └── utils.ts # Path sanitization, URL validation +│ ├── preload/ +│ │ └── index.ts # contextBridge API exposure +│ └── renderer/ # React app +│ ├── index.html +│ ├── main.tsx # React entry +│ ├── App.tsx # Root layout +│ ├── stores/ +│ │ ├── task-store.ts # Zustand: tasks, active task, settings +│ │ ├── tree-store.ts # Zustand: file tree state per task +│ │ └── download-store.ts # Zustand: download state per task +│ ├── components/ +│ │ ├── TaskTabs.tsx +│ │ ├── Toolbar.tsx +│ │ ├── SplitPanel.tsx +│ │ ├── SearchBar.tsx +│ │ ├── TypeFilters.tsx +│ │ ├── FileTree.tsx # Virtual-scrolled tree +│ │ ├── FileTreeRow.tsx # Single row component +│ │ ├── DownloadsList.tsx +│ │ ├── DownloadCard.tsx +│ │ ├── LogsPanel.tsx +│ │ └── StatusBar.tsx +│ └── hooks/ +│ ├── useIpc.ts # IPC event subscription hook +│ └── useFileTree.ts # Tree traversal/filtering logic +├── resources/ # App icons +│ ├── icon.ico +│ └── icon.png +└── electron-builder.yml # Packaging config +``` + +## Error Handling + +- **Network errors during scan**: Log to UI via `log:message`, report via `scan:error`, skip failed URLs, continue scanning +- **Download failures**: Mark as "Failed", automatic retry (3 attempts, exponential backoff for 5xx). Manual retry via `download:retry` after max retries exhausted +- **Invalid URLs**: Validate before scan, show inline error on URL input +- **Path traversal**: Sanitize all path segments — strip `..`, `/`, `\`, Windows-illegal chars +- **Scope validation**: Ensure discovered URLs stay within base domain/path (same origin + path prefix check) + +## Testing Strategy + +- **Main process unit tests**: Scanner parsing, URL validation, path sanitization, icon mapping (Vitest) +- **Renderer component tests**: React Testing Library for FileTree, DownloadCard, TypeFilters +- **IPC integration tests**: Verify channel contracts between main and renderer +- **E2E consideration**: Playwright + Electron support for full-flow testing (future) + +## Migration Notes + +- This is a **new project** in an `electron-app/` directory alongside the existing `src/` Python code +- The Python code remains untouched — users can choose which version to use +- Feature parity is the primary goal; the Electron version should do everything the Python version does +- The multi-site task queue is the only net-new feature diff --git a/electron-app/.gitignore b/electron-app/.gitignore new file mode 100644 index 0000000..7302b66 --- /dev/null +++ b/electron-app/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +out/ +.DS_Store +*.local diff --git a/electron-app/components.json b/electron-app/components.json new file mode 100644 index 0000000..85d797e --- /dev/null +++ b/electron-app/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/renderer/src/assets/main.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/electron-app/electron-builder.yml b/electron-app/electron-builder.yml new file mode 100644 index 0000000..f450003 --- /dev/null +++ b/electron-app/electron-builder.yml @@ -0,0 +1,21 @@ +appId: com.indexripper.app +productName: Index Ripper +directories: + buildResources: resources + output: dist +files: + - '!**/.vscode/*' + - '!src/*' + - '!electron.vite.config.*' + - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' +win: + target: nsis + icon: resources/icon.ico +mac: + target: dmg + icon: resources/icon.png + category: public.app-category.utilities +linux: + target: AppImage + icon: resources/icon.png + category: Utility diff --git a/electron-app/electron.vite.config.ts b/electron-app/electron.vite.config.ts new file mode 100644 index 0000000..ac51757 --- /dev/null +++ b/electron-app/electron.vite.config.ts @@ -0,0 +1,23 @@ +import { resolve } from 'path' +import { defineConfig, externalizeDepsPlugin } from 'electron-vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + main: { + plugins: [externalizeDepsPlugin()] + }, + preload: { + plugins: [externalizeDepsPlugin()] + }, + renderer: { + resolve: { + alias: { + '@renderer': resolve('src/renderer/src'), + '@': resolve('src/renderer/src'), + '@shared': resolve('src/shared') + } + }, + plugins: [react(), tailwindcss()] + } +}) diff --git a/electron-app/package-lock.json b/electron-app/package-lock.json new file mode 100644 index 0000000..a354a8b --- /dev/null +++ b/electron-app/package-lock.json @@ -0,0 +1,11895 @@ +{ + "name": "index-ripper", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "index-ripper", + "version": "1.0.0", + "hasInstallScript": true, + "dependencies": { + "@electron-toolkit/preload": "^3.0.1", + "@electron-toolkit/utils": "^3.0.0", + "cheerio": "^1.2.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "electron-store": "^10.1.0", + "lucide-react": "^0.577.0", + "next-themes": "^0.4.6", + "p-queue": "^8.1.1", + "radix-ui": "^1.4.3", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@electron-toolkit/eslint-config-prettier": "^2.0.0", + "@electron-toolkit/eslint-config-ts": "^2.0.0", + "@electron-toolkit/tsconfig": "^1.0.1", + "@tailwindcss/vite": "^4.0.0", + "@tanstack/react-virtual": "^3.13.23", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "electron": "^33.2.1", + "electron-builder": "^25.1.8", + "electron-vite": "^2.3.0", + "eslint": "^9.9.1", + "prettier": "^3.3.3", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwindcss": "^4.0.0", + "typescript": "^5.6.2", + "vite": "^5.4.6", + "vitest": "^2.1.9" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron-toolkit/eslint-config-prettier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@electron-toolkit/eslint-config-prettier/-/eslint-config-prettier-2.0.0.tgz", + "integrity": "sha512-L+uG1FvJcAZkPZpSi6B1pmdpyJFyOxWDTjr1Vs47vSryxv/EX1Ch6o4HVsachlDq3fMEkDgojuP2F3ZvVZMoLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.0.1" + }, + "peerDependencies": { + "eslint": ">= 8.0.0", + "prettier": ">= 3.0.0" + } + }, + "node_modules/@electron-toolkit/eslint-config-ts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@electron-toolkit/eslint-config-ts/-/eslint-config-ts-2.0.0.tgz", + "integrity": "sha512-NGXadMyWH9+ZsgYe/u5E0mqK2qTDq01kKKnyo7oiq/7v/dWoMoPhqSkn69NZvt7WmnFNOm57l71fv6128mAx3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "^7.5.0", + "@typescript-eslint/parser": "^7.5.0" + }, + "peerDependencies": { + "eslint": ">=8.56.0", + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@electron-toolkit/eslint-config-ts/node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@electron-toolkit/eslint-config-ts/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@electron-toolkit/eslint-config-ts/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@electron-toolkit/eslint-config-ts/node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@electron-toolkit/preload": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@electron-toolkit/preload/-/preload-3.0.2.tgz", + "integrity": "sha512-TWWPToXd8qPRfSXwzf5KVhpXMfONaUuRAZJHsKthKgZR/+LqX1dZVSSClQ8OTAEduvLGdecljCsoT2jSshfoUg==", + "license": "MIT", + "peerDependencies": { + "electron": ">=13.0.0" + } + }, + "node_modules/@electron-toolkit/tsconfig": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@electron-toolkit/tsconfig/-/tsconfig-1.0.1.tgz", + "integrity": "sha512-M0Mol3odspvtCuheyujLNAW7bXq7KFNYVMRtpjFa4ZfES4MuklXBC7Nli/omvc+PRKlrklgAGx3l4VakjNo8jg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/node": "*" + } + }, + "node_modules/@electron-toolkit/utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@electron-toolkit/utils/-/utils-3.0.0.tgz", + "integrity": "sha512-GaXHDhiT7KCvMJjXdp/QqpYinq69T/Pdl49Z1XLf8mKGf63dnsODMWyrmIjEQ0z/vG7dO8qF3fvmI6Eb2lUNZA==", + "license": "MIT", + "peerDependencies": { + "electron": ">=13.0.0" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.1.tgz", + "integrity": "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.6.1.tgz", + "integrity": "sha512-f6596ZHpEq/YskUd8emYvOUne89ij8mQgjYFA5ru25QwbrRO+t1SImofdDv7kKOuWCmVOuU5tvfkbgGxIl3E/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "node-abi": "^3.45.0", + "node-api-version": "^0.2.0", + "node-gyp": "^9.0.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/@electron/rebuild/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/rebuild/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/rebuild/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", + "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.7", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz", + "integrity": "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", + "integrity": "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", + "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", + "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", + "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", + "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", + "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-is-hydrated": "0.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", + "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.11" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.23.tgz", + "integrity": "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.23" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz", + "integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.10", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.10.tgz", + "integrity": "sha512-Ev4jj3D7Bo+O0GPD2NMvJl+PGiBAfS7pUGawntBNpCbxtpncfUixqFj9z9Jme7V7s3LBGqsWZZP54fxBX3JKJw==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-25.1.8.tgz", + "integrity": "sha512-pCqe7dfsQFBABC1jeKZXQWhGcCPF3rPCXDdfqVKjIeWBcXzyC1iOWZdfFhGl+S9MyE/k//DFmC6FzuGAUudNDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.1", + "@electron/rebuild": "3.6.1", + "@electron/universal": "2.0.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "chromium-pickle-js": "^0.2.0", + "config-file-ts": "0.2.8-rc1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "25.1.7", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.0", + "resedit": "^1.7.0", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "25.1.8", + "electron-builder-squirrel-windows": "25.1.8" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "dev": true, + "license": "ISC" + }, + "node_modules/archiver": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", + "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.4", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/archiver-utils/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/archiver-utils/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/atomically": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", + "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==", + "license": "MIT", + "dependencies": { + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "25.1.7", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-25.1.7.tgz", + "integrity": "sha512-7jPjzBwEGRbwNcep0gGNpLXG9P94VA3CPAZQCzxkFXiV2GMQKlziMbY//rXPI7WKfhsvGgFXjTcXdBEwgXw9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.10", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.2.10", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", + "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", + "integrity": "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/cacache/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001780", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", + "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/conf": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-14.0.0.tgz", + "integrity": "sha512-L6BuueHTRuJHQvQVc6YXYZRtN5vJUtOdCTLn0tRYYV5azfbAFcPghB5zEE40mVrV6w7slMTqUfkDomutIK14fw==", + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "atomically": "^2.0.3", + "debounce-fn": "^6.0.0", + "dot-prop": "^9.0.0", + "env-paths": "^3.0.0", + "json-schema-typed": "^8.0.1", + "semver": "^7.7.2", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/conf/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/conf/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/config-file-ts": { + "version": "0.2.8-rc1", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", + "integrity": "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.3.12", + "typescript": "^5.4.3" + } + }, + "node_modules/config-file-ts/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-file-ts/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", + "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debounce-fn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-6.0.0.tgz", + "integrity": "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT", + "optional": true + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dmg-builder": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-25.1.8.tgz", + "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "25.1.8", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "33.4.11", + "resolved": "https://registry.npmjs.org/electron/-/electron-33.4.11.tgz", + "integrity": "sha512-xmdAs5QWRkInC7TpXGNvzo/7exojubk+72jn1oJL7keNeIlw7xNglf8TGtJtkR4rWC5FJq0oXiIXPS9BcK2Irg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-25.1.8.tgz", + "integrity": "sha512-poRgAtUHHOnlzZnc9PK4nzG53xh74wj2Jy7jkTrqZ0MWPoHGh1M2+C//hGeYdA+4K8w4yiVCNYoLXF7ySj2Wig==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "25.1.8", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "chalk": "^4.1.2", + "dmg-builder": "25.1.8", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "25.1.8", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-25.1.8.tgz", + "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "25.1.8", + "archiver": "^5.3.1", + "builder-util": "25.1.7", + "fs-extra": "^10.1.0" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder-squirrel-windows/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "25.1.7", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz", + "integrity": "sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "25.1.7", + "builder-util-runtime": "9.2.10", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-store": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-10.1.0.tgz", + "integrity": "sha512-oL8bRy7pVCLpwhmXy05Rh/L6O93+k9t6dqSw0+MckIc3OmCTZm6Mp04Q4f/J0rtu84Ky6ywkR8ivtGOmrq+16w==", + "license": "MIT", + "dependencies": { + "conf": "^14.0.0", + "type-fest": "^4.41.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/electron-vite": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-2.3.0.tgz", + "integrity": "sha512-lsN2FymgJlp4k6MrcsphGqZQ9fKRdJKasoaiwIrAewN1tapYI/KINLdfEL7n10LuF0pPSNf/IqjzZbB5VINctg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "cac": "^6.7.14", + "esbuild": "^0.21.5", + "magic-string": "^0.30.10", + "picocolors": "^1.0.1" + }, + "bin": { + "electron-vite": "bin/electron-vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@swc/core": "^1.0.0", + "vite": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + } + } + }, + "node_modules/electron/node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/electron/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hosted-git-info/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-fetch-happen": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", + "integrity": "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz", + "integrity": "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", + "integrity": "sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^10.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz", + "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/radix-ui": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", + "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-accessible-icon": "1.1.7", + "@radix-ui/react-accordion": "1.2.12", + "@radix-ui/react-alert-dialog": "1.1.15", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-aspect-ratio": "1.1.7", + "@radix-ui/react-avatar": "1.1.10", + "@radix-ui/react-checkbox": "1.3.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-context-menu": "2.2.16", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-dropdown-menu": "2.1.16", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-form": "0.1.8", + "@radix-ui/react-hover-card": "1.1.15", + "@radix-ui/react-label": "2.1.7", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-menubar": "1.1.16", + "@radix-ui/react-navigation-menu": "1.2.14", + "@radix-ui/react-one-time-password-field": "0.1.8", + "@radix-ui/react-password-toggle-field": "0.1.3", + "@radix-ui/react-popover": "1.1.15", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-progress": "1.1.7", + "@radix-ui/react-radio-group": "1.3.8", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-scroll-area": "1.2.10", + "@radix-ui/react-select": "2.2.6", + "@radix-ui/react-separator": "1.1.7", + "@radix-ui/react-slider": "1.3.6", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-switch": "1.2.6", + "@radix-ui/react-tabs": "1.1.13", + "@radix-ui/react-toast": "1.2.15", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.11", + "@radix-ui/react-toolbar": "1.1.11", + "@radix-ui/react-tooltip": "1.2.8", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-escape-keydown": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/rollup": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ssri": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz", + "integrity": "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stubborn-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", + "license": "MIT", + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "license": "MIT" + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz", + "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/unique-filename": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", + "integrity": "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/unique-slug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz", + "integrity": "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/when-exit": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zustand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/electron-app/package.json b/electron-app/package.json new file mode 100644 index 0000000..180c8f2 --- /dev/null +++ b/electron-app/package.json @@ -0,0 +1,60 @@ +{ + "name": "index-ripper", + "version": "1.0.0", + "description": "Index Ripper - Electron App", + "main": "./out/main/index.js", + "author": "Index Ripper", + "homepage": ".", + "scripts": { + "format": "prettier --write .", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx", + "typecheck:node": "tsc --noEmit -p tsconfig.node.json", + "typecheck:web": "tsc --noEmit -p tsconfig.web.json", + "typecheck": "npm run typecheck:node && npm run typecheck:web", + "start": "electron-vite preview", + "dev": "electron-vite dev", + "build": "npm run typecheck && electron-vite build", + "postinstall": "electron-builder install-app-deps", + "build:unpack": "npm run build && electron-builder --dir", + "build:win": "npm run build && electron-builder --win", + "build:mac": "npm run build && electron-builder --mac", + "build:linux": "npm run build && electron-builder --linux", + "test": "vitest run" + }, + "dependencies": { + "@electron-toolkit/preload": "^3.0.1", + "@electron-toolkit/utils": "^3.0.0", + "cheerio": "^1.2.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "electron-store": "^10.1.0", + "lucide-react": "^0.577.0", + "next-themes": "^0.4.6", + "p-queue": "^8.1.1", + "radix-ui": "^1.4.3", + "sonner": "^2.0.7", + "tailwind-merge": "^3.5.0", + "zustand": "^5.0.12" + }, + "devDependencies": { + "@electron-toolkit/eslint-config-prettier": "^2.0.0", + "@electron-toolkit/eslint-config-ts": "^2.0.0", + "@electron-toolkit/tsconfig": "^1.0.1", + "@tailwindcss/vite": "^4.0.0", + "@tanstack/react-virtual": "^3.13.23", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "electron": "^33.2.1", + "electron-builder": "^25.1.8", + "electron-vite": "^2.3.0", + "eslint": "^9.9.1", + "prettier": "^3.3.3", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwindcss": "^4.0.0", + "typescript": "^5.6.2", + "vite": "^5.4.6", + "vitest": "^2.1.9" + } +} diff --git a/electron-app/resources/icon.png b/electron-app/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cd2e0b75bf55afbeec66f0bc33aa14029ff48a82 GIT binary patch literal 932286 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

%ga+K#W0foi;TW3e2TF7v442|f32=GHg5w+%OT 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

YaUAT4_6qpN>@9A7Ip z*834yDLy=W=cRtthbW%V$SLc_xMMn;PoCl!BW&xrfZGrFNn?g(lBBy%`q3DNFmuztn=LVNnHVyeQ=@5a z=65}26MQ$;eFQcE=YWfS`c$BdX&qOL4|F$39!rswd+kAQ!-=$cY!FiJ4+Q|V(Kv+2 zVgA=g*ibIN$I0rUDV=vzgqf^z_TN0pUF~Wk1$-+n^MBXS>}@RT`&=#JI!nx4)*y$w zKJ^9UOmU!3IhwMHFBii#LDTcANpjQ-Wzb5a}BM!J`qD_`+GVuZhbLmgz*=Q@tRxh+!RH`3`WQCB*fA;>FzKP{mIHWfiQwfP(G0ELhK8yiCh+uQNwLO!K1{eDU_y1hqtD zjMMt7JsBBxY(Bk_Fa^KER*jKu)NLiP5QrTLC9tWrneO5b18-g`b_2^S*UQSQlGh7WdW#3E9#|O++}hkXPec zTe&k}GkkQ46jC{d=J8nCmW*zkx`_dzNc=^*c-qjqhq(5gi!zIeIgzV94@o@CfjYjE zyne8?&C=a)DPBBQJcKyDm}i$NiA5x?6`$lgIjbaV?=*ssZk~$M6FEw&)0;JMHFL|k zE+!1em}(d+CY8No(7uBFwF$2iB`Yvmn|?te>Cbd*3N1`AgaZW(`~3FJ<+W(55CA2B z^;Mg_2S}KnKnZ6x>_A&g$Y$euP{fBhp~kdTStl0jn%0lHgSxDI$fxBfl~@I=7xM|3 z-5|j{_S-zWp=YC09<_0t9IhtjXK3;?E+%-&YHv3rI!ErkP}H$64py6^qb6|>`X`K^Vg!vU;XMLEGi*C75kd~x-?O2{zEbD{veE?)5-k7CQ+%zob zqje=Fx7y6_nvfz0CHdVi*Hb?>jkG_;^wJM%vf|CS%{#{G>U9`;!@l!njw0L$(S&R< zpjh9$AgRXW;EO$3STi|7`UfI#vG5b$P1|BP$|%k)v3Uw~ZeHrQ3{=Y_gwCH?l&+6# z!MSL+ZV3UWJR>7k{wQhnjs+Sa8il!A+ZA3+{uE+@ozU|6V`S=n3tjXQXKT1cJ)z=#)S4mlD57mE@k3)MyO=Y{^oCKJdcn4 zSg_7D|Ea@~zjOvsi+TGM3H(+`1ZAjDQ`JMsN{d*Bl7FHY+fL@xp4hFw zMAHccAzwH0c=SVx!_vNNUoPGY5timn#Z#uOk~-4nQ28vc^CfP2>{|pi`D!6w?Zi{slVwWE0lUYbLEL3%{a1B4i}Yfj%Nu*eTHM`-*GNKp-EI7j*KNP>;q(R zbu?9<84JYdCy#zQ#r>OK<$dS7-ude9|NX!B;eYP;|H1$E|M(k!R0}>Q{RA){WrvPnML^hY$Zw#Q|fvoXDqWj6&RLi$_Kia-ueV76Qr!O^Q ze~MtXi#dV}p4crmWpJZ-pi^CHsXz7Y;+3~WSFb$8H98ImZYJ!exRkKNUWvc}67L2y z5G^C;N4pD9yH`}6Xv?v2xaV97jmsR^-ljOk>;{bP^dn_lOT+G6AjH(qtTr+1!@P*T zV|h?ryKlLXyetIzii;Sg;h6OuDgXW4HeezR_ptFA8-SE;gS93mog~jkeRBHJMq=D> z`)!nP)TdJR>{seTM4Nzeu?_m@o*f&GvHS6NmDO+iuLEIgeZE+RADT=kb@%-3m*;cL zc*f2V>f2b(4{@v|Ds+alH>L8xMoT$!7MYgq`KAW)9QBnI{O!{|4VyHRRm)F8=&Ez7 zrC;Ic;f=`>Edq6;DHnoJYP4+Zz~Z$6sBs=>-=3@b;AiFm*LWtLn}neNi)U z-ac@oMLRil4j4_XA_qo!$30rcqrU+qlXar2Q|1`4YdeqlF-+Af|N3 zl2H0hS-vQ}?oFL@z2Pi8l*m&lEnoUN|MoSZVGa%1aFtk0#fWmP6JNl}nGH0AjM3bNxvS6rDtop9K=%7wV`-VQDvVDj|cy*?J;2X_euEw|hCmg+PL@F!{hmxQ-02Yf;l{ZBCh&ML$ z()^@0G3vH$Ab4n^?oAFV9Lx3MZ@$VG81_rH=Vx{Mz;Ov?eHsGwOlZq2Xsw=L3{kW1 zW(3B3Dc3qAmKZ5v ppV7>Z35gcTWAcL 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

x8$NTVn(_^jAln>y<-L)~go8<|7y(xY^=QhO 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

>#)6);V@#vbXuKzW9f~ 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ξ^i%zg9kM%5>ZJfq2JpBw<=7Fh>LM8`tu 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Ð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

6iS|?YW=-`P+?ezL6e0P{xIi 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%0AqP`gZveFt=d-)~&Eni^1<$o*q;?>jFzwx#5 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�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

x?z6q>dE3dsN z`ah^M=3{CyT`j0*N6j&1@-VTwMkTFGAB&?eomZ@SiBnyUMz9e&_4+@% z^28YQit3dR1YUuB|AeM1>8;?(wu0H`#uCMKlqvd+J#xrmq{|3SQHBnjl zx^wFyz@(hrnzhO>5naG7PT!Q>Ine=X;>X>0wrRA0v)I6t@76njt+uW}`~?*TRabxr z>`>B<6jz2hc!)q<$8m5D2eS1UAKYn(iZ)p_<{hr|@iP16IC@?1`UxE9N989_hs>FbBbjA32)x zH0aAhcG_<{>8)H(bQ9mWIXUcNAgnxC7sB4+0p5stlVPiZ_%2(m7^`IFVp`RSvp46xo2ANHFY|n@L+=k zvJ->Zvdzv6S`m`B?FzfRmgUC)^4a*#AN@*peBLMU+k$o)Ux<5vSK87=%j(lj6RMjg z+8{P#FOTY~Wc<(!J4e`NM|tX#)g=>8szra5$!oR~cR4uaS>QDqwFB6mzn&BYAV%{O zyqMr1+W}oOK$LErmgYRIKe5qJG$w?`KH}oi>O?_(oAKG9(5E9hVj=_Kns*QrDc+5I=|R2|Zm1S}*NDZPi@Q0cvs350 z=EkYcxV10bT|=Zz9^?TqlHrXl?CvDuaG5O-`vRW6Qrzy7>jH&fES@yC*0Ct4)<&B& zT7Q@QP?T*f=_n%Z$Z>8tAcjnO)CJvNZ;wtK*t(Gu%x6CH8OO7q^PJyC^xbnv!-~>GO5ll%+LwubSvItv>fJe9iGa{FvWs_<_Lx#4r0_yy@nH z<8kuM6Z^)%c(;}tEKSG2<@@g8w*a2N#p;`mpX3u*t?ad1i8+2=ZN1#)#nqduUB(7~ z7d$y<;C;*^A9+0ge|y34I-WMTbkoK9Qy&~RGv{Vv7|&d+5E~QFG|o6M)>SujO&5oK zMvwI4e?t#8{%JG1Fw%qEdQ6V=!5blaP3)XNDX@*RaWRuaFvgu@1OEJtj3Yu7m2ueb z(cf{$oB7R%rytM$%CAP;UVxuaFf?W#eV7m?#yHX*P!faYMY~wv@C`5E>nWFxo9XW> zK2>2n_9^4kwIJst;cxS&V9Du4f1}{aagj&<-}~P89>4summUAh*Z!sBCH&amO_y#$ zDy9FQy*H1$?W)T>S90&oKuAbPAWTVa0tqNYP{3Af6|lM#wFSz;Docg39Z;)F5CIiX zkx!KcR5=y4Kn*HZSr&rQD7N4LDk@_j1Z4=I5avL@l1%p|H|g*9dDc4Tyh+f1uOIGy z-?R5xzv1}}YuIb=v(Gv4`4%qhRB!Fz@;)5mvn}_;q;^PC*Y>5c`8M|PRupP zfD?VAoo=bpDjdq@PJ(P9cC~^mk$<4x)B(E5*NeO4;Qkx{OYQ)pJ5_OepF{OZ(2*Y<2fEXalZivVWpzA3 zIyvOalVCbYIH$PK%k;o8lONdac%^3wQy#z8L|tpWG;uYfNFp!pvS<9iD!9BGKDOxo}d+Wc#8VO#IH>SYWfz#-|*k z7Jwy=3dDH@wTa0Ogh@9J{%BD0kSa0tgqKJ6r`=di%#Bx-<`eDYfQac6nok{t;_|er?6*;zlSd|zX>iUOG^<`MshH0_2saAf< z8l${)M}3H17Z?$jCsiL5?7yrs(kkGCchhEoY?vz6B>IkVH^U8)U2IdeX{0-Eth}0& z^jEO$5#}k^SiyjnoYiL|6-$~o63k&m*pj5tLxWO(%5LaS0VA8PVr zT|(txE3xU{OUax|7bdd7K+H}YR4&BA)K7Ah&W(;V9pdG@o$NKRs+lN+%td(WYAgTR z3&19?veNGC=!p4&&9RveiJ`!chb1%!VAaaAOF4M z#V`JKElrH5{{6d}(0sS#ew7z-?{S70V(ca1{N&zue&_g~|Kr!jU%&sa#+4uY*f@bN z-1AHQY}bTfjJ*6v+Zr2wA)tN!#1l>$XW!*6<43umbYUFav?QgO2Mr>cZHE+E>1OcO z8>vGEC+T!ged<%kz4&6cE8c%aKc;{~osUd+`=G=6Cf=D&TR30jPu#YiSlSU+T76MI z)Wm2hZ2NBtN@_!$xd~t*_`i{l0AwA2RE7pQNHogxO3pp1>QcsVV8kYEmN_I$_4%2_ z>u(rWe)7uk(%*P#KN6{Y94KCajE9Y#X7)NK;g(yZkr(WyarM=IJ6`&--y8=Io|iTg z$Oq-Td7~FWC*aj883XZwiyxjA*vCzT_q^x5;}f5_a=hd>U&>Du{u^Woj}EbP>cO-- z-Pv3FJBaDaMz+=1sCxMb4st%p zq5qnZWO3gfyYAF{2p78i{t0pzogUd-7`*O`sCUkhF&Ue)^k?BXcniRp@ z){a_Uz$>4CYo^!AkttaG)jE7%-RX~nJ~|lR%n>yG5L-v}Lk~+415A_GBvIGCd~K;8 z&5gXa;-76p!MNv{yKZ=P77U)c7KJy@a^g?4XeC{O<~v08fqb+0ay7VSD`XZ0N zqS+4snX<5wKbTliSowAWdC|g<$7~WS2~*qJ!*}TubEQYRdRmOWp(K`4H)CJE`U%w| zeE!X;u7npH^`Ys4A{0^vGu>1+He)w2$XA#b^7_Rg3!5}j2+BxBkG+f4d;!+dQ^R=* zo|`@^mrdjPfbsSwr05)dr@yv^+4Qd{ImcLFC(n|8Y`l|c`@loclxRIT*vc40X}yat zBd)5SzBQv%U%C8cmY~E@_=O3jgh|AxI;JBz=V*NX*^hKeNu<&??TprK-zv(}gLsay zv~m2FZ{2Z@fD3BsM<^e<;mEkd>30|x-Rpw!xW_(r{K;$n7z`{mc!YPiII<wn+2@(>#crwxG7Zo5pBf*!;=|*_6HiE&OzbGef9?c1UeoV9C!u@j zhU>@q2M>-nz2%ben&1CJQS&mVwnR1#QoBtWl}bu2cf*+lsmJ?yb#VMel>he2FB%`= zC+ob>+vmsgxDZt};)NilWHH3_1)NL^BIE^O<_Ov*eHelCQ!XS0iae!mZBem>PO~h( zGBFk7Mt(Za&(v?mrS`EIFOFatov1Y-*t15T7gCBg-Q}FNjafj1RE1mBn;44w;lX5c=PAA6E zcbS{VL%idDDL*55-}~N|Fa7)AcqA9#$~z3bDdbpKZm6-l?X6AD-$~puSIB)*LfzxW z_kG{@jn}^Rwd20`yDvY>)?cA;)Z4Ff@vk@YrvF-xnD?OKZ`yR`#eA%DY&}EDF`T|a z9+(gFh7@g|r$$s3NIc8g%x^Q0r^LdPAx*fIA95ri{Sj;(4w!w1B3rU3wy*q^X8igT zw*ZlE-|NwO6ci3(&LzRqMhcO29mx03Mouh=na}-hir^c+bXnj!=tC-S?3Eom*4^R@ z6vf>Qlh~pPRSf8|F<$SPki6OR)n^~fEUH1y-p9S`^MY6Ek zL0ZZlLQpFzlT*^wJ={!E$2_D6FDX0Jb*T|DL+?}l7^;|(`tB6Y|z z+UCDyHwGugObA((Aw@k!2PrQHuYlRA&3!O@)NH5~y1QXgxt;)knZYjU39Vx_M0elwv_bPKGk5L{Ay&Gs%`C z8yeY)Z&s8C$CEjFE^u@@ZB8Ledw9w#>Y~Dh5XI~RmzpxI!SFx0=D;PTZEt)lis*9^ ztgF`4cR_&!v6%CopagQsi+UE_*4^N@P~L1g8>zX($0h(AvFda5cx*BD;ipf^=g<7i zrkwI^^F)I}Q$RE-y9Hk_j4jC1i`Hudf7C@PYRQA;_6;u<*8(B^E1Y8KOgxK!G0?ok z;si|+B4`Tnn|_YGzO5S{#j(|Fy%}d*EatkypJ5EP-bGf&-DnH*(oN4N-9D2VsP?<) zr6nm=Mthd`j#uN-MYxEG(ZMx!lOuQ-EesOI#OC%?pxs=Z`2XPYF-C{da*sxMj` zwqf8qUpP+F-P{G6gWz=P{0t!ag%Vc`$GI}~*LFMLxaOw!si&Pf&OiVB@%4}T1}?1L zmiAwL(rKw%&{n1Xq!#bC^jnoa3*U5vWgPA2X7o$G^h?K|{K+4W!#5rp?|I)}jRRcR zI_LKzC(M)hlc`eO3$r%7+6GTFB0mc?r zw=R^pv)^UymUl!p#l_G05_>=Qvp+Zf%YXhax03{vjR8Zl^3`@6Y~BkX3!$UFARB7@1un1kG?a6YiTW4k_No={ec z45+oX1#=?&Kov&)EbGtes+8)YTUeDD^m=vCX>}UP;er1aG zoc7d@eR##CX8yodLled?TFim#lam39OtqsDB(1P{ST+zze~Tzs5fQKXj%btsk3ew0 zkW*E8|FVF98v*q!%yj^zo+cw)TPP;#$V;b<1dCR2j6s-2jSo5=18Oo>5YUd#a~(eR z9RMchsNd3rN4=My;|?;YuD^jt4!a;_v76b1!RdfxqTs-&?7}+(*CCn-BICG|PiMxI z9mMKn5=Pc7W8HDjUnUFia`NLOSBdUJlfn!VyVVvUx|Qk#rI_wa9JEY*CvIbY)$B|+e!6AYP_ zB|~X@rP%P;vqk(9si#F4`k=VN9Mg-7R0I;@X{%k z-0~)zEOIe}oPGr|mGm#6Yo*-*tt@b| zNM*cxS0)ll)&Cfr#f%T^rf*7{#`2Mo*wqIY4ze0c7cc5mz6X`-GY1*GYz3{VXOUQ9 z16^g<^)sEer2^E&hZq|9SBXA5kbE44|!kwVM~pc4OyMv^HP>)^FiQVmxK@y3n}z0{I%JP-MmW`}skgIvCj@Bu znOf*n84opwtbIU{^n|o?JLE(`(F^L?LG!5p)z@7+KK!8%kKcIdZ-$;52$~dV9%hl6 zeLrN2-DW*=2yCq3y&xN>Bidg^KUX-YF;PQoXy1=sI37~=%K;r}zA`OJ7D zPk5Yp=IP^|?|l3CktaU|41FL%la_7vpyk)3F8^0`d$iA)zglz-SewN(&CW5Igy?q| zyy{i28bAEQKQzAJ;*0a035fw-$68tr+<9>wTrBaQ`JKAhnR{ono!g+%E@F7>&jlij z54~|oj+{J_sCw#}E(<$p6JJhxS#lHO6GH)QXg{c_Tgse(eCf!{#p0T7myEw$lTj!k z8#;49iIMN7;F!b`pkiLM_Qr-i4jOE?9PP%-m(a?zT@|nve>2zW?xb4}&po{zjQR*Z zbK06?&7+L0$<&G8v{sJbXBZ(P44dbIO&dv2mxFVC@-8j1>c}bGyks28mmE3=#5x`< zl-{~}JqHSBk6HV4M;8;&^e<1p5TAL^uM-$6AA>n@-@ao{0GQIFd|R{AQSRpFwtHus za>n)7Uw1u|JCm#zYm7fnk<11uXM%K?`Zhi^*R33!88QzziFNXn;h=Sbq7WQ&t1wmT zEmEd}Av=hRm@Etw%EYIT{Oln)0nEt>qSBcj!4RfWf~zkcK1inGqB*{%3X^{*Ip&d8 ziowFJlT$RY=d}u~#42BleTo5zDVKNYH5N>8a?~fM((2>EbkCBAzQ9E zar3}G)zXRsw(|P37TYPCpQR7*3|LFWY<4|4q%Fl39Nm0=7x~6+?nHO5E@k0T<7gcp zbMt5NWyu%L3A?p;6CcB~!)VKxgM|f2Zs@>CG5N5Rs=L{WfTxzc>jxsz#L#|@0_i)4 z_=pdjO`@lQ8}|Ji?Xpu}$`ZrJ70pY`fMuL<7iXJ0u_s}}eZ@wF#Us*wP90bJi7ya7m!d5)JHGvZ&7UD!nI3T|EdbF(Uyda_|hRKtk_D0<; z@kpB(l`|qSmB&Bq=BKwP#~Yzh@Er%&;|OIZmify<4vH6DNtP#d6-7hO6TRbJJ-f0t zwiOl>VJR!t_9a3jBLIR&D6wm7#t>Tql&8*iL&pB0;d#?CZcvrf9R&cDb9`iei&6V^ zgyz2DQk02j9_TNf)0hX;WAo6PUBqPG6BN|NSI?v*5Du|zj->JLfH5B8GiHlZH8zq5 zF_VBY^)-35LEOTweTpG$+(smQKMt#7nJkYOYMT_SQ*G=h5Ft)+Y1 z{chuJmt8h4yw82co8R=NaXer6=A5P0%<9y~L;OGNF9`6oz`=W-JAUjL&oB*yR{0vR zjAncfyXMN=+3%2Vqv(0vUr~7Z@4RAsh>PVL4j%%Q9Ohi40{)R_yQ+TXeBm)t+E*H; z{aiW9`BFG=>!A=)rXZzT^;eC#BNu{5&{g~hfUyTbUB^eQ#J-&i62wKN%pL5!lkNHi zGu|}2?6S9yr~Sy&`Etky+AVghFT3-Ii9Ora%{^qp+@jdW#<-Tx{=eu~UNp|V$GLg; zKVJxl1KLYUDCdHl`hk=DExnt^yDxtipZ9;y_@SphWxVy$w~vDdFK|+59|ZHZ%-)BGf@?9B1&|NM31(T{r6IOp!?j8jiPouAnBomt0;id>`#?Hr&U zf5ztK+>moZ8Hr)FYtM!VI_TgpUG3(a<9HJ<^J8&^y_sj|vtE@S|02dN-e?xAsXMXM z&iX)qx#=Gp$QeO;2(h5vje>Tp=f)!aynTDi_q@1f`@C_cooZ!oAMbpm9GvN^H{T7{ zU^S-=^OO8V(=;}YJk?>pj!bRmd>~>vt2ShU52(fD!_dVSN4sptk+8(XF0KT$JRPaU zzR6Psbt$on@5nEG(9&^Bw}`C-F!A6UU1w22lov}nWyK#rGSoq5Kw8vvy5Xy4Muk9H?N*Zn)?j8m>VeCRNPi*KQIMrHFGTsvePst#f{ z27e|cMrD^+9oo)1$~fCQ*?2auShJCZJ3hOjdf7GYUR+ov(87!ecUb?u(cQE2%r_7<11@i7AtxqNQug zMWtUR<>cpG__zgzJb^PQX7LMC%KkPg}+kedzL_=iLs)lp`RV&a z5&7gH8e-Q`BNy7uFZAs@&+yoKvht*ZghKHZe<*vBGY+?$@Y*FxiSz2K7DeJ+eZsmr z#V>Fup<%2fJ~P_QoChfJ#WsEOi-uS?%OtmuP04({6KWmt}HRK51po^CX%I&VAMzp^N0o)#p&2M}4I-0)A`Lx+!y zyWaKg<1TkSdpzWS`{MD6SNslmj%8c{pOF@E^^~t;A#IBlDRJ$my;Y6y%*7s%jeykS z$4PuU{A*tQ>hU#S^N8_gZb;n3<1_nt$GuONDd8OLWZ^hHo-eOEJnnSXoq3G?L*tdN z{GH^>W-yV-okjdxam$+7-TJT0&Styp+0Zwq7k~c!#?znn^zoLryoKL&N+CBz^Up_5v zO{bR5hK!%zupfcX%cQB;r8v{XDAy1dwT-f4aPdXG~2oAHj3=&M0o!NAsfDt6_izG_4H z_Ul+QcI($J~YxMq5q1;QHVAJ2jkHX%QI7v@A+3-P&2t8Gv`mWa^aplBB2drwB z1C7DuH+1LSP8Xw`yw-(K9s@#zVNS*@bWpDWoRN*LlaO1v>~k_u1)b=J9o#J9lm{#C zFou=Oq99~a1Ia~hCv9YfL5Bm-o#M5n{ZO4*nYc4~M9LHY+$iLvYc)4IkP^aPs!%3f zCN0X^*0s)amPIVRvdU`_0AicrNzBXi+(ZtuFbY~Z&ip}2p43I$4S5$gYeDVeq$$Re z@LKlt$+V|`u;)+T+Kr=3h_6-j9mE>CAB*zbrEnD{yT~Df7svUGN(wja= zxXMOfoor$)e?8hJ29AXLnW7kp_Xblo7V6i+X;##?1W#vY8!MTda#9ap-mYzJ;%6d@ zKVa~$sYcsUY=IqS4A#%~5`3c}G6Nvq8ybQn8^9GX4>nQGBZkX}z}W3y;kA*r&`$id z5X;jiYn^Yr@nWyF=?tY`>E#BA1vfv(08l2&`m#`V;K>+A1izXWOGZ zHE$u5K7rB+C!8=|@{*VGQ-M$7iGa6_kACdRangw=@!0hVNt2%;H*(wAn*)9s-r0A# z^LWDdKPf)?7`Aig!qbmsYMc8RO7b%$rjU9m&U(&e*fN*T{E26dFaDA*;oa%?@za1O zpolQM&e_GRE-eF4S~`u0TqZ~9NZ_JzRJLzE=tVXgWv0kD-Q3%&Gnb%y?X}ks_kr}E zHY|M-|JJ*w>3b;UsUYaRIfV>1{cQbJS6#^$DIOZX`-)fKgZA`vXG;>|U5LB9Rqip` zyNbc;xF7t4;=9K4e*Ssm{DbHByAx=e8~7PV?||#0PobQ2;(nH3$BnmKdMO|Oc+YtD zv;XaQE$_JB>wpl5 z@z;!l=bxV^Wn3@jyD8E{MBu!HZrVnSSre0bBnT%U# z^6d_5o}`%0$!(nUC+&uStUT?kbwtiz5YmUl(k3y*1^q*|9)dLk2oEGN>QeRNKlu78 z$3Bb}28{!X?pRd|tf>`@tgDd^1~y|O6DC+XZQFX7f0muMgC#%X#(EqsU3u&#*6EXD zKAVQr^M!%LfsK58%8Gi?iF6%DN;T*0C{T``^jT86H@uzOmjFj|U|m_s*wnAYt5Sg? zkuTSjrWt+My!k>o=QLs$DjOL=CQsf{@C(atfBW12?cyFg9=#cuJ@M#u@R(=(w8rgk zf9NK^1dwE8!Q}$Yi76wKL7o#gL)kimJco{R%4?Kzw6z6ch)2euKIPX06zR+kh+1Li zp^w$8jP9g6+2^7nO0eAGWDovyj$0FQ(KNh!>9bEx2bmp&^&27^vr=D`e|uWxO!;+iy< zZMn+}U+DG;4eJQU*mI_z)fDhmM{6VnZJXP=CnMR-4WN`!P{k8NZ^&3P9-ms6vD>$1 zdOv-jA|)4jED*wv#R(=xGHs(z=6j_)4YRC70HV``&{}v4p~%2sI)W3 zfJ9b2QFSwd9364|B^YVEFq$?COARBTshADA`WjN0PUu+ES1p7`*#)V7SIvm5)?n6!7%alX0qO|+UVw{fzuDT=Uy=@p zNYxeJu`fdU1tJEbCpujp1(Bh?{YU=%<-gOlx$>HL(@!Aw23W_F z>i+!R8FxHmT=aPtjmLk__l$q{lRudZpsq8CF@#x_ymyOaNXg!GeM_A&7>BVdxx$Oq z&JKI~xhemg=RBJW)K`vcueoOY$!q^)eDc#*aiMr1KgVZJ@oBxooWFhD{cd+3SA6h_ z@vFb~Kl5{OxzRTbXl83_ThgL);MT|GL$kM|l=QE;_qTuRx5lAE*K=dyBV+#w`^hb{ z%f!j~PkWfz_>j&}TUzh}@;KrRpqMNa$1ga=KP~rSow{ z5_w!}myU@(O>yYZq4CalT|R#P|M_{|l|LhWuQ$ySX6(kLl^ZL2eB9>HOm+O!-H6)P zQ1GV_AMy2%;KKP#E?jOG-#hAt6S|@nsMB((!)2@b3vv_qZ>!6%+iKJLw8q+aSxi~y730c0 z9-ehO;j{y+ePj8()?QFzN0n?k?Nhh$&0Ug5cMYK;$gEiR^fmK4@x>hn}fSE3Ze|!OaRU@mKgPc$PCjZsuNEa z3Z2TLi%=&8Z3mcHLHko5(t5{SzAsPt(>=Md7iVGiBDLJ^XwR*x*1{c4EZ&3gzUW%{)DT!Gr>;vHQ{$k2==7>hpvHRkA&Z1K2gt3`+6k z1{QrVQZs33o!PG%RITNEACPejtUrBoOKx&Z6#{b8W@NgnPXwBAWIl{jJQpj<8z8>L zXIUxNNXzA`mYCQsJY;S!8 zFDli{LcluYEEF`yT#?84pnK!Xn9P6M0G3crgi>mD_xQhhkI{z4< zo6^cJUo5Ku*E>A?j8c5pAMyRu23VTU!j}Aq()P2=3y66?QK|gJHYMjpGM!vIB}(6X z37vI(Nscn`!AhgRV4;=A`YB3FF2s?~f5ZffeH;k6wo_KXf#*{)=;@b2$v-9Mv-*R5 zLx-1nOI!YFAGrvXlDcSV3i2gij4ZeK0w(*Vd9YCO?Ra zikV4|eR;&7&A)4Fd)_o2`tXO2_rCAF;LUOCa3??I^xiYRC1Nvsu8MB9&;+S6&%u^Uzbd+tk7K9{4`7sW2 z5#I~_=CW;4{GJD}lyQiCnBHtU{y1mB3ska-~@tNi~zTpky zdcKJ8#lQaRJ7O0gla2ijxWf;o7T7dPARiKom$*m36~)`Eb?Ug zz5KWT-|=y-2o=%{~N(_D}QV zTo7Ex1o5Vq#2I&<-_@yMVLJ~fYkPvNPB4nwfgLCH3Tq|Y*Qp3 zIn@0iOO)?gCB#q#+p2kvH5N7!p;XGm0$Es&_6R>Z$j6=dxbofae&=J~0bst4@~tg? zlsoyk?%uiQo_oWMM-FE|@)Tj_5+|EZ)s1`UPKbrMFlr=@5Kb4nV(D8?WZUS>HidIb17rD_70@~zsV_imeu%z{Q|^#}dZzJG~TP?_m7^JH(^+dqVb zmp%xtcR>&;Nh5pZYh*po^TQBSZ|S}#6?5KFWD zSGJKve~t;oK+>+I;t8Z)I69>SfAW>(6+!)|r)wANomU~Fhr89izGAeL&{p`c-8oNt3k z-C4(3&8-)N3Z)WXQWR%_g(;LdFlPN!zO)n*do52x`F1Cd``_<=le=+EjN&O9ZFC0SJ_%U7H zBwfSkZ#ox02Sz*j3odqwgcsT6t#J_hYB$Z>8(8FA{@(ZSMRwXTQFOWttF zxZq@uwZ72wP@*T*=`W(jsxAACQU)$( z;up-AN;V#KmFeu}QE~Tk{Y%lgFQ)OOQK+Pa)?N2Tkz@bLi$DQ8-X~1KP{|3%SQ}A3 z`lZbL&aO2}$ah{%IMY<>aX^c1@>{A`(9`hph$}H|Wle&A(U@be>jK4f8ZL*eO)IU( zTN~clz?<0=H|LUQDlvW0PJD+FIYH=Wj2V3}*Pf#fakG(|kJqApY&*&`FuTi9?&asW zdly`A!3{^42zeHdNzReSiP9SZjA#d|gFS;gt8MisU(ss1GVnTNBfTYkY_U$fy$-aS*2- zIFMIg@Lf;GRGw7PIs!9Qz^mlHeVW4DkkYg4le@)GShrH|esdy6-}7se!x8qT-{V)t z%Cwi5gI?g=%t6Kr4Ha`?=214gF0t5e8myEqrqV0%OX=PgGM@`W{ls9Zq$3w?%3dH8 za@uMGgOjAH5c|3jOpx*f4GV%?$fnQg@79K&@m7EBJ#IJcQ!^hr7JoXn^j@5d01DpN zRAyb+uGrOYJo+5NIrl<$VR<=vPKFC-)uZ{-iUDU)|-^uZvp614bW zVD?`q1_;EO7vcuA9A%LNaT2`p{_|3?W55ak9Xz3ii;(f=BQudOhw7-Q&-x{-5%3a5 zracXY(bSB2w2W^2XuSDWc%*$|B63VNrpeEm?mw`9JdkgO|HI$^gYn=8J!o9VyR=2N z^C2BOA?*}*h-K}OYbBNfu|#s*3rW7rS!30J-@5J?`0mGl=Xm#f-Z3tD^Cjb-{j+Zx zpZc3mk2k*Q5@_#cuDo$vbm6_n3t#ZU@n+sxPUp+X0&w%FA$IX#x{=DHEz+)E(NWC; zuj9Cg)-Si`KIb{(;SYcKcsF0B=XViEZjY`#yK~#bIvj5vpx2vGW1MvINx|_#KJ8Q3 z(suUKV-6KC5^p-}=Zp2^ZQd2OKT6S7{@SkIG~9o}@#9mU`b54-{8fMOd(`_CnwrVj z-Pp$0low&u5uBvOQFnLqrdK}N!0|i2VbU2WAFF)|X_wxmRZylff z)ThTcfBR#{2S57Je9Xh~q0e6MtaJM=?kd}Lmk*1!J#M{fXq$ zpBm8#Asu%XDh|_v2Yd>?ttpM8b>`J|{>ywAar=<|*q(DdQ_1{-ZpS?WbKdYgwa(kx zs_hk(7|?>}$1to|4S0-X8u5lr=*lZBOFy*h#$sAp%4^r$Sd*T}CjRzmymHgt%0;lY z7IN3?Mn|mZJEoL~PU04Sp4rW(K50kU2wB#bdg{S(qE`KdP&F8mmwk%RmrPs_L@DA| zQD-P?Lw}+!V6s?{=qX|rWZAxyc;%_x*c8{XejHEGTsICLJa`R+W7|=lfi>QbayLJ> z-8Lh7B?_5s0Q%QJCCK;~CS<2kv@Xh#_;>$tpV0Tf*kySt@ zI(1<>*>|e7q8#eZmB?qJhQOLulug|)dsDf7!bi3z3O(ze^TnFJoNrPXUV)GdPY$e?g zL94Osi9-LyRF}!QnN*)V0h;#JFAbn?5Z(n}Cz+&I7vvRm7Q~%I`bOimb%cRuVfvrk zrrNl=SkR_1C_PQjQm_OFNcr`fa(wY*x=z~23BVTHNy4OIIM~5K%(}5f5(Ue|ySaFR zu6*2%eQI`il3sfDNYDt=KktimTlw%FxaFScJ64ncTi`ZQRLquD`(|MppR(~JHtg+k z>uVH)-@H^`Tx5{fM9ONYujh$7jPR>dB-^4r&bpZTUJo*EQkJK$<|;iWCPXZ~^3DnQ z_|Kes)GuNL*^!ed(IYh?CWZ7pa5Jvra^u(kG`@P1r<$tcYr``;!4ueb$#ZiB z>o%1>#MTscDpS$rr!CuewC7mB#u^JN6J0DOjpWq6M{g`-x`9}=!8_CrIfLYwP%NrY z50G2q8v*5D$;}0`n7ll6Gk*CTvo{zBs;Pv2@}n>;(CY{WNYq1%!3@dGC)!5lLS4;}qb+mJ=j{p^vAEsrRau7r?N zn1kQ-l6gT1{iU7r(KRk~{g(PKdf*rG&ie<)MHk(Be1OkqD;teM1us6fbgTVFY*VNw z(;?(1W0G$hfQ)g0ZCtwUJENa>-_IW}c)<(CNBQ>um%r>c$Net;g7FuB`Ht~vZU~%z z@VxPV|LZ5V?TO>YX(Rn7C~LPpnrnNi{%l;Ow!6vEzU^>?KP$ic%2)C(@#%bd-R0Z_ zIAB7d(=W{9>^Xhc5=GB<&^2=-xjpg3lLMbcKhn3Af?Z!6Rco^=w2xvXg9eUl>^tv9 zG7T&LxN-GouI7siFCQ=dm0um_abbRBUc8C7*Y4h0E$;O~ot31;@p#j5JkdZ-p8x#k zkIS)j@Z9sbXt{R$CYHH`eQ#fE`GrMxjAz(ZAA3A z;nc9t0p?8tjAeR}7;9~}wReDI+|E%|na+pH4d@ovGvVQ*7Ni(o#y|T+qBX6Rc6Du z=Rlpx5q>N|GUquz#s)X*IQrz#10VRn>!N#XcT{H}-SDXP@R)OEfF~c1ABVjOz#w!Y zcA(`Xl!50--T|6_2Jc)VIO3&qaMyRcGld}Npys4v4`9GLkk{4;vt46NINEb_Ue2a| z7MaD@l#A&s0yzQoCxbKtpUo*rzW!%npA$%`o4PV_898Iv{W6q%nG86hDdz-KR~k?5 z>O=Pe#cMVtU0`^^Z~?CS#*n31YzNLrKtae^+k`DldfT{s1+ z`s?B)-^L4GJoN}$|C^gk_uKsIvz~$9n-A0P=)v(MIM0e*0BcL{;pr14C<0K{hJ;w- z=pw?ZP7}GR^4UDDcClR(qYM{<qM-GbY)fIkg75i7A*zyjlZ(Oc1IDJ zkl4A%Fm^$@NI^i1(4s_{SQ=c8vA4-$FBVkM!*wyZy8xNSw=O6=bJiha?r44O*gN1! zkDun-y7;8hOhe$s=Eb~%+7%C>=!K~-(%TgQMgT*VY7hjCUo?cxAoMYTPxUg^t$d6=vDKqUuU4AbhMY4Y(Z~W0m z4;g=|s(l_=2+2=GMWwELVpGn)b!+RH@$e^awly^bl=1}_Vkm zXpec92Cn*T8)L?^sbj4EWkDuN8S%9@%)rsU_}KE0ji8L*dKYQhBwpI5?A_J2-pq++^S}{0=~Mb9kF5($3XptSL|Mm3*vO?1 z#U3JD45i4=%=w%57hZG`ZtNQuTyU@P$A9$NwxQ6bzzuuGxT=y?Id&rB?zXl{EcshW z$lhUQjOU68-x(PGJl0n~;;Zwd!Yh9J^5;g95C5 zWyezR<=H!lWrwv}lw)()7W|N(w*ZLycx=68+`v!6efT3E9w+emc7^gwJ<9U38Qk(j z1GducwBJd5)WX=-d{!y<+kmUuy=}DR(r5Cxz3sqxQxW>AU+lZl#)kdOk8r&G?SCD&NgGW|G=gOSCn1?ft5LxY~-VCC1DOljrEkhN&7avu|24y{1uS%#Cjg5>k`JcAoWDpl6AZ9H3pU@_f+I_L-{|Gc&$thf+AUJE}9#nfj=9@tAXudCX&O;t_y3&|IcE)*NjN>P#$7iqLp<@KVo6rS4HW zTMlzl?mUO3(uieG!W#y3SL#?q69J`0}t-LuzfB6-Hv*jHJq z5fM$47rjfK+;DMWWx6>3XZp2YPJ%e>Fy#V{{cZ5-Taqor2K(jsbm} zU^FurR>N0hCLW@0CjW(^T(!6$%`>1vae|}%ctAir$&F5;C54Tjqq)PBZkiR5)s7AG zjEFqj-hx>6&D?ZAE@Rb414ZO0{MWZRad=1rGG&6mI;OIi!+YMs0c* zz?cp)3UZNiVMsAgG*B>a+O;iYWL)Wxxq||Pj^*NcYg>I@5`rFYi>L1F5!1 z#wiX&&VY_*uX`4xdl&!XRoY}Ff zT%ZQ~g~`m`YAv-a_<;IZj=6C>>@ z#XM+wS=b=dhuh;C1N#t-M5_KCl_YP_G%_P_A?FC4%1ikFXjUT{Ir*Y>~J zXQCS|AeN!j9SeK6Th?{#7AjRhu%2xBky`*(czN?AZ)$K(|CWF8FUBvw=$FQW9{gZ@ zJh1r^Ngva{nZEC@JdjFL()l+P>S<%FH3^9!eM`a2M{_MvlTaO}0Oj8ua@N)7-Wpexd>w9e9azCN_Q~5B87R7r?g6O^GO2uD#fW zr=vo-I}5Ck7^f>Imf_a zeRSMS{=9~x+ad1!(TTBjCmJ^gbl!P$0!iJIYo@KuWLr)rA7z{ty>g81#73nhU@IfW zv?1=qbRKY0=q7Tf(3`zbla7E`E+)2PuFNN6L$1Rv1Gr4J2z{y`nt!IFM(FxrdaB{OU6HS6I7EN^&Su+JK ztWLDO=ykG`+|OaA1$-MXg6IfOKtZLP6yTTbsbYN;nL4z#pfMx>6W1Ki$yhJkatx?> znE5qcY%PiP4rcNiqW+XCLVbxFi8Chx+2F-Jinehs(4kLO3yMMTl?w`D>BX&1xs};| zWDNMFZ5IcWdjg^?iC6rlU~#j78J@&6T3ky`#S4p*eEqwB+EEO@lFmqGilcXif<}$KYvIr|(3Ae;cQfw1niAWz)chN+FO^lY1!BlSn zLPs{1r@RPJo_5vgoqtLQcFbDVX6>eLX#^d$)6Y1l8{hFevVn#|P}1vb%W5`{X4;L+ z<#V#4NI_ahwMn%(qRu8lWITur(|2y#7vi>zNU^F#ck;xEDh12cX73=HbC8O1;7^3j zag81qT8W8VZf^crj*fb4I-Pc*zZgSEHZ-}Jw$mn*OI4niEr@_6<=S$oSy>{p-)bMC zLCqMUM8Vg;W-gMCUE*JTKb^&oFGUxs z{gHl}4i%Ew1>;4(@+;$A@4Gxd5xDOF z25bk$YsMwM+hcR_O}n0mZ)tgb0xYeGjYM9@>YpZ-?Hp}KXtd=KE=<#Aw3{S*^U^=e zXV*_S@x*cIrEeV%{mO@pU;4#gL_){8F^;YqZm_Afv(0{}uicuI$W&p{r!8tLg|GYb z*Ny-DpZ+T*ZXEZ%&%MWo_~^*DJ@#8-!Jp96md@wGJNV18bYWM09i{DT)89}K*&~%o zeeQnCzondG%O^*?LGq8k`Vriqc*S_Y1HP~~O&CBOC+N2cF=wpgiB#C`zS5fe98J@j zMrP4`qz@^p_ROijFysw6@!Zzf$;UfYnth?-YDRk_n2ULRiF)&&HwYpsQHLGtoGt;N;*LlV#o7xw&2!A#$VzT zM}2S=b8L9juJe#aXd~ypgcKm<6yhL%y=F14(Vn(QZi)itm`M91yy9lPk$5%67Qa2# ze5inFIzGV?<0kS|4d-5H+EoDktX0CLOkFvBH$Jz;kB=rZ#GJ)i%n%bx$Xc;~YQvZJ z)(#)KcAR?JsdRz)?T1AAW-yaqwIu`WVZM@+C`zmCU?NUbQ@p;bs@o!IBQ0bd{Mh6e^x3wN3{AzxVYi4( z_GW|4F)gNI*43xn$I-XrOs3;8PYDoWGoiSM_!>;xL_BMjifg&(`aw}na)A$NGuuU3 zG>J_=?3eLyIz}~kK~II!id9)~l8aaZhtBR_nmZQN6okD#@bVqc_z_xmcp!KO=sLdOfc39A9&zc}9VrEqcR^ zhx7RML%#GO<7F@XjdA~rFCJHY@+xf4C#1AHeF^baJA9biwuBAc|16uHRn~p|T%!8v z-`czhpn3cIH+;kYIX?ZVzu{L5{>k{*l^@MJzCH@?MawQ?lM6{77Ca(W>(<^v zb6jPgYI^jeA2oh~&$eIs*0+yC*Iu7rCuqMyDlonHz3%$!#;LbEg(nF1L$-Z1h`_X4 z;ho5)?d5_;ZvgdT15Et3N82l%%WUuctY_Z&&UcJ^UUaYVD!!*+#nO?v3uw7q3f<~T z%F4G*tu7C?@|`Pg8lSoP>hXVk%|9AfUvu^N?(cs5c-8N{iVK$0=6GRAz)hs)y1JX_ zQZ4Qlb-E#8muTPLtxtAI?u)a$EXv+}rH&ha`;{g{xAWv=hG^qG} zI)I{MGVLQqt#nMz`OWj3eFlS(b^gzowH;JZn^%%#yC9elz)}Yh<(&H@*f*uc61vX% z&iBfsRz}9T)Knexn+++dxuHN`j}IE+W%p8YGY5;fXwn@U&>Ay@@IVkhiuFR!%sZ3p zn27~LsE1(FAB9JrhFSYAUT#>8x3BHf|Z}r^ds9`yET>EQ!x&^Le4kn zn0bZj4%(ExU8es>Zj`>C@6tFt?soQF-v;^E_75Nf_JL#DKY$FJc)OGT>Uw@t*TLy9 zT?5{W1_z?&oHQmQ)(Kpx40T6vMKXXL#`5xR4+>6_kp?&K0yEP&lv9=lc0UsAo=`ih2{w&6CIZrnPBRF`N*rr3szy-Ck^0EFM{v7 z*u`N_NC=ydB&zb&k-jc+utQl;r%57YU*E-bqLxy+CCXXcs>e%J$dr=*HIYYWVF|qT)ymDN2$H(+Gw2EfVLXO{5*h1sz$*&=bjyy8v(=9i4X1j=#&!M82 zs3T4(?Sg&ess%q~+ea)#5+w9!#*P6Lhe%~p$-)NPwGUloG30B1+Vqft565rUxe`OWVbJ-Ke*ruJyPQQsDgqY=+3wTjdo^53Z zW6M0CEndB8!BM*Uz9^eUc`{sJww|R1GU`>c{7W^* z`le|_5=u&z!OaWa^xvH7yK-sY^e@WfU!ExG+cs+az&3UpmP*DWi(H};jEQFq(W(K`NZ58fAJTOPjc~p!96b=uX@$*=iS5r*K$z*uAM$~6L*!3 zW@j^Dx3S$O)tCWVSQ%sVqyO>WUo`&rFZ=TJsRR4%&^E;uVN=>;7#!q$$@jHerkeK5 zkzLw?#QN_3cYXKwj352+r;oS3?b7`2eMi7HF7umT9zyTt`Bm`gQ_FEdcgxo2*8bFr z@8V}T8d4jLgZ=DgZqghW@BhGGk5lh(`|mh!&w{9pJ_<3Ie=PjLf+e9%vFV_1w$Ok3T+ZL8^T%Oj6b0e@rG zS!#n*N!1In160;_F92i9O{urO?QLB2Upzkfi7Uqg?*9OWi{J6@Oa<3?Fq64ldB;t3 zh<%WXa$<1QUvs{7TRUuFB0F`v7C{eval2PgpzKu&wyJp}M79ti z;}dkG8>)T5O@BOS)DE-86olZE%f1e0?ok%eeCGsZ)uiiF<`U)=r=H|SSTQC)Qp|TT zTS&%5&Zvka!@B;?MW4M6B|DYNn=j^Hx!A5_!A7=!Ra-aN>$e=+Liq(rnSa}_ z^F*wo+)U4G#g}qKuy0pCNbYPO9hlgWH35?s06jw{eL#Kd?Awp@7i|4^!j~%u0`-@Q zbzaULrsO!@P`m!R>&6*po%v?vk8MYP23CJM`aS&|_wejH-Rb?;@DoR##Ik6}olwVL zN2U9`lgceQe=FI+DlHPsj2ZWt0-f|aS@j_uCyEYgC(w>^`2}!b9|0_%6%>)#p|~Pi zFR})fmA*q0LS@)zRq8RyolW(9Guc{(*NJS4sl2$p_Aa==A_u|H2PgCVp+G>Hq$nz( zwzMuxik%5dxAci^z6pm9dZ6s&1q=OpKzi_9yE-dRRv=vr7Y#DBo~RX9)HBgM)TVb` zgQtR$VIA0 zUeZO&JBTr$0-BL6&)oK1d-4!?^Q}O_)DU$H3a078Hj+=hR7}-C zx)VQMRZbF2|J-b{U{rJ}YEve!xyb0rmwpmoInJPhLQKhMTm&un;?f>ADF1HL3(TX> zSH4Tu54ESUSz7G|z0G5IbF1&^j{!hGJ|at=l6#V1-m<{M-`q|KfWg9{EUdpnuXFWY zeXw;U8}zUt(Ep6Po+6F17i!6gZKK!zyI`l5H&&Ggj6-+^kTHmILPAe?^{T8FY{DVn zPb^yQn4(BoPnE5Yl@~vu;ZK&G)(cT-a_#s`Us|^0$8QaY3KsS<7NU%e_z$9e2bw;+ zpzAnoTzW9IwV+sH2%bFi8G3IlX6}uQnsEG&^AO{Mc=`dq>_|6H`V_I+naRr{3yoCq zdEI!^u46iUR%u&Zh}e{_5Aj(!B2-2_b6U@hpJ)UZZ2Br+L65THFBNYBO}D^R9(~gn z@6qrZSH=jk8PER31!VLF#&aX(%eXeGAn5-%lvD>9A!&m&T=rF zJN)eb&Ex6Mc*c0bzyAL5wtP9D7niu;x&j~kxj?@=;M(i2;R_b|i8*+_Q|RVid|Usv zw%LP_=i_~}!FpHS8xQM-vp?VXzW2Qs?8D>DufK%&PqGE)p9n;x(LE;iRKg3z-A0y* zB)8a-+m;pBrG~Ny z>sYb9`^9tOWqZksUoyVpAzv21F1+xq35uj zc~V#q`Wohfu@ayl3VxYp&wB`@uDtETJV^YRd!(1FNY4%FzY#^;HdIKivg)HjHAwO! zTD~#~M;FAAk6&KI*N_rLk*M#P(`0fU++4_}&yy!RP|WE^W{Bd)zVihbbs%$`e)?An z3YG5-hWL$azH~R3KLX>|=eAD_9%1uo4$rMuf9Be8 z)*bJ7+2S8N9^DzpP&v9?Jm#GlIOm+ZzyHv6*Jp@kQgkurMAK24Bpv4on%T%V^78Hu zlSALhpE=Bg$be=X)gmlJ@Dh*5Bydvb7b=gB2nQG}71Me|Un8@0kjAdd3N zPMdQIRdG%z=)u=+zcpAJUBG&QZHxq=(c(=b;`Bm(Vn;wjrL#v@&)x7nEOwdfq`;Hy z1(tq0yK&SR4{llT&Z|)UFwN#5Vi7PC=;2c?Lcq%%b{D|@A(*^iC?Uj$CqvE?SyDA+9=F6h>7UUlc4nFtEN1e!Z#H; zQBux3BH;V02yVd$Bt9XWc9@Ku2+aX?F=hgC!YHOompC92f7UI6!bxL~jwce(v(S&0 zfItSl7hx7FW5ERi=o~94uH@ETNZ8zNwsmrbab?$29L8$Sr5UHkI#@qVv;Hn#_)xMC z3Q=5@kkb##Ce#CH(PiIb$oNN$`eQ$gyhjR*fk2Y>Xn?0)Vx(j=(KF@%x~pvZGUe!p zDnm@LIogjkMXd#8;w)5-(#^1&{WdouZ0R{qNRLfcTFTx`-?O!Xmmq+N|4_^W!s*#2 zFv{$-16?hBkq?mtBCj>KePuvRd1NO_d>)r)=s=J zztGx-TEmmH1?3rU7RJWGUo0%!q63l$JSHi|JLwkOa#Yvb7viYn1|Q$n(;>pjP5Vc+ zxluiBmV!R&bF>4U90#wc@t1n#iji98L+a={R@1^%j9I=nchgR`3$p1e$Yp`Z;?9)D zEo76u`MoXiIri-@a0FJ69|bbMbBJcqj(CGPDyz7W7|YiqN?U#9S1gARCM2 zUMQ#p5(fO%`1K9*Irs7IwKoCIxZ|1QK@WV;c-}9(Fz=>*{NtYh#LuT2Z>+ex)m9H~ zMgHuBB=@tkcR9gMr!))n5q(EMx_!c!TC=#aAukS>S3%0sQBrS5EsROBxid!~CxqSn zr+?Nc{J-4;lFWt<${-jP` z_VI;(j+K4;4vZ__|3NMmt{HE96VpX$JA6vA4&q2ij2i1 zUh?aW7JsWeV+ZABD`{M9w*p--yT|aB7L3%lIEscVeZ7|WIz~syO(FAb>&K6$KkY}y zxZl%9IMPS&H)~g^)|A090EBXsu|dx zH-yVq<$Csq1k! z_WETvvbrVRSrpv{Br!NiaKJ?Y8Qr7=VO__%j1GYm#F%p`63Xb0zItA^#f%~IK>7dz z(=GYRP8dpa&Yb>gE8!!sj^f8iXvS?dR4Kv8dAZJJ;)w0YRoQin(9@vC%`a5Ly-_6NLy)809Gt?zCVPerJ@(Ws(feq$84eSgmJr z4TO^vIIK|W=|Y|E5yWN^M5JZYg|GChqc$K>b4zBX-%ye=Z-Nb%jhO6jbNZ_vN3=KbbMn8a5%>#ICG-Ut!A&z6VddaFElT=%TK) z?VL*1|}{B1hZj(pP~b$TonMfY5^$U(@cot6$3EzwgLL zPt4j324&@(9|DOil8H;(v!|u)G$V#?E8O{`Qj$79Pb0RqMUM7C|9tl)zQ&Q-RS$RT zr=^e}k8>CPIpp$J6VVY{-Hc^nkccamypU_2GLNpZI&h68rpdX{FFeOt z*vTQqBxoa^_FMKeW_}F2H%eN!#YFdIxot&R%)l!*eWyd@U1u2i2|D->dhi#HPki!9 zJ`!*~-~9f9+%Ql%^Ar$k6W`5<_HK(4W?4I=IJ~Q{%iE>Ta?zWkj?KmvvnC)LATvIz zxAv)0-A~zF66J;DF=_W*0@_FSUikdyk8l3wf1XFq9T)z>g1+qA&o|BU?!UiKpq%}r zy)`|S@|##qrpyE`ZbD)OEpzh3uG! zhW%Qyb0f!Ww1XFG%j2wFsN^@M;;%ZLg)?s<5DfOtPnmll027~Mv!xAY=L9`Q5T7!S z!WBsj=b+qtS0j#ghEzNto>Qi+wO-nsuaR-Rm{i6GqatYE2hpPVlFWgP!2G4GG;dP& zLS6qUqn0@+Nvr8*j4t_~QPwA{H($EoXTykvF+^kee%Z*!mQdYe+>WC;M zOA(t}bVr1FHh{zy|Ear4m&!5cF%Lm;Pdk6klrk;Ce88P@AYHa zqoSO&g=@_dix}GP;uRe2mK;)pW72a$^l?M-ejvL&k<8XWXfcY!NjUFGPcyHI0H z&E&|KV<<4WvdSwjYM~S++a&uHLHKjHu9m{SutNk%p)O%7w zr6A-}oaQS1S2ajIv8!eKt_YOnKMR6*rC?~j^Pm?!(IK=jC^j$Y90AvKUWp$Pn(>3< zy#B#taf1^g_VB}|bF&Ys*LZ9Mtn-;T-RT0T{1~_2Oi9(!2cnO^s=7czLg!KE4IBKWCiBznVq7h${ zY3w$yjVud2-3eNAzUCCiUoJXg69z+uqjHxcC$!4^=2>ZmBiU`YF-2_HSbKW`(v?5RZ(i?+ak@ z!eVc^AH$%)=G;&o6@9m#NMKXeB&QB$GLoav2x-T^DHzl2bk}j6BfMN7WFBIXiC*B< z(KlhB1;YJ?sqw{grj*f`r&2|s+FjBXwWwCoFhNsSSU>FR#*w;+feio^^sM7lQWsz0 z1*kc%Vy#uCImpch6JdS?iS8JOIEP+v{yoRNFTCe?!V`aBeC^kKZN3fP4td0PS7Wdi zFceF=u-&vDt9ymE0-I9w6khM$RC}?q!!Py!XYWmecFWH4 zyxp@}LZAUObZbCkBelBKgIc0ero!YZ$AGIGiK-+vp^OOm14NFYTrTV+MN;8U;y*zc zgH=XJY@iBbVKOkp#=-{Vp(-c@V<7{I15(h0npSs9-RE@Yx~^xf_x(=O@S9UQ`}^L# z*LsHg8P;0Q-uoR@Id90Gw=@2^1s9RY&y2G%|IoXC==l1#zU}zsUzzXk^2G+Xn{U45 z__usZzNmeHruZitErjGIZ_HK<^+Z0@Y6)K;c$r^4pmW^9+x|cL@sAxJ{_uy7fAkOE zd%WSUHzad{8>IG6ySo_L60pV@+f!MmK8=Y48QO@mInSM25F}oaoCL1eAu*eBOLjNU zs^^x%qqAxB#_-@QzEAM`F$=d3{`v=xFM0KAkAL>Q_wh~tZzS(GhvAf{ws7o8RdpFJ z-=eQCou@srii}4%*Pm-@PoLI3{YIN)c=LOzt74%)vNVvYLV=+JzU5sTV(Lf4)tZ>Z z)q?rdpCuKbAQC71F!a&CeC3F0H|82^lk29B=<#TCtk^{FhL^r6t$)&hfLI^->iHxw z)boKJwt9hs?u@;CS!*tIRFdpLZ@rmN)!D41zIdWx^JhM*yBwL``K6rnYB##VtI`u5 zI8ZgF))-}>m0z#()E9bu?nw@FNz%?CsG&3J`sYSlUy}8`X8I1k{d`~75=iRTOq z6(4k}C8i#EZxK-p&*m_vxb>=`#ZV0XEPitVPH~fHUq5_AI?@Fe5MW+*`xzkVm(@ze0 zgq}Iuuax>3)o#etOK2ZaD>zSfVt{Smzyb|!y4lp#iFe1M@BX=&)igO91u+St-RWiF zZg_C8H%{L-;;O4k=ItfWvHW=Q;!qLq< z8;Q6omG2y7EEt#OjlR3-ka2Q?X`FIqbosQXym1ukS!?25@&EtwlVvdi^ag{BfFB*Ty!p*yplLEt#@uBChqv4-u_Y` zBXIrdhxo0GuCmsXk>eqX>X@G-Ney{}=EcokP1?{ub8SAvUSWvNzlqcd{a*f{-#ir- zVf7tayNMWpm^`}Rhck7MW2o-6q^>-V_6OwrL=_la{6L3Y?i_6M7m_{&qd6Yta?dXm zdO}COcI9MoYXYf{qhCEoqBLq_No$AF!?yhMD3JDOBuabgqIf*yhd#%qz0LF{C)TWh z1yH}o_^56W%h0GHz~U5tm>hG3+C$TMS*cw7##Kie5RI!Re!_I8FGmtyO@8glV%H7j znjbT76HTS{u)SPUWI>Hd^sm=naIg3K4R{;+Wxn+7R=(Z-0e(;Y7vA@a$4hU!{rHLZ z{3O4v&ePG_vi<6&ZR0821Hs~0{gkiV1=U<#^leDh9y5!FyO|d; zq=j;k7{#APoLBa*JHDTHVBE!*{aJ5bePDT zvl-XPuD8s3Q1Df!t=G(p+NZG;NBYtie+&*SIt!p&2IqDy@&P}p3D4yDag)wS{s|=1S=btbBcwIPI#?j6tATCw=n$0hC=K4l` zuCyW2nnZvwwm4&;0xGyQhK$c@=a=Np>0}*Wi^d%iZTg>o9U^I`^Ivk(#n;Zl^pN3& zVSRDlDT8gpnEV}|9wXQGf7~oKi-1trl$q#0%dch@^B$G zs1UmWUa?t-dOMYiRdM#-SahM;3Wj2h8Z`DwAZZ~}9=#>j$;YB{7PnB$RPqZ0_XUoH zFKvry1;V4AjG6t--gdPsCtIBNsxw03RJ6Es!Lcw2sZ+yMpB4@+HdS=q_KTuAq-{}L}!DyHVLj*>JZ z>O!6TdU6p%K#z^A4eaFqrfaa99fi=b?`Hlj^cd+PkG|VLm`6uP-cTF=jH9D(%9B5z zRrY@gLSF`w6(KQp;&^~!Q0Pp$=>uLa)(Jy7)zx*EYg}+UKGHm2`U<)+kK2ITNI?O3 zf(N3o|2m*Q8rFftMumR9pf~TrP!jpZTRm<0AX>2`EoxBO&2|~Xg5x9U#2~(H}R+Y963>qwOhY{$DxuI=gfSIy zeA28S12pqu09`BojbF|=+Kq+$$?sfXH!~qR|1^Lshq*}AH#_QXKlN|I1^ko0_j_9Bxw!E+<+W4&ysgd^G`X8#468rko>h%0Fy|_E_O|89 z^Fn$WGm1%`OwuzwCyZ08T*}xUCDJ%=;q%elJ1*Xw-+#}Wj`#n{uk^)y=IFr(e}6DC z>Y8;<%w`zI>BEsbZS=3_*9@Nh%x4~t^BMM!ed6QC2Y>T}$B)0~J;zsk`Bx&$JY^jA zyB<_E@3VQ_Z7H-bkm8uONw5MZ+k1@2>kwsxjXYsZltY?zl#TdE_t#;MMQ z(1+<>BAOh!0R(G{`9+M6@~cjN=4-$1`1W^vdpG_sdFgFj-(5lm-^tC4FZKzl+Q_lB zBA&K$(1S8cOCPhRbG!CMilN--dQM0u%>6Ej0W4N?t1NITb zsvD?#N@^X$9}x(fCO=txBe)PC_yxhfpt2#8As24d+YH!a4VM(Y_d{Qsz~~j%@UsA` zcY{)grXGZtJ+DWUne!U~(WS?qPpVr-Cu7(V} z@P#jY=z6}C$8Y5I6%AKnCzXmAi&Gl^E$%7-%h;k~so>~RwRl%(TD)?sAg*c;%0R0c ztdan)i!b^9LH2r1JPSmVXVis&fQ$m-LKn-PTP|@7hvTZ(8xV1Cd@XD$P02j_!U-tj z&@UD{X!)Q4TFGNqQ4Jc=&}!lHI3>SmEu_l&VAF^6>Khms9pG^6A05N!gtYbPr&%C1%V8H;&`e8v8$dn5t8R)4tu4ty zi!I({S#I8|J)b4ps8xJ6ZhE$jX6sCL6ou zQF~&0@G#&Y#3#bCWN>HznZD?g zkS1A{TVC?{bMDdP;h2qX^BIbC$He%=Fof}SB-cV(vBA@)ER4L;D)pC9^b#PVZhn;* zU$@>q0yg8MOXh(ma{Pv)YDc*GtMjuix@bdoF2n|<*z;$0c_*Iwrvuo_@6%i7cOTzo zy`uqbzh!o3s+rvt>VQq&kTqA$59qY*tK-#=e+?1s@=D*=tNhN8T!VQ-JmkrpdZ!u< zy`o@lW>Ug!)H&*-UFRJSIwBj}UV{lK4{koBgVYQx07?K-YB?fgc7<*JsZ?jgYL6IA z<6*-u!foz3*xKp>cL|G^8FQ46VyU@C%Y2E$ep<)}JhYLOHb|=KeiJL&vab$weK8L@ z-WY0g%#8{@3gFm_)w?ukq^GW90nagPzA@f2Ul<#JtU>;??(?7b+~YpJAn<2@?jIhn zc=;C}-}}Aq#3F#`m)}z~z_4ld;-*V`i**H`&P9O)AzTF6O3KwfD{R)rzjzd)+P(FR z(?qbPr{Ocy85`R07aE=BI61kAFX;QnKmXotNdNgipI`Ix)n+yP+5mIxd3t8nf)7l} zHU@!ze`4>2FMPrAso(qk<2Qffw~inCu^&7B)W7#N4aGe9e?fjXmrETcm#_c;KmbWZ zK~#U(Aye&?prLDQ+xFAKDBr|MG_@-eu;}X$7CI+0p)ce$ZUa}=M(b$}I~Qjy^a?$a ziIKyf`9FTVk8Tk9m~ZyQfX3l6`s3p=0?`PK zQK2G6;L*=KdVIliKL7t@Zpyf}T?HAqP{FPOZ$F1!T$B{KSH*QlBlB{~If(d}Sa^+PBIt*f|_68ZQY>Txg4=aIhaorcOUJ8Wd z!Gcj2EsjnukJ1JS4E)S5$jRcT&s}sw=!J&z?)>j(hh%D7=VVS%6S12!<#_Cqj?shw zaZVIFKK)kMSUhwJNY{w{voeo1*f_Pz7#!&2ExmDp56?#PT3%IUb{hC_ew!;tk|0|F@2S#SF*-GVF#HtOmz+Km}BE4$CMtIyd# zzm+d@;I+Z@qS-c0smg`aMYhW0!lcp|mJj+A5q|~X;YnhDL;rSN=8V5~1fN{9A0-XQ zF?Y;|AahzgJ<*JOj=5uoh!6ALSj_ig=*!vHl*Ay_H%O;FH|XHm&9po8<)PrCj|w>b zT4%~>bWVHOL`I+XTjF~aeK{uPo_|H;Fq>QXHjS_ROjLS1eqaKbJWy6fHfY*5PDyUC zn>|7<-h>Tx98fd7LDa5#^p+pi5`h}$#dA#L&qe3Rny;Rqfujgny8YO^=-jit9)hvrZJY$)pXr6O(z_rqZD+)@&<(;#9|m zCso9$MI6D)HUMKIr0UPcmhL)P$r(B2CD{|BKP^ifOj@a!1K$SQnzpXdC$()IB!JN$ z1i$6oCoRl}W9`J4S+!TIamyRq+Ps-3)jhY}z^m2wyy0%XdHtowcYf!091pztf#cWy z)dz|ZNL&-iwJ+|}Mp|sMuX#m#C-o=arV*N1r_sw*O;+tQrHoRCjeboD??ylm1Ks2^Td zrnXw^%$o4!%RICVf5H&mF=5X@WDA9A5`}Eu9SBbEZavHDtS$`&eW86A1i+JDaU(*< zXUBkk=OyIyOP|xx2Pqa$Rjh{UD#u~2;l-D?HrXMhun{=_kI?WITv&tCyz)A^`E>AP z>SM>}J@c7AzhSQ(|M+KsS#oW=N;2^H=Y7GmkH=YLzJ1J-f_11H35&vML0dgttQ05D z2*`C&Dp4687BLHC7eWw}wbFC~Sjn&ybW3UFV;lS_5D_S379lU5EQ4ZBv5LDz^3e(> zubZn=VN%44wMF-+5sRDt*p4|Ts~qn{;jbX*t8@`251tsb;fZV8OMt$(V+R3=bky!0 z5p-3V8|A^PU_#4{itXs@lR1~}E9Ix(IoAgslHDmIMi<)SL|p0X@^pddhi8PPr1Chr zPUhxfZfwe3d}NSkUnn-}dBi|(ixUH6Ir9^4Ge0WWDmJDDqN6#RzdQePk_B~*y(daN z5l?wfhn)ydeaOZD$5+k6M11tLr)8Yd)+AvalpkbuwSdsH7|d>{=j_N>iN7`keXdA( z^E|m{k4Q1fX)S%Zq~f!MkCf@YjdhJez+StJGNM$SN!f^N>| zTBB9j*eIzTZMW>{50VJugrop`xL93v;+4%)(%d>TEGFnZn$$usVW zdD`s54~LvvJC?QZs?BSH4qa{X)0mqDdpt}rE%DD6ks~XHT>s{#iTIF5lvt&LjNRrx zbGKy&t$Yd0zZn|~D-KPryvT7#$I6JSJG8vg(98qm+>B+t=$1n75J+^Ps60yn$?>EZ zbR3^^3DM@kDlS8H4;5ZAw)Io{KwjrJT<-G*SPKq5bI4pQIp@b|IXiHgg?HI?kqok} z8@u7f)Qu}Qv83`?%wmHQz0S@v0%vCO)7``~@|ARZ7i5+lrK8&4 zv%^4z((xlevz<>IdQYyMB(ur!l()dcOkawo5jW37c@nGtVln7HKDOA`N#b;(Wc zM|jqpvR(v&zhrGm&~0~~UvtG>4-YBKi#z&VAD?zoOUB5EZBE3m9%kOQX7mtF=P13K z5hyoe(J0Pvkwv)Aw_BgY;Q*ySTR!=mX?9vy*S${`+YViS_-nK6gF5DVNgN^18P@XR zhU|PdBl&_*cJs-YP10EEIvVD#&uMWE*|lGrUbFPMeq$!L4Zl8wu7EQ7JDBGchF$z> zToh_tQXD04od<5_X(JeO(K`tU%655O@zPsa^GKcL`#zrkMPKwU#JRRz9T{j|UmdP~ zZhJ}5eewk_e9>=x`q78iPZyDo*06=dg*R^mo0rFF~(Z7ywQ5HdcBNg8haN}n; zx&lez2Y);@+;HlvZI#^Aon38HNGhUq!;bj)z-g3*VdZ&C@UcIwyNRL)Isp zJWh^K9A_pk0u`Hds5&KHD|g2TE2H8bi9yL&#n2@NiHNP4xadDp^0_* z<+!ag=wPFfU)bbm#`J%d1wB8?kTKVjb{+BaNmW*U>5F!|FbHJrj73G`)vtBKY8MF+ z+l&i}Yu21fwr?2wdQZvI$sbHF+Mnd51>1TWT0FKZ+^hJGdGN!M8!;PXM-Rg_B>C75}XqWziNG_}t=&C2K$I~P>R(kr+12k1m zzJ^HO+NGDxjcF-dYtKK$G_L+QW1RdMpH9&>|LW1qkGWw0*N<5Ypy_CPlG-BQKML(Y z(#{XMQ^_DBGj?W9hdl^k3Mb#8ao){1Kx;A#cNxsaT<(rexO!j=jJa{lt6|a-4_n`* z&%DlKAVqRBxwECi07##CKx_~vXArIDGZ4v0cWla=aqdTTW#Yb$SG=YBJJy$Zhn=7Q zwn!W@=k#p$>n9fi*%eH|IDb{>e9 z5gvdjHCKP_fALf;ASJA7mRJHPL5 zAYe^mjCi8aahef&8Zb!L^Zf}z-E{M<$KQVU|8)G|yWWL8&h_L?jIyPD9>!Oiwj&Vt zdX2tK#8=nNS>-0k7T{?24{R6s^6>qN&F>I-Q=dHEeBYao>v%V(bVF~1zT<$%Qd-TJY5%R?G-g_WEd#vYUyY7xOFWC$gWvi2CWeI9^B$M(2LMH7`o_c2j7cEyj#18bVR)|Sj&CQ$zE)$#zSa`c=4YtLm8$o$g1gRDVyS35u zWZsEkuI!{_V+x`r%_1{4vc-ua8J(g^ck;3=4hKtLMi$EauB)+#OOe$+504_52mRSU zw=>pP`9x@LUQ|+^jicVw_dtIa$LQGitykvDo73Kjuyj2@?nD|&%7e*%u#lLyh$(G+ zQ{pE&r35xFWXO|drvNMqth{@0lgRFzSf((?oS1O1*wE7rVLQ^aGclSU)2ubI^#{U| zSBFUJ+w>tTFXjwgt|2wXy}Ab1qH;r+Xf&Sa6u^p78bI1KR$D$s#2W#vDl450$FsS7 z*(XYmGbS7X^oC^O@aPz$4`_MRak|PyjywFO4szFXlcbmr9jvW;Y<<4Cs|GCKzg+p+ zGsdw+Qyctw)9W0`7HzliIQE2G_-F+(e%M(m9Lzr! zt+?4iW-f?!jQTE+C_d2XLYj_>(Pm$rs)?VDhwVK9$n~tN$+mNQ`%+9hliEUY=VvTS z81L-wcQ`3mG>sFG6c(es&*En(< zKaHstSR?O~kdWFuc*7Wsway@7({KzBbp83|Q_gj5FEb;^UP`gjuG)#g)>vV_dMcbo z1e1p$wdOx2RB6jM@?jn^AH6a<8xaimw2K7|p3`+HsS)4Q&vQkoVyxo-DEZ1i#!ONM{i+kI;$XK)ve}I3HK)W0f zuXBsMnY;J>()*5Azxp-D|L}d^d%Wh3*B-BV?U(T7e~%Pb@8UwncXZ}^JA-yU5+_a> z>uEj0@my_A%z*X~TifIW>hM=y5e*p|BXI?9yNL*S>On4}`U*nyFWb zhNG9RU2Bu{i6tsR_VLZI*3{8{r9Qfm3ciMFSPc^Z?K-vB2m zAsZMZ4zC2Nx$4o@tl{}EfL7$UEd=e{uA z^G96cw4Q0G2=Y^YynN-wp>W04cX8BZzvy9QL+s_Rt;Rx-k+(LE?<$V|&bN@-<=_yd z)43JVYOJwn5CLTzlg_~e?JrgIrJx-B4uID@@S5Ki;@Wm~WT5fBI$XWx<(+rl`7iyJ z*a{Iv!y?13#I#Upxx6bP-4M*8Rls68@+TD*Gev_LU=f-Nt8)>t!b}mdFs#A@T!pU- z{z?~pbu46Vqu-$huLgJG+{nncA zBtS)xA?8UX4H>-~d$g<6mcrb~j&ivp9*p#sMKhyH&=g)z%w8h)Dz|#;V!w-5bp{}L zy0eiq_4Us&Xncyc04DAPRz3`dF#QeQnC#4UMO=I2hC@DAxXo4gbDTDW)Z1ZKd89oG zj%m6%<-z>unOyhc?~|{z#t^zd-Heq%!Fa%L6N5(=41!44Hx1>06lv}~osFiJou8BE z;UQFfGu9*MiN618$(0rCJCiI4(e-v=qk{0TUW*AuO_6+ z`c-Z=vPVG8Pe*M!kG-_9ZeZJYG>~xrPB(1XM;F80C}j^MO@pPph(%yrw)8U_@JyNs zn18j$|NM{)Zb}LmoB-lapVtSM4Or$&5q1r6){yrdp|$lT$2g+724fD=#a!#0re5}E zQu)`-E@bqrF;~M#l!}><;t303%*`o@6YsQWiy}zi^M<3U5==&PMkc2Qd%kM`<2ay*yW*m$0LhT;O&i9 z+wAa&w~lg^-+_#KgDXeO@gD`?P0bjf5(}6@RXJOgJIET-14m8#>Q8iCfU_sOTx7!) z+hiI;&?>-Ncyh(_cE1?OQ}6yrUZ1syj}*M}&iO^V^Hz9rO8;ZCn02e6Vv2HFLJ9R&9;2l9rWzG?W4lGtv^>NZ;tc} zye%(|bnR{*uGEL_e3$=+Y(--Z;G5yg{PbVnFJzybnH$dyOW@?})1n4OUV8HBg2AUa zkiQq$({zTiZQ0e4I?bikVcLsQjAuA!&fx5unAIJTi!jU9%!pxMA-hS+ZNFB-bBJ?D z=bQ(RKKdvheR%uvRbTbh{EXtIs)dk3A8F!aIJjFHU;;3x@m|pLwT&eTiblLp)mJ3*Ej1VL0?;guKP~ zoTVqqwJngjwpzNLUyMf}!f{3!O1?q&d@7-9QtjcmctYmBErJYMob|UnMleaPU%`Tx zKNjId_k^IV=TEOks-X){P~Gy;4K#@^pVG9OroPy=Hn1y?jxs{h`LAtIO0Hx`n>ZX1 z?aOr_&7(SAQLZ{Mnh0oz7?yv<9|;8E<*`rCw<#iDfAcsh*ZEzZH7*Ht*#;i^%u&^{ zB|QxF@teogXO(R4qfya8epJlT?i1a*AbOmi3w_bEU-T)pu5DLG24-en9ln0fdwItl zulv6re)y3okgY@)AxqH$!pX;iA%mUML zk7rvON?H&sPI0Mb(d=tSCUSG>AcTczD|s9^eXC|-M!&_I;;L_XN(^8xJSqA#CpG9j zV}UF(2C5f|cJT$KrPYV9a=U>~ik`maArWXh8=fGPWb-I2M-e=+^rxH@V)oU4PbmAs zajr@8VoB#zZDX6C{&Sg4tkN7CZAAsU96+z3U!DIg{u&#A0a zXSN-vQJf!H)&;|2>*R$`#;d*jk&|Z)L~&g?gsLnG<-~EiP>O z*T-mN(C_*v4+hiu?wGk1lj9)Kt;4i+-@N#i&&zh&%_XTvLO@(m@{-2X zs0h`J(VdI3kd4QNYg?wuCclRa9_G82YeW%j*cg^Q4P~=B;83Y>19rq?=eM=%Ys;Yd z1l^6c#`_7n>Z#&+4;>yrS;xQZjx{yw>!$SWuIAL+^}Bka|KQBcg7Qhw5TAeTA>$mJ z_~2JJpljbTG7!b-&y$M!J!J9r{IoSgHohyh*Oh+m8lz4=YfxaWg8er7Jq%Lyt=z)) z^S4htdFgnJpThI^>~DM7i;sKx(zXx&`Uj3T-go!$AN_~_!SRVteiE3;UGbV*{r*EX zIcKgpz#fl_TQms@USKXOLqFNb+p{`4qcYfrxi@3CFMgXx9=p36J*ia3pIGBomD+Ol z3EYJfhW2RW+X*NRems>R9h&-XvY#R{teH!rPJ0P=KHLi0g>izgvvJRjG>dohJPu&I z*)782ZP6{@96E2>v2QWuKvFMVqRqcg8eO80fB)b78^=p-f64J8|#9P7BqTX|0KX)7p(vz+zgoGlFesxGWr z*NS%F$kSfyok8sj)ssH1*?CNi;06Xi8&wS;z!pl=?5C|PkMaPSbr4;B+sOIt8RL#m zMvq;c?TL__$h|Rbs1*k5*u<(eqnrAItiEdBM~<$umxevhA+ODXpyQk0+LkRPnl_VG zx(bO%dTsV_CwciR+9&63~9M;r&pfnYGXD!%pwrktzuNW zCw^648+PQ=&sPAH#Z$JOt!G^6K=;0E7DZkrtA3ld$~(%?EPQi=k8EeQIdSXCysBxQpsB}k468W# z&R@@Roh?2uS&yoKzdwW4c*GY|YzIK0!KGEVp04VUM79ogX>`&VvM`fl7;`f> zW0kzg##AR>G3Y+&*|Z}EhYx}9=ww&L1Qp{?IuBIJSuc*_x8o&kUe%|8uc{hVt^Dcf zo%}kD4RT!&!tW$T?8+C{I5hr%bKTq(3az1XqXk9}!i-tNqgCQ6&h=zN0D(Y$zwxW+ zzy6a4HPeDwY$}VL=QbH_V&{AvIXoSc6aKpCkRsXqah!;koRiZz<-b1j2i$Ba;Q_q4 z2(mF4^VxR9AW2lKNcCTz_A4`v4cqy`$q|t-R-Bf7Uj;z(#Y4)%TfQi2TJnvX&VJTQ zB6Fp9ZDYeHd301CgcjZQdg{NqteQ?E@-RaW;6{MN1sSBc8q|vqVz^ZIr9m> z*ihNNjkNrb(l-tJf+{RD^FWg%VJ<>xvl+f#RD1M=O&7#%S57C+>Zmc{+vHr$CF`_v zL%CPAle>sBJ_N2381t73k>Q9p3GmF7$c}p#=9h*dyqsbH-(I!LR$~Z_fRrwc2`eRK zRVyzN`CFcu(fIr3yo=(nP}W93jwa(lF18#1qh~`5258>Rqa`+Wsk^?6)xcy_7m=s! zyP>N4kPAn*%mc_`RzJ22@t}L;F;6V4HrTHF=8pWtoJ%7f9J^*z`qmyk_JuExKYp1T zxSKf--+A1B_kG9D{Or#jFT4Hr;~W3NHy#f?^pIY3v+O+a^X`W6M{eVKX>017p6#Maxr$4C3OHT^J^>r|NpS&qKq&GBFx9r`1jZn%+|)+W4|n#y=HzeiLFR%Cc|Mft&y6 zdpBsyB88#33f2w0!@+X>b9_AH{`>AZ-tiaz(($4fyrA!puvW~wHl5JSD$LJMATE2w zrBBcesL5^^jwgOLUmL!&4|swR#m^LF^do_$$G)ymJSs<*=FST(2H~o^w)lLZ`p#leskK>LOu`#^Slg zkGSfI@_h#w{N9PC5o8E%c*?MaAojY(pveZRH0MTktdm2_vvF7x`DXz7a|2J>KgJiR z-g5Iz|BU`Xg?DZD$2|khnLqAMUh{7<@W2BPeB6Tm*k>Q_gfq|h$+OV0ccI&EdGBTF zik<~KHDHR&aB16MH%H{?bGai-3tipOuw=krSkkHjDXL>}n(1UQvQYCHad;LhHw$*3 z-KrO1w<=J&uVlqLpDLXvyZO+LK5COTg!`DovIfHmC5Zq!D3sZ z+8U{9bUL2=79ZAr)woE&IB~RTJkd_)7KtFk%y;9=Om@`n723v7+oHkufz5gKHqW4t zZzq%fajfLi?gA_g9XQj=A9sJIW{86BMgZBlVUs^L%abO|@fyc{ zb0A=t?s81K=;@F4^gE!@m)=9WHq}=IY#TdbNuJ?xA8i@)upb~O~xN|a3F3>yiKiKxfKk?j2G;)dhQ3y;Io zH@V%A%jdJ?^0v+WO8g8iI?JENhM4R+XxIBqYhg|z=Q_Z32^*#TB-D0w{MGVI$MI%` zVm?9EjoBtHE1L?E=Zykm-HT2Qnc|dmMa9L={_?{-xW1ci>g2B0eDH@(qKm?T4GRC> zgevEMEp*Qc20qLK%rJ3Uz%iESt;sSqRqo zp|KBed70c8fh@@GF_y_O#34T8CCCA&}{`TV| zANfd_-W2M@Q~+rdSwG%ha}_Yshg^ ze?2aMN^U z$xj>)yy^bqIbZnvezIAQ_dC~`nwakiRqXFN>kM=FPjd~y!r<7sfKGj5!nhIi1RKYE z8R6WvshHJFC2dR&?ZC5+$fA~5qnmcLTh4;Hu}(&xzO)UhU*f4`EmSEZ3AA?Hb)6>* zL;aO<2d)2ncVW&Aye43{Rrfg{j~T?#3A)a5LxM4RVCOiV>JU8tMR%T=W+D(~J)W}vY^IxBxfZ4Ote{xHDr(PGw4;ct{T~dyg zU%SpLM~vE(?UpU=8aL^&9y%k+SAHR@-{^IYQ%7s^Q0ZD_Syz*$`;IVcms_oRUR_LS zaXliZPi|iAT+bI(Kl12jk6UlK^=Bep+g&vo*z@YDarkrG(a+p`%dL<5ZJU0jtGmQ z{Cp)LPI964D?wA>DrXmbF@x$s@=XFXI?<(VwhSDmCr%D?_igbTpUwR+$OJa$#Jl|( zmYAL0xO1;?OX$KD^x%zQ*^#yF!Sp6#yO3{mw5?vaA}*ay%;rM{@WaX0_-fLO$G2zu z4uRp$Rv z2+oTOWAUM)_Qmvws^{p@=O7A#%@6qLh_B?bmDgJboa`;VE$@k6Qz%WoYU_iaqBaTu z_LEQWgcHa)H)f{o8k>2ep9>`I4Y>5|io+UC-oC!!ge$k)jgg@jAvS_(0d#`jdDwX} z7hS{vHv+AcayMfMXb$&@d;HHxHhl78)~b0PdTg6Kq~#`UiNFSt*T60AtjD9z9zEeSE{{@)A2~)$(M|==R+?kKVo*dC0bu)ns4+xNwpSJl_ zNSu+WhK7ob^mBaV=Ce7uk{%;<%F_uMkMtE?FW$WfBdG>a&h(&4v+0gD@r-BML|<97 z%<=Zv0DLEb4D;b{tS2Kjt@5iGdZo+U*)|!jaf1=V9M}#h65+$ZtGV$Vmi!rG>&)nR zf!p&3LbXQ47K;rBW=zBv&rLybc-u36NN$SgNX{Ukh;5mGn^D)d=Mgz&KsGnhh60+> z9m}n<^qYS$%7(?jh8*qk(9X-hBN{ z$KChbb-d~ouR7lI6Yt>*-(G$E`9JsPk6-=OU&~&fe_wtz$1}8d0AKWDch=Yyn4*YH zdwWLLCXbW4a=X8eTg@EE7iVoo9L?Y^(4|g>m!Q1xHiq(Nh30x=yyQ7JW_!!V>Ns|@ zom=)rZpv)ob?k7mHN?iyxrg4K3u~~0UaIi5#Y13%l|J)(B+egag0YnHfo^L_b?M7x zpUBbnmhoc|AN%O<9^d|L-+sLAwXZpT>6d=tc;JCI9j|!Rt9XKlUy@Ls3A(bj&fnAn zCwB|VER6vx^d8VHKebN|ks!9?nb9F~&sRAo23ifM^6(5h|EF!9CYVV?Jxr>vo#KM% zdT(3fX{_eSRcPSqX~ssL>llyLJG%ROG*0t89q<))n?Ul>&n1nI+BIrHN6>?!UYjID zr<#c``Y;~}PzTSL3zJ7htr7IJ>jS;fP|NmQ8Pcq8SO|4U2eE3QwJf0(yV9O>kQZs! z&&CM3{Sh;H+VN3r4n*E56IFA8RTykx03xV4qX9|dF(-feuNA!AIViWr$+-pIRoK{Z5=+kZWoXJy%!|26)=p@FNO{^vWVf&G~R&5l6oZJa=tVaO$Z? zLyEHN`~8rDp??)MdfLW_{_YFPi5V?zE}SupP{j(E3=e*JOF&@?oNYwuold6uJb+UM zb2o2Bdr7CG<5J73M{^uUpQad_m!#y8EPeI~4Re?@0^Yn49?+A6TtH5m`E@MxF+1iU zT`5G64r11JY{xr<=6}+N56=HEi=j@XMXH2BP&*=v}i7EC@{Kq-HcPW zJn{Qvlz&pk96aI`_u|+l!UFIj!zX5(gVOvdzHw%3?2~lsO=LhzrF#0k#-*Dx`Sd3S z?Fl*Fsgo>*P8S8x_RUCjK$8n^7P43fsS9XxO*}AMyI$+FW*NpleNlzutB;z8JA`?8SsHZg`j+Kk35JgWkE6 z4LTJQiOQ+TMt;YKp-XHmG{!@`-pSzHQQUq1=8~Q{CuiHo_~9emBp7+KA1=zetIf9j zC<>-8V1q7GB(V$7pRW?*^+H~9rR(I_$YPKu|4o}EG`J8 zft~f&wd6S6z?WaifBdtTj+<|}`FO*fcO9>P{p*jP`strO?z`{aM$r|k)K{>W&Sa!S0l`EFG9hpX}Wdm{YXBvUx?~?XiXV??5ckAft(wlc5 zjqjnCWF-}tF>X#o8M?nrWJc$2{Fo;PXdBm34=)TQU4wgG(T8rb#AbZJ!LB@HtT#$jn^(N)ExZLYajg@gy5d_` z)Xi^@vY7>}z{o|YJ~@B^CG*D+?QQq%hvdZ4$BG;0i|u>9W~RJ&el(Y6b6XW-0OxvR z;*_Rb%mG+(Wl3AI*Lg#+!PVg8Q@%OfAEBDK0?lXn`rwREa=uf6X286(LoDh%_|Wek zZ+OESejU!W?JCN^%!jMU;m>tf@4WL3KlSJ%kMg;j`3$y;+AHx21Z%XC&f@gwlC9ua zsa)_Z)(SIyPp+@GB5NufrTj32@gWyz=w0OYC21o#@jEU7 z<34n>dQ|*nfH@iVhC>@8<9LRezQ#vtq(Q+kk*1t`f|*O^-pK&9cY1gfrx+G4W9)1A zbJYtUUZ&XPnUJ)~leXz^nGjYm`?T#i%!zM&??%ZJV>a=5&E;@3G|=Tv_oa*BX1lE; zx*qk9aTD?hFF?q-a&xol7>th*T@PiK3$*2@K5NtIg{G!O`y}=v_}@qUc{w1}20rihI#+JicPCUIR%J9R+-z zV~oz_q4pw71xpUyGk4No6rqRXay!vJ7# zIxVX);Ap=1SEu?WD-CJ$m^vO~{8Cz5khai7DFJ;;rV6~{Ql^s^-sg>fo2c#W-aXzQ5s{4#mEe2FA0L16tNzsSWq;yN96$9_Kh4t3kO-hULG9NK zcQqks<=h*IWm`^uJe6;MXu`IQ;E)$&v29iI<8pON+g&-E!y3)_8+$r%whajzZ%|Oa zz`W3uEj;_P&XS!`ilab{RNbXBMs_WW2G~&dq;9%kekl#}40^F|I|DSL*U>U_wVBY3 zW=|jytmn&5J@nx5&2Rta<7F>>>G2~!`ge~ndChB&d+&MUamy{Y9G`jo@!<9ogF^QC zYR8%N?0i4npswV(vRV(WH^;&vT(L46azjy5uMjcb@w4^g&po@KWy5i}-I$%tXVs%& zZ}UWMh|S%SUx_%6=?H*?E#|rwU2!VRIVEeFnxO_~XHR_nF@u#6J;wfCud-dUW;F9) zrbFLhD?S=ML9LGwr^fp(Ad9zp(nsHpK57Sfy7Tk}jjYf&e#QE$fj*S}hokfo%7)#> z%OKb;KG9IkOBv6@Fy?h_O6>G7YJAwGGktKZVe*if>oI*BpUtgU?hLuQMmG$!N&|Gn zR9+n9wu~`O!Z(GqG!yDi-Ch1>%Qm9`)U2t=;W(iRhQO0QH3C1Chh;=N(>mtIogVr0 zL&ukX>6iX)>}$KLCId5`SB<;Z9KGX?*Zhlzc((0&E^cR0TMw*4J}eAuRT7pcpI3D8 z^3O&IW;OfQhKzsx4n0+bYZ$g&y(|n?hJosW&jQU~TJUTU>>bxL8j zN;LgW)XJcwzPj2^Z~+arG+4%;)2okgF^_gckp_>LJVAl!^`Ef9`>-dDMZ7I09FI%C zqbCke^f3m+Qap=SB>2n!xs%(4Um$tx3EtR*Fa>j=F@E(7_6k~AeR9OgmpS1?Fc(#* zt80!pX0xNn*jzVw^QuFF#a#;7nUj1F!9MOZ94j$uXgMd_8{5{?kX9TwA4mcqVpke_ z<~WB+46-=eMZdg4pIjJ(7Z{G&yj?yNP{%K0%s=|c;KKw=ALd?D`3taKbU`An{*WB~ zt7!-|*`;V_{GiqSy!O;=BCMpSmK^kQgp6yzw^J+K+aohDzK=z$>jQSw?}?Uj#08YN z8H8c8NuWu6L0){@p|h`#pt9@2LY{Is!NXZ=NBuQsJ~>fr9%W&dvE7nA3nPGpw!v4Y z96HY(JVZ=YgBFm&InsF8j18|>BosyGIxdKN1=sj_yMDiTkfi!+LyrI177od1ZHhVL z?mU~iBW`{eZvu|2#`s^cdBJH09G^lf|0>QLqrtZ3$@0e(My280$qt06o$9Tc1})R& z+c@zH;TZ>Q0F;lW6|C@}GG_Z#JN*q`QDx~{TdS$egVqT2Y#WytNx6ezM>+nUYJvy4v`F5Hdb7Qykfq#pOjyI*(wKOg$7+jy)0i(l}<-zwjU(O{IQSxTB zca$}GovTxWjcLcF$+3i5IrEw0rLfn#a3sd1(>$kNy@u-iYfQzAu4(gNpB?|^vh`Pd ztF7*m?;LKQNr?F3&|quDpRmM52Iuu(+%y7er!o(AybIJQ^x<`s!zrpGpUt+v}OS$b)eQ*r6l zHh)kt;8C8w&AEdvZRy5l?%aH`a+>BE4aj@`3_e5yXw#R;=My|3YM4_S!NdQga7S!; zPw*t@!;gOE_>S-R4&R!5ZM%vxu&T{f z%*}$lDIoiz%u3**_n{7r*1iSMQ-g(MS{5B1E_Mz0pV3_eB!~^)1MO^BYp5{3Q+p=Z z$c!77jVVi_>>G0By68oyo83+V&^$3=TVE?(nDky)n*L;9HTBa+F-!x3n}Tfe^}j;5 zd=TFYI{Dh+;j6RT*V6?ciBes&o-o$AqW%brFiu-ox3Ipb=N>MK?pI7Oy2vPR#>l2` zsjnQXDFB8$prD+zK9k#7X>5c*Kb|VeiEX=>)#KCvqgUikx?Fgu$6s;F@2ogjFvA!h zG(5gGE`sxzX%RxQ;l$Dl?Sj3)i(sFSuv^Z-n2TVIr@>KhyJb#+O=^B-b=mL62w5+* z)Sp~Hh`;=&qYvl&5MPT1x+a_ps;u^?BGZlS;F`bEhD@^hq^j{L;y6iQJN5N;%WFtr zwhrv(#?f4Zr4K5Cmk#aLmipct?+e55jAiUEF9y{Cwk6Cw8!|pYcCNGMAAn@^n+M)B zhI*Cbau*J?xiAqBDuVjh9MOoHso{kpwP(IJjf5H^dbauB_RSZgmPszQibsDg<6p&z zHcUO#&;3pfft(lo^0l1w1lCM*p3e_u(sc`Mx zp-B&&6@w!F+TJ z1mkv=HGlO3;rSC2MK)U0KrWUMQs3+1p144^Cx8`GwskXev?Uv|yoK*rst=-GWQzbC zzj6S^yzPG3^}us!o{QXPf}P)oGBJyzS+NGQ@yEjcY_j9?1UHMvlQ(kC@H-i_1s?yN z!#?|&&m6ZrGgQx5++qeB+<{hT|^YA@Htuy^Eh0e5{*9b2Mwv2(YJ1 z9{Fz&>e`uYBKU*t&7X0zhvZ4kEUeCnwl(JHP^)sz;g$TIHXFOkkv_@BNkc>yI06ya~J~ zkKg^sM~`p$Pu_k!?|IKV-u*wmnyKNX^^C^$vLAO8p}99vYNfcjh|o{x z)imTYL!{{@49B{$N2HG-C5plYU!7Azh^c$k;Ryt6>mkHsmM=-uW<4q9Tp0e?9W3+6 z15kY#hmhTp&^=$GbPe5H53Li)w&BZP}a01f{ z#g(b}>5&D=a;0x`R2&j!*sY7S{6(!T8|qDgwGhrmD`VzmOmv~TXxY@(@==qXXtZYq zJzcya)2RgJys|1EJuBcgH4-|REhQQS_`-JsFqH5e2VSyNFPoDOam<~5JExqJw&^Wz^mWPG z(6~6tVaIku7jOE~n>YG2%3*q8k}i2aJt5_DCTk9LA%t?n62kcCJ0gs;OTWMYEqC&D z!ranQ7gny|rmtHlQ66Cw7rSoWlS9{6SybvYtS>^(Y$-=7LF*OPtve>0hm&{1a_qQ3 z!1-FpXnWE%V;xT34(8Z`Ag*@Q6q|9-2h{CkVjSaAB3fiv;5PSWzBmc;odGQ`nlaZY z$uoA;pqm-(%{yWks3z4g)$ULEF!HQ1uqBawTWcmEFKWhD?Gy+7o& zBK!&N)ZX zb)$fG-1|g{Ev-%lrE1^a_(99LxaQlFT=`C&gLc9lLuCt^e{Qpu#TpCUST{EtS*T`4 zOXz0IugCSQz-YNv-0A*V>724^Sb$e`SdgUvR@BVN8+vC%ZJREZK1Itg$T?b)|f8)$~pHFo4;i=mSqnw!6i!$f-Nh$p0bR{rd4@3h)O zI>}l?7JzfuM|VXWW}9)S9Z!2{Zvn2Oc;^P>>zR9gv7fJ${`CL;za3xuHD7bQ;*~Ey ze)#YH@Nw^b_Z@G#=icMyn{PTE=bawCL%<1R%@mILH}A%@KVxR)@Ofi-{W1P-87DBzQ`@jVkTC9 z=5Xq*V>^BWrwiDdO&sM7a$d;biCIH4<|S0@X$)bmTSm`%_k_HO-1Q$p6D5J@ z07OJ?zV;%E1p0}HBX3NcXyW;=X!`8R8~bYRAmFcxL?VAX9+q0O8D}i>LZfziw7Geo z=l_*aCqo_eTuiAmw(k8?#&NeM>i_sAd7_=nK~C4}u1EPYRIXP4S83O_t1AOdr>o25 zYu-NZInVi*FMGu+?tjKJp24D@1?j?dG5fHbwru554CX=_iZw0fNX?BS<;XLYa#L^8 z-phR66~NMmHw6F;JN^UHMWbay-KvGyZX78fIi#=+SvJ|}?^)ecx21&*bYZP86Me9HfH?Haru>{FJ5GDoL6qepr3)LbB&v9W(LjY zhCG33MCWW_Yoe0|KiebY1^hg*plpos$=Er`%|!$p*f+5A=0?ehi*{=IZ*(F|&f9C) zt;6zPsm5%iC;r9rq?&UmbOI*}HuWCc+LC>Kn>#T?7gIYjgU_s;$?WRF4urfqk9-m% z6)~_eoXtH+}TKQ{G;IoO=TNFLgUo#_~++K~(J>%AnMlqmG>w936GDt=1wsl;nlO_csa|5=YIGjxfi|Yr;AUVnmi$+JvW2E5p&0crKj!I-8It{(f5lA(&@AA_E7 zIa6+^&`{Hkb+LRbd~NLn)86#~>L6;oJUS1h4SqSNH~wNdCsQiVzI&x7u*65tt)>IB z+cKC>wQ9myrg{nm&2-T;3_?mNHZKR@p0C*-u@a{vy5`o0$DI}HBl zZQ?x1(B0`g(VnL96s@O`xJbHE`pVvxyGXs*hF{m6ZDJ&qj!P+(B%N)}H!v^ohI3&L zZ^!p^VvebEwip=N5dF!k9((*@CY>1XAoYInGQ&HxqtiCoSV8$mO%Fhb{DZqfH8eO;X%n)ep z{EJUYHlC-6Z~`AK0dBnUhU23j|ERiO`GF67;O7Fow!3OF;Pkq-U1b@#_uhN|(x)E$ z)PMJq7vIjS3p|7E!f!#du;+?_)oO{_7Y*L7Q1}$=e+tQ}470My!K$4jV=<^GazJAc} z$pCfj6dTeuE6>tb71KwkuzHAD29}z1X;F0rtlm0BJ1uNPZm%o_6(x@v+%>WM_)}r# zO`>dFNK&w+wD5g~zZjNG7e4AF8alnvF)p|4Dgb>>GxB*9zvSVmmWt_JZ7*O>E+A^W z^Z7L;(Mxc~Sc(=JBmFEUh|)GlGPCo7Xfh}cG#o9C)!i!T!k)y86$R(!WpX+C$jqds z(I^z}oOtK9PZ*z|^2Tb$YvPlx6EmI$uR~CbkF%umViy0(VornAZ#E+~5+1n{S5j9U zOydNvIOv*|{NHoVuKEDz>&`jNp+VO(0nay_dNTf?u4pplOi|?pb@ME5J3QJ+U!Y@n z-YFn&ZP2vPrmDbR;B2bCNH;wGMpTdZD_7=MGCQ_zWb0E?v^Qim*(Mfc$AbPqKFtTH z8gq$2U!jm%I*XPAv^708ajdHmbhCR_OvdqmF+PqDNN6-Kh5`h-`jkHq^Ry0(8QK-$ zXrIOiNu5!?;a&jg3%)jeaeb}|fUg={_O@+~-7$y>PVY#OrURkFPDZ^(ey;Bp=+ukx zZ*rlo{gg&|$0Uo*uISrz>^+Z)OBph~?Cx5rf-%YH=3KBb&4ETblb2ClqfVzhr91Y+ zk+`?iC&nVa1WxX74nqp+Wzbj~>Is zArr~g2-rlfWbH;cOH2&={GBfTyO`g$xVbnCM55%>PML10_pBsT$I=IOT`U)(D? zPm#aKTkQ*^;@|n{I4Lu~f50zDr&RwDR-VFo+V0b!Um^EYzMsYS9OtX{elLS#$DrWi zb;s{N{P6Lkf9HpfAN`RZJ$`|o@w@4!>yO*Grh5Jty`axL&v!fQkI}60HP(e;OrC>{ zyeE@s&Bq=a{qZY@mRFA7To8bv&Y}-QKINM^6g+1|*W_R-8Xf)96A#EquQu3X)Bu*XMD9hR%tg z(Hs_g1T?8!#esOzx@9H>v-GS z-ges${J;-*Dr4g=K0D@Y=3r?6Jkq zD+p`hPZ^O7VHJ$TEh}rmpD&DtVNm1M0h4nRqQs^_j>SOJV?ZeI zJx=)}$+Y0al2>X9UT4VywftI8_0xxO&#`CJJA2MWtUqQgT=^t`Tv%aMw9)H2e54i{ zs3^_NL)svC`)v%i^mXiY(XOs11sG!s0j2!ceTf<7p#SJf+GEv?GyS1s8(H}o!s<#J zI4jz54gq`szj=jlI>FfDp`&b=Ys_sY6W}r~KxVceNJd9<4w4-sBx9jlr?g-I(EjfrWq?9dhCjZnXV!fVkhj4}n|0uu ze4JB7hA%xCs3pXEl46ErY-tyLFKWzRaVi@m{qjGa0vX+-?s?SZjjwOs!NwDFy4e|A z6%^qpp^liwGx^i&CYpBjN6dT(y8fx0sUMDb1f|bwE6&w#Ucu~!Q5ty77n=GUzdO-= z!37nWQP)rLFZRb5*HvS#@8}!1QXa~MeAV5CI5PPxfjOWIkK)vffb9AI>UN0rj^t7kE*&nQ_;TeR2eFP^@$~otWSKpT1 z#)wYX($&JDbTYc^;%?3eZIj?U&IAAQ&3Ms8e=fS0oN>Y#FZIcwL_<40=P(4pIGVpC z$9G9 z-0_;%9B+Q}TX;+QW5-|n?*H<5F&`KBiZB22<9pxvy~l$Oekz}CL}zaP7Y_K2m1Y_< z+RFndY})NWW3>g^O{K5c&J4)Ii?rv>Nqpra+St6zD%<1fDB z_`nDM^>OFx?mX_l_x|IHUhpEu_le`P{G!8UFRDx|1D-tqwtZXv8p&+VHlJwlMHb{; z0M7gT<>-q7I;8`$XV*MRX6F}OMT~MzH%%aD`;{~--kX)wn@Tf@j#a5#Dm)GN;V zKs=TQl}_tox-!Qn8OnN~J!RCHy5>CkO=mGIF!~RT77yCSu6=w`PJSPen4prG0FhN` zZ+6i0RN6ULi^a8Y&mO?%yq}*sIX<)xs_hc>^g3GRh-2d68cgJ<$`wPNtFviS_kqzT4!s+iv>=2-mi& zECUYXwe2d)!1sL5_dNHzzx%)blCpHuf{x4bEfN}RA^{M=(*Dq3VU~}LtC0BaL-)v1+NjUx2ow=TZUJL;fP>&C2P*7RfHu(f34E29)J4tXB&3H>#x;F5SGHYCJT3N6IHmJjGbpvu_+S zJan8qt$>E8sAXI{PZnUP?sG&w*%4Ox9Uy&}RbttO2)ZMZro6zO1gwh(6;f(H zuMj$4u;_$cMjYd8DMhDLExcq;{w;qH7{8D&U~LEn4u@u=T`vUfvn&5=y^jj^d6L92 zR@;XwL-}NB^wEigBW5Bwc60G|VQ#Cqa@br{1-yxEjV&}~HipK^JLEQb&=gB%ktNt~ z9nx~VBsqz^gXwT-K;@>0ZdJF>X`>`u0UG%Mu`}f z5jF<128^k#gS$=PNVj>YKyzcpRfwSA&m6*@&w%lBRr5p>{K8L)j)ELqUpCAEZRdRE zso{-ocoJ2aIpWx;v*!!s+P>qic~YMIf8GWmFt_2d+&YY@%3+e>+uc$@i!dLdG7NkK9JDlkq5lW%?)T^=B3v1 zU*g_Y5G(XEf-xT&|LR9u5Nt@cAu;!@Dg>8#CmX3=l zcI?D)W_43q3@E|;;0OQB@%R7!dyfC*C;!3m{`bGX&+$L&)@L2J-+tTi`Op4*mH7fW zMvU)fNX^^@Y7YJWk~Nd*&XYFdj3%oMDmn1Q(aO>be3@~nX2MQvEGz)|`aJqcL-2fR z9OpVQnj+Xri9=5~K=In6cb&i-36Q|C?YiF7sVxGy&79r2^Em4ukYxiI<4T&Hj+T69 zFwrq#wm)M9wHCnA*q9R``Vi*-q&LGhp02PgGWTTkSbp%qBSNz035?0rfNezh zVD2wt*1Y3SrWyM)*;(N zYhTionmJ{NI&kAnHy!W$gyH|*S4!H1E2luXCHJ?_C^hN2b$4bz??M|*F(*i>Yu zKqVW0yxE@(%^PrrIzCop7Q97I@1+@!7ld7m!qCr7$DaI`KZS*G#WEnYcO^$=@MH+M zVwMl1B0QY(6%7kooD0_RqZ@qXDSFz2IrixJqLVhd=+&MQ@N>nRerV$=g6WU#fwArX z0^!il3X#t;!~yo~y7{1O{x_XC=2>`%(U4Zv0qTe!+T_&?^Ty>&uUr({Kq03CUPxqV zy6}vk6IeLJ0ZyUpLvHpbFafD zCCAlWH0KXG=GTkc+CGj=gq+X&)&QBTAOC&d9${?scM=Ax@ZCk(9jL>1_Q>3$k-r7 zA&LXCa}fUnD?W6bD6x{0&N+vXlanaN5fM@n`ydInQ4%Zx7RFX=Ox$LdX4?&2-CbSV zuBu$ubwBT1U!nYg559M;`M%FHo-yumkMV5Y&75JK%O7_4u0o?=6sRu;wCzx#9*HLa>T;;xH7GG0 zByAGR4W+9e2cqIO<1>%N2#);|$K%tvD25mQ{+m%MR5J?Tx^Q<4xprQ8voE8pE;l;9 z%eh=^uQ)8E&29~~@o-|TUuiFX<34g(H0sYdk>Ajy%1ofT@yo*nKc)4I-{Zxe-zhF% zzN@5W@z;ogQ0kRaxI$*{z*05`D$uqEI2(In?(YbIOAJr*%)wL%W#5R`SdEv_nkm~= z5|MGuzIfGVa}4xpJF19%K{ngul`k(!++u!y6+50vaWp_Hlb{wrOMAqc?jYX%Ut(Z>oM|$UxX0FO(51dx-BE>v>aN6z#u_PwE$T$08`c@>3$T!3aZDIT$qctMyFl^nQx=qN>zqraKE`nq(VcTrr8;P0x`0|lz$d4a6h zN`Ete;`m^Xd_FB)iy!SSb~YoHqNK>u;@=ezgAj?)Z?Xi;2^0 zvDwlo_}x=jo!kyiS2*{4yWp!$V+D4A?)>VaJe<1@x?M1Lg?pV0u;*PNxf%bGy?_ft zY*p9T;@(9f2{Fz+5YSK^6lYZ39a`-z?*yj{Z>o@|>sXUl8jHmu^-FHB>*;a)Qe$rZcF)LPxaK~Rut^c7^1E|*B%_P zAve_WtDVU+4nu1k8Ci1BA=^zf^~L2;YH&cd97xSR=4RH!DP)s-mvrd6=Cy244mg$)k1M&bh{RsKMDS zR~Y3^aJh9Pk^AG7^X0rLOLebuHZMsN`66dpbWcAEWKKyR^r66DI z;vuMQRY0I@-*%C0-l!M*;XGX>da~`1GcF@U*l5}!9>DZY+@dkI_aK4(xnfnRZQ41Y zexDP^qnR#b+InzDo~7RT5-PT-Iut8nti3WjIjY#$LwV!t(p$T69}vy3K|iiXt`q%) zor=}+Wxs1|K6Fr^ z@7?~vKlrKJzx?$7Q(W)klM^p}^kcWL`}&XG-v7S$gZWmUxbWS6BCr2kt1b7?9k%cK zkrZVPPp}(D4Nhmd**Faz@qd31hmQ0w8)>!%*n7wu7us9CuPG>DG+gm5sY|S9fV58A z1ykjqABZte99zc;3ig`_&J#+G=`fTEz+Mc?jx*O8xuE0PEM(UYb5(oh?zB;tfEIU6glyv0YFE0 zuA|o&y6o*X%lz(5BK=Y6Q)mj`er^4Z+eU111 z{9fm|8LvMhQ{OHpI=HgcI5D2af||y`3hcaq02cM_FmNUfVEf~!@{Hv2-``#_>IM&r zjc5B8R02Xh*#ZQc+&=frv&_BUX0AQ`mp}dKPyf%VJlP(v4AifW7v(2x&A_*P+qeBk z+~EJWU;lL<P3r4rrG%((FdOxvjg&G$7-9$j9#YD`3?8NBuNS%w3hlThL`H;gSK1XH?Q&6-^Sr`|^ z>!#a9tVLOK+xT%&+&A+N=HZfqFmz^ZC!{UkVqzZ#ZIoe4-9Wt|x|UkCy`b1uTonZl ze0;jdMRfA)MTVOm>oV(Ipt`z)$3=-ZPe7vtiDGmiKl(<7I8Lfui@5Sb+{66A5d1iT zb;aa~$@cPxPy4qV3^=_c*3p&XPIxCN$prvyK$5?qP0Q?TyJYZ7#|ylJ;US!hr?vtR z2{}rSst9J->jci{#XZ!k&DghTZ=0^VPNLR{)n=5qM{K)alvkqxgQdzSsk-xm$DCN? zRr*ia821ZH6z=VcH9+Fr63FrRHph(_L`Fj(Jy=kWpY;g(r@x%M;*^7fmlMl#P<1l< z%iwHc3__@Pp)8}TY`71$L-w7oqch{;9&PjHSoWyBF-4?(7c#5XbPn$9Ms~5{d zSFrwGTM8#_B2W+Mij(?136!rsxjWP6BfhoG|B&ovufs)Eeyzz#>@(ld;iKTogj#aj z+zHiuC*G$58RcCu{b$epEJ8w!Te_| zjX9QsNH2hSjp~x+WK7^6vrQJJ0aogmKw; z(oVd?4drme*UtM>vH0k%`Y->%7jB>bgFoOu3Vg2oypR73^vO?t+wI@_fgia2N&avB zpXA}d_kPcJ-=2Tp`_3pR4FI`Gz$|UE^cL@o&yg`NBp+7Uwuf8JIcoCZc;{sK0NJnR z#=2{fO{!b;M5gWXdS>4Q-Y4?l33T)Dhcz`g{j*>C=eM8#xu3oL%+LJ8 z+duxre{%a>9ss<{zvp|NoBXf&$VYA;`RcE^eekPZ;Cuh?yS??+Yy3R`a}IBFJ|?2g zgQWQNVS`+i`mX?=CFWK!1lK(6rc@ndQf{14Uq*sC>+VHfMNm9x8M|H)t+bq!%N(5Z zqKZf7ny;Gf9JU8TCUrU8WK*czJ&XE?!e%NnrO zxw!5c#iL~KQD0d^cNT}|k)1PASedA$YCI?;TrT`WpyJ-Ynv0rS>Q^C-D7Db2d?_?4 z?tE;B)0N-yb9BLYq$|qN_!}oWjDcsLd*=2pe)(VC{^DQw!EgD=pZv*R*Zq_2@ydW* z`DA+xGw_%H(qH=1|M2Jj(LenYf8yV~z4rPWe7(_$xsO!35IxJH=+fvccG^3~H_s_B zgT49a0?frz4M)Oec8X2@4AA4F!@H=a>8wW@bfMy+sBQKxLl#{sT!iPhC)U?#@+>m< z6PR@56U$@8Dx2$8HDdtkzNlGs;l=`4CkX9p7fV$%0`QxrF0WAuM)x=?ju@(UPQW=r zK{ofZe%>_TMOmJGZ1woNE&{wL?Yy(y@udw=hi3?#ycUBgDtG!T%uZ!kEFa#)j7N-i zGk03W-2#0sK;UhE_&!nZ!WI95HDCEQXXsrgy|{~GQqUjXLRG)CWO#TASHH@TD^bdc zM9&zWm;N&Fh#cMCrQHck1bMBaxrE{(cqegSjV1n+!L{ruQ=YkDw~I0Tg(3qX(cdv% zr)+p`({yZ+Gi1x^lwxB)nY=3okG|+9{kb(lE3)h6$;&@S92>ZH|3zKnx!ztCw;_oo z19NyJ9#rLZ`CE>i+X(sX2FWW7>i2PIQrxf)DOE~}RV7$+*v;=FeV|#sGiQfxMvN2l z-TXNG)H6yW>JJ`vH#7CDjNNLs93vu!$xDia_KPk^mciM(ZR4oZ^>wJ}Xj=?gkzf() ze{_qfah-RJ;0YJGOJWZtkQP6d=L*hz@;jdPBRqPyjrl3BK3UOCIBkIKV%F$mjt+7G z&f&T_(rud{CyUp^30;SMR6I0<2D6Jx#-CnV(7=ATcWjMUJT_$8IiHYUn95Np?z@Iu zO_GB^JGKi)I0q{2UHl`zcn)f9j+^S~KbU9RxH8INHAV}FdhtIXH6Fk?Xj(|Ox~i?; z6xO$EF_z9#<`F3SXJ1F5Y#jZn%Z7t=(Az_!gzPj#$Add3DEXkYEu6{k4@Qj$Jum#d z*w%90P0ygOiOzQO6ZBl5dtBwq`BHzBL&q0(CJX!Cfex2S$vHUU^CM1*?y+x$8Hv-$ zIP+XW+p`{dAn?|kZ{EK6#V_&t!tKi(zrcSN(AKXAeB`S?!gt=k>GrMP`YpFV^Jl*6 z_U)hi&f9l>^4o9U_)Y)%?Rg#=JdG)0~t`NM7gPcf6^3 zns1cG+k@+3)>P0dk?Y615*k+@8~yYN4=1nlNr&I~jnCYE`ImmFoBaR#pZ~MlKmLVZ zxc%Pm{O;|I*WbK7#b4}y;e#K%efX-F{g?(X^mm#)>CD*yX!}8NQFvxahHeS(M9LFoeF%+_nq;Tv{VIGmyx2S0+ew*s` zrSk~?x+xLFs-Eoxu(fGKuwDHqniyenSy9Dp=%nnq|F{+ z^;_LN7%l4*uJgO?@8@euzxWHkc>5cF^KZQGQ=j_On<6~f9JE-;;s-_r1FrHgLTfH_r)Y~d$5pSea!0?dTOa4i^~bYo8o6Yaq0sw zJq=SmXsM!fclC#p`S*NFuPT(|fyBQhY*wBw3KTDh^& z#tY??IT&|cq%2Z!Cr;Jfc7pb5CMMlecr$vXLZD9k#=~nT`04JQZfuDc#JHueF~9kh zi1p)MO}`JHm@L+(tEMK7Id<%nXOFb+3U&SMS!#(dwpmH_*r2m9uid#75ErM@Ianic&t-AM&mAn91Lg+X z?2lbg2*bOGdCaK%AN9#DW+(W$y|~z4jMJWCp-%pu<_n5`*Hpj5abJ%ius`gEebPp1 zzJwmgCle$4>_bJ7fwKhKG}`L?VlNob_K5_3BS?47f%5anAAp*0V& zaIV+4`khrKVuULo3XYT7#b)DbsBt9UCdiRG0LK>I;`MLWx4t~A3 zO0IiA`q(}=Mk!EM@F@l9ST>NerHGc{yPXgM&;<_}{LA&bsxu-KlmqBIFk>zUi*{#; z9x5iOA=q}ODvFyii(4q>r@XDzz1(GbNjjQ~#*-v4N7zFg_b$I0;#cM1L zo94z3x`*??0lGUmMRH=kT(7(7Lf<;Q!`}6ec5)=l)X<;WBeujkKSH- z>FaM_`*mM;`-ZQ7iQ`MR55Dlh+gE@1!`yU#;P&E&K9qk4*warxNBQu*lC8kJHV({< zbvEMh-|@Zq=2vd7zWOR}T)cYw%y0bW?Kgk(H*cT$%&*@*{jYxY_N%}8tG9pk=}+H2 z_dCCL`!WwSH1?tPhd=UR{Jwa5;r%b%KKP**;_lx}@MHaNy~V#mcF?F@4$Tg|;NamQ z!2bASjdqqT{r0B>Tx4==FUP`)4h;C?%aGiJR^PR>^(=iG z)mf~mP}li2eqGrwS{_#(nzqfgOvbZ_aXK!p!n4=1^}q&rb#0|Dw)O#I?L_gsi~ijU zWTp>Zc^5*QP?tb)GPqk&XP(G~wsIjRmHcII=Fz8I`(>)0c#JET@YW=9wD~;~IqvK| zJbO5(I+GfHlj(;}x%lQWLWt>0|CpnFV*eNV;hSS8__f(5a^DWA{WhCaB0S#rHbH@C zt?5%AYNpsBZMl>Y_7U)zy8{nzw|9?J=q?=4A|pO zw#P67EG|zy|NL`b`u^|#ffwH7Uk5$sN1BMk>FXlmf_0y?Ep*NDWIdI3abiMug1L_Z z7fMeA!^w>D7P2jVm345o`vRmg)*f~OqTs^QqH!+z?hBWyu3YMSdRI}O!w~wq`E=0~ z)1}l4(}$QIux-0z$nE0Ss*T=R&*pPJ#OVU+Eb5+g;w`m!E(h(!17s&UeR=umO||^Z zD>>`HPO`Lmi6-Bnj6g7_-NXD%XI5&LwvOVQs5#=xlfHi)MkO42FpVN~@>qt{yaVa; zy*Ib=pU>q3dm%Xy1rpT|K(!4&DNr$=0A6t{{$nqsn~4yM-|cLWoyRLHz3;Bu(&O{C?6;PwJu9_E-9@m{v0CeLxwN1MR&d6B(% zp3BW0vRHJIQq=lX0LkvWCo{_B{v7o$XD!69V2s|hCmXlRZVEJ@V@tS{d%!~8i;j8> zgrjij)S808)7cFxW#rcb0sknTUFglrGrUv2957}ArV|l7~@jv+D3%3uw@Z#;ouX>Rij2AW*_^$Cge8k^8yu$+o!op^4CXJhNMcYdXfX^q!}cY96d6T3{0AHr7d6-u9WDzMoi&ZozwMMSle zmpa!bH~RisPikj<;Al9F+dznbv2pRvu`$rLIKfem&2!g2Gf0^rrBxr|!><$U>_VIT z7c7;2(I?N8W!0sHpw?Y4auAuiBxm_lTPM66!yNCnI+qi#E8jC&KTA5;hLHBQ64&(* zTuVfI(C-+a0`cKt3;(<;NRfkBIq|S_Suk+1pH35Mp`fH(d!UP#3Ug{Qw2*B*R%te3 z>sAqq=c^>qhK$!Z04!GXL=(1=`t zutQ~A(TPBTKPLVT6Tj6k{A8Qe6WM3*aI$k znRpb-G#9|@cl_dbz9R;}!H9$Q)7i%!b0GWpc+xCFP4(+VfV>4nrpN%ouN@&ad*~)d z`DC0pU5`T@|*42Wth$$r75s(tSSq32o^(9mc}mEG(vbCu=`Ieyf-`qx@R%KVECm#z;V z8qiow^-GI~e)Ykop1ZlEH2%41om7=}?OHTYuWtVEhYWU&C3C&0Hhufvyv&XxzxwTB z&P3?6j{(#R`PRGdMCsz1*Rgl}tF9TFH$eUw{KgjzSI=p053BcZ)DT&EkyyKa_>p3` zIXHe0`suFv%GMxUW3W&UbcNZmyVm<4l=_tk2g1qoeCR0U#Wo0(yxcxm0CWyhl^0;^5ygZzk+?>3GyGdr?ze z|7|^?O#9A{8mWJLTQO)mQT*yU*YnvP)itU1#XUjCu@>Pz@Q4?>0~#-LDKoK!GGFiT zw9ff$b>m$i^%NMDWycML@*Bu-g;&=Ao0wUtoz*y*X>d?ncLapZv@&D-)i+p8NjUVp zd(E!i%2&A}@kL>L=sC7jPnHvbwvFGt7Pt5y64hkZTTvgk*1L+fgA1l}d#xwZfW_6& zwkxyRXapx(S`Ew3%6i+=l4C=)X&$;2;o4QUbX}JjWKGDFx5t&^&|J%7`(3LH*j$+( z86JDp1ZZp>9<*;g^56eGbNij&`Yrwgz=!?<|623^3iQeDvCF`de*^f~b=d>qkAL&W z|0h1q_t(FHkL|tr?prJ*e&_9c506#ArMdG!Gk*)l7S)!sfVRM06B`{DfmV)|%B}CN zoUSTI?y(aaVnx@IVxdL4#l}W3ygFCGg;*Y!!C3$m*t`v1ow+no-y=UNw41hV@A^(S z(AQ0`EkPSFHr*I0SOz!>rhzgwEGRpoNLpD7>*R=7woZctjO6Do5;3zT1DK=m#CDpI|D+$dYDJ!L`U4CcP{;lB`sps zX7Bu>r)qvNGp>x2hAZ5@M>x)%{~cpBc+lBgYU<6g4?5zZLn9%_-M|3h(B3r!xt9kT zjU83vO)r)wO&{e0ZWlYKg{w3DP^m5e8@5(O&aXBCqZe1zkEnh+q0oq3Z0OsgtJ$)^ zu}b{QlYYe1XBg95&zW`n6sWF^wM@i8Zu6CO{Od~Mtrf&smk*=W8>ob40%f$xqj-MXW_`&dw7-H1rbI8JOe6m>cd!u z${w5Q#-b-h9?y?owTOQdC_7P9&^f_~lxcR5sqgJw4A7ZIjjbj2k!@yV&L}JLroIAY z-<74T8)3HFdvr<9gUgHTR=;(+7urIb)83KCT3C6Qcp2$?;Tjw2&C${E%6I*&5sl($ zYpfi6%Hji%At{#s;i@n1InggYv&^4!PXK$st<1Ri##e3>Y3H}_d$*0YaO7cwI$(F> zZcsu2u>sGabJxb2|Eg{#=!wbh9&kwtqa30JSe&VCm;sTzP!=EqNCON3xH6se6}C9`K!hINEwr zczYXt{<=81=E0Qu5d#~er+lzbT#(eCGIcnXKk_JE>qAHTO_{osn85Y5F*=z|mTM!v z&`M8n{e_L=JA2*58!tIiI|}(aX~Pg9yZb{d8r0McKy2F2kMB%-?sgbxlZvrg=ID?8 zV8X5#24a0WwxPDeOh-y#^^GUV%U>Uq=&My!i4z4G#M8EH*-^f7Deqv-cR1IY76gIF zayZvK?wS*fZjw_wWo7nNb3h+J>sMepKpv$+z&&b0=Kwx&J^jAh=lMG$fBMh=;4kq2K!YdSW0(Pl=9BF)&A<=-&=3CN=Rf~>Ca2x# zWMv6zRdm^4y1Ej$5H_J%teVB1Hk{8c0v4`|G4*(1bLrdOjZ%6?KMWQJlw2qCbCq@1 zi8SObq8FYz@?o)Wkq=WIc^TEiOf|)|S=n#(7E2eVM()+dQ6^{6FFqGJQ*bSs=zAeO z%`vx}9XeNZljJggvsZt%wp+d~2=rwp#MZxu(_v;&pI06n2TF}uPGRe_F=7V1U=`2b`McjNYyf~g?(+7c zO-=nq$GSz1p73_zI~K|f!BWnTDi=rfb4Uvq&JW)WaMMfcf%58!DN`p$n}_SY>7r$P zna{7Qh3OQav|er6E6#)+KHO~frlDhk&HL6B{i5f^^EDUMRHkEXIznI^kSpvke+7%v zY7z4y3MskW#p7^%H@>SE5Bd}20;@D{tw~&F4GONlgUlLMN{f*as#>e0XX$RN&4n@v z?`QtYYA!@N1{{)eaaVtb=8RxW8uJeG#eV;qEQ|5WDSE`}&wYI8-g}}r>cht!)SX9i z=;gdpDq9>i4J?zp2PJX^@~LMQC`uq2w-$b2{hANTuZzpsIn%V%_wd~d(`7gyeZ16x zlLO>5W$D8@BN`+X697670Sjg7{T=rk=mlt_Z@Be#M3z< zJAKc-2QBc#B23e<2Tauc3IIQb^{#KNb0htX@A3n_c>#uFgL&%T2|!H{m0NGU@qU)E z(#?IJ>_V63Zkkm-zTV^=`qy54oqtF83Lo|V^6jnH`98j%4&a9V^|#) z`1LnlzrFc3|ALQ?@AruTbHT^=d*A`qI>$(mOPaX!O|5zlEpQqK^MPh0w2(G0|-+02m-6*dzvaxT$y!o+fGL48THxO;5j_;);rXeQ@V?Ea;vioD{iM=_J> z*FIBd45v(0WceqD$0QxVnLXMG#I@vF*^#m_2y9)cKC2v!FxZs?$Bpu1tiD*g0bN_| z5to@*^BTTjAoWm>@_`wDD!Zlt!TjttCwi<1q; zTxB*RwcU*NPYbcf1{sLdV1qsvGC5Or}@90iHu793yU-`lpZ-3_FALj<-$@aKr;F>y*EB`;% zynpzIfB3V%&jWz3@Nv$kIKex4w{4=kxGLG=$;7R~MMn$tCNoPw@@#4M-32GVi`$v* zgY?Ym^mnn`Y6?2|f!pE_KD@~6ySP@AcpOqom)Kxz_49bokc+#(E?CyJiLdej5Q&e5GLbh{jz zm7VB0h8!D8I|=%fI0Svy0c848LCbvh(%9|jT;)=AmnWSG?p>k9@>C;aU8J8@ZuVe# zBC=0YXyqn1_mi11m@qF~(c$K2+|Usa?*5UQ`d|&^R!;IfJx)6%>n6i(Nf{PdS!jnP zz#Cf;jr$a+AIw>hH^+u*0??t!GlPJ#I2oS~!S$I&P3-O*ET>X;An(9NrVSjsp&QP)Ap(dncKv93YJD9$0P7bpE;TRTQ=*4TV_W?&vTdh?^^ z^2$;FHr+RB^0KZ{Rr`yw{*oicUmW14LHS0+z%nwMbNq<`0_a!EzK&W%FzBfZve?TSMqLW#J3#6{%5 zY}}uAV)ZBYX(7i?Q!$(XU0&O0Z8J}s%Q~5CZ4VXevXmR3?X5JH-T3@as z@(2W7`dR9B_ttr?7pVK@-~AyIq`kS-rW|m5_gy~TANlbxE^m%}sOTEwO@QB~@^1t9 z#{lj6`Sr~PA3Au>Y~&&57mDWVio?Fz+!u50J#ZnR=I0p<5W|Oe95EALiElDE7UXJs zJ*YkWe5esy?AvC9jCqCsyFRvV`rTJwPVVI=Fm`>ij(XP-dMS1zzy}<0hb%_aF+)x| z;-278&`*x^9xvIjNhp8fQKXX^n{tx|+8$~UYi_4bN_ua&Mp=TSf#>0uzJc?7>c5#bYK4+tmTk9vhNH_vd5eQM{4oB)(A9yp-md}hqIPeL2Uz~65Fh^WwV(fyKSth&%k7ihW0wKf zt0&uInt`AAiJy4)xo4mK_g?zCm%iy4&%|s_Sl0e+&1IHljxBH2o9QU1<7DR5)=>o) zh5_6c(J8cWxnw-D4CT)gunR|vhrmIiDUqEKkfI|zSfRJTQ1(QiPg}CnW|Wds-U;5t zZ5IUH+}}CwL>w<6cG4~zI@Iq-s@M%PQqIwdDQ!7hs0W6MR2lBxF&qg+QaAJ;p& z>L^dwlW(|6jAdAz>mSkDS0iYv9W`Pz5>d8C+0`6~y?eeKBSADzmMs~l-CBR0*v3ia z@#=!cHY{S);!mxR?v7|3Rx#K}KIVhmUsxGobNWEcwCZozd9s;gG3?5-3!HLq2fATD zMwW+Syeq-dMmPQNTbzvnm297vN5^Czxzpsh7!${$!eGw)fU)-Gj;0#hU-2iL{KAsM z5*@ee0!<9JVHwEcy2qs7IZ21za5WOst2SiiP$M=~y6DjGaNkGfX-gxmQllVRs2BhvAyH%)aH4;7W4cdd&Dk$eraNU*63#J5cw$L+ajH$~IT5W8^=AyGuonO`vspr6Xx zyXCEBU0nWkYu~QpEKoEKic{sqCQl;bgBu!W?$e;mHNmYrKvH6fn(7Y z6SsFQjq2D)lFj;oL5H&>&5Klr!*cAjHe%wju8^x`bIDJAs_m)gbwPka&2`F~4HU(& z>D{kzghz8k8FBl7Lo6|+xLZKc*hcB35}Ey8oRn+x1_M1G${Q3IZ%6DjPlQ-JczeiH zZx)x=ty8BRtSd)zkLIyk2546VkKW1l29r3NdXW-`m@yue*+)<02e|P8Tz%(nn`H`e zI0c;*P;%8Q9Gue|VI&V^Dvbks?>HUu6fCz!aNKzcc@z#X)n(^k#lbuuy&Bm`6VAO8 zPLpM~XySKqd97AM#d@9Zv~}L;=|H5cp5wN0??kw75n)_wv2ha-5duMc z0yAf)sPF+nJs}bGH$f|G{Xn@q{v6_gy?_3y)4H;&kBIxj zSBZ^Xxq%RVT5uPC3qBeEdg9=ea)e$4w5IEyAJI90wS|6*;gMQLfOt@bYh?FSZN5x?C4>yuDbs=-Nq{ zv9r|0pbpr2prdb+BL_9y_h|`()K+ZdI_v}lFSq)WaT)d`?c3#FUn)-L{;)xXTzNqT zGUfVas`pB!`1YQpSOSuXxR+Ni8_oy;NPqhACb?@TTagXcY?e=}YdE$Fre;h=R(+z=rVeu2+tfeB zz5^WBHEG~H+y%`6z8i_-MKLSSOsm=x5uuFY;)mu&FA@2m!C^Hh1trj zyd1D{G7o6s4Vx1wjs!SZdL{??t8Xv)(KwDf98UY#uQ1q_Lm=$8!>u*(@`lPWo05K) zfcVBs=M~g+4teAy{7Ug#R<}2IX-Xd*`^528*&TIZM}GxzYohr&C(YMAGqj*#FdU0V zD)D*m;Vc6Pr>moKE{Bpqo&5C9c3O|{5i_D3!dC%-LsE{Hj2Jc^#6*X=uu+3TfuWu5 z;#wc5sMexA&H20X;lH|#ZSvH&E>wCF4xu@>KjyTYsF7=S8>?>E)u9Hn% zr#PIzXXjpkL^-;iFO`z}^+sxYq;Nk+kPrVxL2(IyPJe?!ETD`)j)xhs#PAAe%^H!9 z<~?}Ed(FYL#S{v?Mh@Hxb#lNYbz$T5=J;T|N<|dY1y|HC} z5TLJLO^vrtJHku7?P$wJfqHNw~8>-s-qpo>PpxE^2T z#5Q?~&7Q+@BGA+@sU=@Li>h5NVwKf!$ERzH@*)C%^J5zw#xKpKOn31|CM)|({)t|j_s0qMl!$?mKo z>h^TqWr0cykqgOLC|y2!rvV(-92bu*TrO$}1n*Odoe-0q6ABBcH$=YExr+xD92X;A zo^-W;7GAEfVFTQc4K3>05`&AC`rc)W?@kiRtSM8?T3F(F^Qf=+@eed@qfPzGkfT*G zT)o_Nf33e)ks zvFP<_3H2Z#?;-=(liOJ8fEUIt@xD9>uFoe&vDnNZbkAn)j>SoCgzKv2+5ZES6)@hcLYpF1~fn%H=p%a)7Sb=~E2<*js5- zLU#EvCdwtA6TW%XYXd|;?7?bmd-e~^EXad-(l`}^`jEJxOafS#!K$#_h$zM}v9OX2r^ zmlMsxE?M5Dv-ah@>cW?cLyD5T4O5BM?)Y@;2>+Us&~@Fh3sie*thWxaUKn-Q2N z4d+*^jMUWyn-a%b_~6OizV@bah$^pafcN23FpgVv=OVE1)K^y>e)~6TN7hH06&r&Z z0CW?NY-3tQwnH$y>S$k>Hur+7+-;XMSek7#9sjgjzvTK-)o~&BX-x;!x%J48x$|ea z5ZQ`hPOTwbHwNF}{g{Adb?!vQmttbwgmx@uP?6rqj zkFIa(?!o!=@?frc8ROyvlXCeB_TnL7H~Oft=NLRZ4CwGlrQFiH;@S(g)eo7oE5qf> zrZY0sjIZOOhdh_Mi6ute8mj%-Ly!E!ulTWLQf-W47?uY|nl)Z^gL7|q5d+(iyEV&r zNna~e*H~*HSNoB0VAIYR z8?hKUmd2~=T5QS@*L;4xhwmQW{@P#rYrm`IlkM@%z((Zb%m0r(^B?=MAN#e>zx;*% z4bOcdyMBUS#!Z)QYT0GTHv}FNwcLy3nqR5gHKlg76i+l%2*j( zdbYlKGQ-9NL#@^ocjI)OL~Im$3Z68;@d+a$?|)sWx8@dx{Aj3gc9#YU?#q@0FC1*?WA_(8@ zohRk<)gMA0BdQ+u%dE-?Bs@VCDWz;Ski zlJdnt_~Q*5wH9*^X{>iO&k=)8*hjrjQV_4^!x(0Qa2~i`GM!%CVyi5@AlgNVzR%{x zO8Hr?${DS;_xC5x#Z0+n;K$K^VN8uTngvK(1a*c-b@(x)azxu$>Q^BLG0^ebca}O; z+NY|pB{4+>A^_S|)F4waY<7gCk00>No9$vRmD5x6?hZrCQ9-qkksV9yX)HiT9a%!zyl&RcRuy^g z;9j2WoBP)8?B@66LMx#+A0@)CivzY#F}|%gHO(~-@re|=`O3ju2;!8mG z(f#=ZZP&VeGKfW#h1FBX`IiGdqex&B>aRGVTjli4vDHGHVz4Y;n8u1&uyQdqA-3Te z1w)U!Hjn5v)>2dtrE6VMmWvGY5ihIgHGYkO`nBe0+Xb{Z{%X&r&MB^7-TtDXUJ&o( zK`isUe|356j4eQHVzQX)@*~nmn;g_)*TV_QSDRL4+CcP)rygp%{$EPR_c+u{7aw|25qw9117JzDi=6~bC0dlGg@5_%^0P3uv%2EH+ zO_Sd#wysD4k2&r4d@8YgQ-KY1Ka zuHjYBfZ1FI$@M4T1{P)TH3@TtH7h}nmBiT zWpeXDsyvtg^-uxUIoZj-H44!oapVSwOnmrAS>1#jwAllXt-rRe%^c7@>T8R?eAHZT zXzVY!5Kk-lpa7=vBt8ACin&os{-XQZ*rh?IsB3nQ`tCy5BWj~JN1ects#C5Y&`~@#m+-7?=cmOt zm)dv|M#_nj+PdKAs~Z#B$8I?6MXXISY?nW~&^BN0yC{p#k=q_mno(Box&T;%cIaR; z3t0K&S^u7lG&Uld%eNQ@zy77JhSzvO;6ao9ErdaeyzTVP5tM<-`Wv_O#6K36;`Sie zScS~-JYt9E%w*(}rN78@FoEU5q}gH}K{eFmzZwO{hz(pI6t6rSJN&HY>_?j2_26qa zu@@_i%N_0IrI;SdJ}u8)A~|M6d-niu_r)fDW$g09lm8?L*`SkxZP$+%V2ZtHa=cI~ zKXIteP6f8vR0#Ltv$1g6C7_D@yP2}Qwv)Nqq@F!_YkZ$vl>=q8FOTw;n;&s;&;VXX z=*9k0G=j!@s3OGM@eu(P58*x~VOcPFYUI(EdK}B!0ib%g(q1={GYhI-d+NnR-IhVJ zXWUut69--mB%P@zF+824L+l|NRl>*~piInT zW9S436oSa>uYNIjEmh&zd!~xoWE4x1mmY1Y8v%{}A~45`N+}n_ay3D5u$)mmHhJXhyi*5z z^BAJBPgeZOs5S?Ipg6Im@{M*1X9TNYo4?@tz)?{;%?sPv;FT&xOVPBHsk^=GC4nwo z1F^|9S7dMG@_>#*Xi|@sbR*8n$u1xrvem3z%1J8QdX3x0w;%?DzxW>2)%0Vqy5dLB z`({}Oz8n`8W$X_{VL3LBtsnVruMyPBUVaV_+c6lLc$OR!ju+%qi)lM#=qt3Jb%gKB z*97i0qRpLf&Lzr<%D+dORDPP^x z-}#XPPRH=VLIHiTr%bzLKw5JdtLkqY3uZFSh5BrfBU*67rk;D->!h-T3=D!Y=9(N2 zRC1+UXloZPf?BO76V-P77htvH9(!5^7sK;e+PPFLl z4|Sps!1K>Nck@4waj)^8zw*i}zm)Nl-D8`9)BTSv|0l%Bz)Roo(trK3ld`V?IJ=$h z-f*yJF=@AGAoGObR|H()Spj-d+(OOb;6lNI;4)|177Hi64T-LuiY_O4v8i_)TqD^$ zA=x?(9MDy#jY22vDroWXK;1V8^>Y?k|4V_jivy5EGn9QypEfa2+5+p6xC;eiFuuJ! zD=aq8Gh)W|fUEoHR#Z6E?>%m>#w2+;q;Nk;VJz+bU$Z{-0H)s$?_+Q3s^g0wXJ>xo zq7#3I9Tk1+4_W6&+N*Jvvaj|d7!8NJSdlmfTEgz)2)*mwQPuA6`WRAZ8lfW={PslM z+)?M{kH0qO4i3eb%`j|_Px!N0kITRtTWaDRtm}*;&i&tY<>bP#rL4rKC)JowVqwff zymuz_<>rMDzB<7{*xba3^7V1rGTLAOfqwO3)b8!VC@)&UTF(8WZsQcFJsPKYVk$OO z?Q`)|J40r}=41g<{jC9g#C!ZgFS(0b2R$%AM<}ITrGP1g<;FHjIMd&g(Ih z@gwJJFI<>MRP5MY+tt?GP}e46^{;Ms9R%9-PE`-xM5k@N0w4YrgJn9SoCd&^>rON< zJ+7{!dVK;WM=<4Lp7@GeCW!J@tviVs)MC8i>_2u$= z&2!!OXvwcpsb&9n#IWH)ZU2{DsT}b^F&!i&Iyv2d@amJ$;5V0G-UBO3#geuOsm&hH zCm{9qA$&Ecet2-XeDbje>iVZl)($fYV z%{sT7n2_~<*(Fq)T9oBi-GWtq90ZP|(`YVyV5V_>Z#=DEQW(!yw01s2xp&+nyt#}>>*(gl$itOgeqqBq461yqg4aT zj%@)Olgiy_3k%FBAKoiFT8K&8>{Rllog8UDJ~n5-Qd408*w4JklkAks=Y*O{@g{U- zcB{e>e8yKYedRNSwb^c5Y6y5-8ry!dq(9||Rv7Xb{cB(R%52!gTK){g7Z$@b2A!A> zeN((J52#@mIy*;#IzkRd1M%pClLV_LLx?QlL<4 z4p2}>gk71r*if~x3oy&*BfS676cx@w(_bNW^NT{m(LZ0YLS*o>3)6PaZd1((kp6|+tXmSb0?Yxk2CZMJ7~ z%um_WsHv8G`Z`0%5-@&GZ1#hG;lZI(zi}x_?ZLO45KT_G8!rXQ!r~}zU-8UM`gT+& zHg9AA?&Ei%i7$YvktC`IAq9kS`W|8f(Dw zxoD0~9+3mmkG59Zw6&naN#xpBKJ>$nxf`_D5kt}Z(vU4xbORz+ZgO7JQ2=BuBFD$zvE#pwl1ET zkv?tZl%w%`6SCuMcC%3QaP=l$D93`b{YxtKO-F9vvJlll*WAj*G7Y}WjYuBF)6QcY z%NI3;Xtj=E+&EUHaZQU0yx4BV>OlweJ^w08j-IE&v2V2lR=Mj3CXGLNxp`b4S##W+ zv2w=69-ts2zmA8}Z2yYKW;Tp<>xt+B+Gb?rr138gIb09RP^o@YDQmY+uc#{?BFJs* zp8BOHGzoPLRCJ9=G5uG1f?F=>9@KYkt6#vhAO7O2C(_oPm{jhMyctIwsv5dfw|1zf z>~It&6oe<{`J~P!E$4)5iFAyue9HrYtEoZPUn2Pw%p|DB=E0W~rizYbbuG9a%S8^^ zRJt45l06pr?-&3UNJx@@Sg^SlD%#dB4#i1BILo7LB6zuAX+AcOhJ5(C9_3?WcWvTp zC!~FIdV~Qcf+=5s;C4MSx#DqH$-XTzaMU?|g;t!0v=kMx5EMP8^%;OzeF;ZZrOx>n)(@S)VQoj5aPdE>^VEW=T_a zt`S}6XydsHDQ;@yFG^twunP|%?4B}* zSj<8;KIJunR6o28_G!0&#L8BbvFn5x2W6-ThMtT%SzN+6BIkIW*zWP6Babe0uo?)} z+#THyZMpPp>%YlBEa!V?Q1pVMel-`g%hy|k zAuPtsZ9`WLRc*Q6wb7;z4#1=S(5Xt-f#QAcTtfTW9AeYu8vGj-CZR;22v~fBQr<7n z#%!Ci9nHr+ws?HWh35KURMbv?fiB<1beh8*Z*t(@=1{#KE!4{6q+HF{C)AE&8c+Gh zx|(#V8yOoM{aHl;I3-Gs#y~E-sWfL+wTtXbOndRtu9=@AsVjm~Cblp88QzqgN%}y_ z)@-a~OQ`y#RX?_xMJq~;r#k+e8xI?;C+`<_@0938xyBSX>nWE*IT4RBikgFH&NupT zxa={DMUGW2l5EXY7-LzS8C&PqwOhKr6qu6wzh>iJSE+IFxNaZ==;lB>B2ZD6yZEJLWSy<#*`#PM&hCQ4C(R+23s>-eT(dwSe*LDk88)*^ckdvsCNnjsj>A%5kytuKgDUY|Y~v zZ@fj{zVqLG>QkSB<&*7k&w%UBlkG9jz)%15Prv)(hhF%^hhF^9cRkyax-9@t_YZZq z6m;|NJU>r%y9{A&d)!QB`gftZMPMmm0kXQ)h{9P&kX`G~7Ok_Kww*vx<1xLI(eHS;YwwmHj{UXSjJ9a1 zW-eOoIuLvKRRjks!P$v34fe+D$?`~4xyK&3g0z(XoSodO|FBUu$Esn|1wc0v8k@{9YBvrSm?TUh4Z)Wy{ep7j`m9l1woHw~0!R5pjQ#Zqo~2wloA%d6L;aEh?adb& z(eVPW3uIyEBwx$=U|P9?J+WvyXY+93Vy~L7i@r_Q*8Dl+P9<5TU3szB7gyaIgL0?< zLzDZ5Hytz=@JVrbRnZGqqKKQ(+o%plWcf);7S^d?R_|M;kNp^b>v4_L^J8NQ95SFD z1-cx$zm7)=tO>Gr+H$adaKO+O-wyf_SKnQC!fd%hc08&kwuo9S^oZv@sz!%KBa} zPJTCbtoQ?w4U73x+ftpG&LxR)1L-~MfbnUAYj4D|J@vTAL#mof?`pGZ!zHI&Wa%KU z;T%fheAr970KWQ5fb+mXGweP0cB8!kS?=_8R{trr*XiL5%%;ooKufWDFn`sJk3lvI z<}W#anmH&F@g3hjRNYOnhRX&Gn;Y6=vyer(=9ymDYRJT7KZYdSQmF$~JCK}z`b z;mV>{!;KbC1S_J#k3=I2f@7C$5ml&3f5~*xOFOSOjt;{8fjsKMoy;~T^8zR2z8dMQ zk=mY7KYg8~t0H9%+qIh4Vl;9-jg_=I5rI!D16q+2@{R4Lt8zFmt!0JF}VjOlIHwblKU`c-pVk#CG7E@b|rUPt{x? zPpf)idu|bXbytl}9If6%@u6~?1wPBZ3yKM~Z2-2z*45`+Xi7;ZexKe2PKy?CWUjA6 z_r}GID8avUc9Dc0eD@(wTI4=Zj(Dct&9LUmt%o4o(8XQecDmQ^K9(u->F86vT^x<4 zIE9i+7h2^HjMKu$F5a#dT;+H5aVast>jkS5x7A`4zm{cTE{yH)CwKK(od6=F%((kv z_hXfA3ruOgIw|GAoO@C+0({Wyg|TO^zNc>9_*K8nwtQUvt@9H0$V+u}74lLZehAao zhF5Uxjp54p{el;TF4SpXKfcWg)fhFEkw0Q^ydHDrZ_;_w|EWjE92+NA=1c;nNKmUB zym(1eJ`xNBevRwk&z8nah9DjJxfKISLb&JKJn7ojHdiM;B^vWW-UaGCj$s@P zn#DKa$SLZ_?_KWGHr_be!oT*(9yR<5Yf#y8w3<8eTi4lwUSDf*F7eav88cJPDxW3j z;CSI9kG82~yCO)DMAVp6s=>jc){KV7fy ztexmksu5hxwtrc{<;=Yu`J2a`7hQ8L%-LFB^s8C~m*5NqUd{@ke`5997Nt zimLViTp|S4>LBJ(bN$;q2ozIzq=kZE8Ve2`lD4dO%4oVd9$E(SZk%gJJa?9MaB(5l zg9OBrmOhJ-x&m8Hj>Wh5-Um_YfR9bNpnBMANLjzqx8JGP==irh8+mSfIBIu&qN50H zG7bj>{FRBim#i#Sx?CYU-YC(gV;ur)>{gd0kN&XHDf^m-jm~d0931AkAC;PEIt$Aq&DmZ& z1aq$0<>{rMB@O6rZQA;wapNVng891~OIqHVcci6Z)Fed6=H9C9t`YS?Fl58V4BEvh z1C`Ki43ranZ0%T1PMJO(^^vFz+v2c+SDN|}k{0tMuA+B&iXpG^sLJV7$GTiQdx(Qh ze2^H?)Yd08q0=0(?@+$}PE_j#{WC_7j=$N8<=7P?pZcSsbimvtL4+vYhW)I)!wG#%2fS{oPlqP^VAl7`+_iT!IQrP zdEx%Fku&-v9Gg#js^=OnTYy}{b`A()x}t;y0>$e*(4aW)ejiyLc%ll#o{ zPF82Ow{Wy*IQd&$SWH?7P(KT$x?PaCxFGZVVeuIa)eU@0mc>&YPMWPel(coR+D9~7 zh*5Td63F9Psdn1%{AHVK++i2ET5Q@y;nY1DNKQ}s;ib;_AX6V5Pc!O>JfEJ%)SX;2 zFVJACpth&@qlM~vIuUOdA1?Iz(U2D8Zbs2{1NG~;3vd6x0Ci zgdxx*#i+h)(uxBD-!=$OuIdxlS@f$@9!IPR&~%ui;izq{L>$L*4@^&&Y192XoUHoD zpm{~aFPnbN>$oOa>Sz~#aa|`~PjC(*_O%w8M#dp%Cq^+@>2|tGGkNn4zvn z=DqW*Lxifk*`|&qe-d0}#pDsp+xS3jDxmmXXQBW0*WLITjZ;M#IiOI&GGtLj_R+!Z5O}PrK*S7iD|#)A7AB( zT@N1^1RD*P8`x{eI8m>0dN{{e8O>sbFX(R@WR(N% z7#LUQ;GD{!>>dEb8^rM}4)7WyIx+@fwO8svAH6vEuo%u}>RLmZAL^aB#In5s%0WDn zY_DE?4;)pb`Jl;;NEvLg+yd+Jd!36|TesdtTf&7uHhS6I(71WcP8W?WZOhHLlwK}? z6WNyEqKiuFsqL--DvOj6XzDw-`ZXW;J@%L&R>xdngWeUV7+Yh8l}?T3-c|ZgDr zI9qsJ_6KQWLrhyx90qms$2@c#_&~fE@$0AE_1?Ul*)Vk}8_xyai6s^D%B+&bccx+E zNIr5grl_Ge>n=ZzzO0 z;BZzvo&-Klus#dXY_8L^p4(Vd)w#G#PR-SiK-MCze>BO3Iz-B}6Js}a8hbIJT5*d! z%ZMn~4!xwRj%xBgLYs;Tg6ZC)f8fd-QsgDm_MRnHB4?BboGe9SLUn3*1V9{5ATFqF zLcl>Ci?$s%_*4J1uGdG5M62(!jvO6*2 z#~)?OkAQLs7(-f66~p7TS@b2eFNrjdypZF^Y&a4vJ&Nw3P!For-o-V7vt@3zchP`S7q0s+1y#~C|Ne~?!%%&B zyFjSJ-?%-_faAI0sE#9fXzT8&XYD`rdBu@}A-bt#xiMk|9TYDg<~iBfTbq^wOGvOS z=LX1cT}qR@)u*syd-YS}XdD}xX{gLgm?0~$zGD^0S~Zq%)V=b4FFe>Z_8sg1JN7i| z#)m=>lD%?)&NTI8nHp{P8=GUZby?%;)-5 zuHz#;N8_w6zr!ysn~2dvIrZ2kFetRe&2l`_Jm~Pd_s?Z!JGvftjkAeRbvd+rr8}Le z&W_m2I{-$1VDjD6f`cneX%Y{invEN^_dBi{BXB(ltiD77hN0x4{!>wzR&YJGyrLo8xo>zaO)aU}@4DrRV| zmowGVqfOUFlbX_#@WX6+RgV7xM9fo!&0!Gib95ZT-b~#?q^{|5uOGdPhsDO`o!jwv zJ_&{K0VHJ7z~`7?*5k0&KhJ%P8#V z3g-!Chn)m4EN{7^EjauIXeyLpA;%g85knNTkX@%0^_C?-ib)M#hqTc4g_!=j;Hu{d z!*$%K{fWY)`P|}aefd{KaZHVVN~b^iUc zT2|QS(gab@d=|@#W>{ng5sm$10(M%ML7Zl`ixfCfC<}A6{0AL4c!6b;rxBpOsA~XO zH6^ps7aCOrj#5gB9*Gi08BQ02+TA5S1uFLIJ0~Z$-BDFRkY#;%>cr)B#k$1k^@>&b zk(&;TpV!hNZ0$9rt0d|1H@p+rT7<-2*#D2cxB1n!JMXie_se!8rwJvhnqehWgir%g z2qsi1QX^&3Oe!i%RER-J7(`49Y9}otBoHH%NtH}sz#x-A5-|ZO445HBRU`y)h#lLp zV>^yrzja)j_kCWD-}^IYp%*L8pFy_FkdRiKqH(1W7Z z;lR!@LBAVV<%0Ng=LjA1-9SxV74RY8&TYBT&egU$4m_(^^9`jOk*Q1FjyG)%zo@&@ zH6!bDvrwq>sUO|Pn3WAZ)Yf?(qUd6f8U?N{8foR>U}{o)8^~dsAR6*cz;Sm>xFI~U zc+7lx=^L(xB9f2cSMSy#Yez(5Zmn={z68g3TKb*HSVCla)XMaZlr|&m+M&C;$k$oh zIBuzpD;E*v;n?0+8#91HqTa;w+ycEg~?wckUMqd+SBLdjYSkSo$L;r>%po z{`CEc^N!ZOt@(G_SsvR&)jp#d04^A34)`^No_@T28hdQ|-g^cw*SJ#`YUZ>S&qG z>zX+aOuwz9SF`aOr~8cr=e+X9yT;}S zM2@s$h`@8Q2_@DUw@42~^ZV;)lh8LAeQ0>g79`z4$_-}zno#4Uv4;LQ7i#1_Amyw- z_=h85auuWXOQtFm^Vocr2?B{2YrO~dz>%WwHwHE1QE0J_2MCx5vbUc1Yi+cvvvFs7 z%5&;~jBP`SZ#DO9kP7?yo_h$aZ5nF9(VA;=TIH7-p>FDAldOF0t5A6O2<4`+NI1UB zi+_I&;7ecm%Bz3lCw}6WKJ%H+ynF+oBK?cMYd-vopT0Y2(hxZJMTnml8&Y0g}%~2gs@(Y)C7dgy?bL-av3IUHJ z%Xpx`$$1xJ~gtk0YpZ zE&lY6rVXCXyZcp4ByG-Qs@k)0!>*J;ZY=dZ@sQBOLmMNj>j_4(GY8-=V%+kveHtqC zQ9|+VIUwG>0XE~lc5B_WgV4Srh!xn5h4y~WMP2!8wxEJr&5jiNlnT_3-O(ZY@RRuexIH(syni*7;qC zO%8IVPQa~1#-{CDDQdMbGdEF{kje~e^K|> z&NbH5glcd!e{5o+6VJwWfeRov&s1}dqGn+A>rdusP1rnG6RW*1=hEkKikZPIE9k+% zS`(fyIsTZQN4^v}#{OyrhOG?5my0x=(?)e4|Jb;5M&iQ5>uNR#^xLLZz0Z9I%E&pf zn|5#f+ujRo>mY_lZFkH%2iFbdNbJ`%f;k|IzhL-DmuI!QYr)~xeAE6-9AT90kh8Vl zxgj=l4V!ANkwsNs%xkAJo3-Q6+^zytFRU|{<(=VLXdM_46V4@i=6f)FD({di8BgS+C^ zz}HCDwpk%}Yvdg1s4w3BoXN5m|G-O^aM~F%p89Zpw^o7a5So(npl2EQ4NBx-O0$_M zfI-;&bwX2eQ?Frbi@^de=5~s zuW0wOJ&y`B2cAbNFIoKBul?FLzW$A`|MC4JfG$X#*)9}3{Au~(6NPlHN?w3jOu81K z^_tbDOh5Y^TMX;4bb#ZVn7xzv+OJ)E0nYH)vT)FE(J?G8ptUx;XgcX!ueundLD0>Z zk$C!X!2ke207*naR3j-kHnhc&3mPw4{$qo<#;vS|r2)^~sM7WVHuv;W*F+?#b^`#3 zE>w5%Je<`%)k{Zfp|3B?-@qt@7imni31|8BPO<^jS|9z`^+Nxk40qW5u|v6GF261& z*mrZoG2ViB!4y>@cNGs$2Yg4Vk?;bJN%WScm2VM`I|_T5R9xx7`h}N1qrO>&iWP!QbD?K!f9MXit3f z5-f+B@Ne=o5+RvUH=BXMpo=?kB*(eBznDy)F>olFec0Y}WL!kS=3sb}YU;aTT_Q=2 z+?tg|cy>JZs+>1?Gs4zg*+_*^6xh_QKBNsjsGC4_|hzBaTyxl9<}grv`ZTx9V&y zGNxfaC>JM~0*vB9mu(pj|K{WPX>#Y5b=+EDe4gsX#HO24+RH}{>h+$#5e%uZn#D$s zP^)FadTOTxz^KFfiw4egmgjS#xHec_3@N1!FSi?oo8(gHaJYvR$}pot2Jk zVx4Va%tt)r@j-cVxbsvF+RCOgvx~cty&kddM?sDAb9m7yj&BTzFn;D6KC^tiI6xVv{thL^y?uDDXQkw5gl{yWT7f0IRaTE{L~ghu#&{Un5vSUS=zwLgqQ@QEY7r?cJu;8_ zdqqSDACdj)D4r(HHT9-}kr919=6V^JF>A0ka_12>c6uYl-ik`fnspE)EYB@{2(EEQ z`{$Av5Ao#HLw_gryq8y1&pD|r!xf5M_XQ+iI<-;vrnGj31vI>k=xmW9sgoz$%#UXj>5-*;+nnb=-AEmtG#lC|5S(aTz$ z-drqLP+Y?vn->f|{qhs1uKlb>B5fJnawLVm4#-h9E?F&r9JR!i4KMZH06g&3x(vG4 zc2u_HAv<{G_COYEoAzCtXgsZJ`1$UJvdp&kucXVZ8Vzt{VapPoaTfrSsRpu&b$f3z zx3K9NuWdH%1t1Sm*H}o)-We z1A4kxJsjeu&2p2TyyA(IZq7NcM@I7nSYsidCdF)utg#;FDrtAN;+?jF6XVX85~IFe zJgr?pYmC!ka!XON7<40c<_xm_T^TO`Hr^h06Im{ikfVll?8{vm*rsBU4o4#HX>@SQmqWd(&GboQnjjW(Btmjyxkl+o2K6jfS<1 zNAYFPgZ6Hiil||aT=Wn?)!o3ZBv`c>)5ZxjU4j(xqwAY08wJ4PzDRGl7Ur-FZbQ8Q z@hsAK# zv2(Tm+Onv?=rh2ZznHgLt=xh;7T8^RAru)uf4QKgO>4#g$vHmmk8OS1XYjPKxlt2L zKdp?$#@zEVe79+WJ39`1kkK_aKgD=j|5G4+LWQGkbw?HK^t(>QWQpwjv-b5xz(3>2 zhZ-Py&sPiJJ%c8VSjLCuf^@waNJd?s4y(gO!)?l~Z;}V$$u<|vWBuiuM?PB<^%UR< zQ)E1bS(wh+S`CDH3x8Z1dUD$kyn&+)+tyCk&AB{6SvUVShb}6+$c*n{c|ADF(I-DX zi$B{pSHUJ2Mt$my;XPM0e>$JdjbJJdU~d=^V{tb&>98oacFhS#n6*j3BlBpDt@nZR z5Pom|k=wboIQIkUG%bnQIxl=6ddz$06q~Yp{!W5W8HZBPhDSQn;QefOPLfAqyKzWUU^^Qr&%_kaKQ zzcI&`{d1^*GvQ@>MiuyLKmFJKr!VrqI^X8s0rq7|PjF7`77`bv7LqMAtpO|&>h9BC z3+GuMD$V&trxrtZ2X9|hcHsdZ>@7U5_{i*QitTGYvgxIPeRO;2p`8m7k^4G<3}_xA zTLkHd1AewS`>yx$5G+muExundR)(9%1)O&)qLOU{@YDDf)eKouaV*rqAG?%&Ew#Lh z34^5>|8BSP<)+5}6mTyl@;Zj{aWQSP%@x6SvTe$_H)}B3n<_YZ#n@GYu(YwUmR%H7 z>lYZuR{z^~0jJ(5tA(Mkyf4!8#JRZWx~PJ0?gxe=M8S}-aRj?xr3NWqHP+djRwt9g z2fy{bfzo-woB#t)YHG_D#Q>*4Sc`tK zlvgkL56=&U)rsPvi+FJ;!{sFMk#z`9(#AXw6^yHtHmBu82he^u`q=7GEOa}~SIc5s zdw;sK*j!NV9m?1l6MyWCF|Ij0au@`h&Scl$IyFBvrC+|zL35AKgw{lMCZV0$$2_J0 z-fWM3R`r%E))VD*0)(O1GRVu#*rAt`0!RSj!(p{%nR*;ijEL(C&VzHHhEInIhFf(A zWEQW_Jh4U@ibYK4cIP$+UvL?|7rXfNw2UWPv0mZEYxC&%WnRnitZ=94W)AAS<4q))?I%jp1M43`qI`x^j|dM&`^ z_dwCy8aW5Xt1&-Zoi8y2bN}UL_{$01l7$5Y*#yvl2mMi9BZIvp~4;Fd*= zY7wDKoTmaQ@vGyHZ<-~>u^%gKZC1D5ut0Ezc9Ap)h5e~d^GU6=d>F)fXnXQ>PBm`! zwL>SRu3;RrN4^gsb`5S^#A-g#fXH4HArpX~=B2r6S1iT17K$>;wi*x-317`|e84L? zaFnrcF42*j@!}eDtUZyiNoRQD=})ev>(0YtxkwcoqtS=yrao!$nL%7fS~viKx=hQ3num$6H6K2|0CVC* zk)8AMDd912Kfd>PYW9Y2Z)S=uW9vpPwH-^7j|9-x{N}y^^c~3ZEPd_p*}cZb?`Rvf zr`(OfyvFuKJsi!yq8PPxz<$_q*`H*Vl~&%1Tx&nucV3I5(sZV|Cv-5h{KbH!8|7>_JS^WU#!HN9VpL_3P zU;U|1ed@tZ*P< zcvuW$#HZbb?D#lmG+8k~-&cR{yy}I~feD&+)i{Jbxb6JJn*r%YPfVHZq{ZUYnK|Oz z?dAxZ`*llyov5nrq(Z;E{kHbz;xd;?`4FTRoc7 z$NKux5VW7FO7jhTc<)nZNtA@1JmnCY5{wRd=Yw+NJhtK?NRgF0Fx?pgzc_26BiAO5&RtEk%(o+5#nyu&^z8{TlAw=Al3Noi7zvaYz$071u_Lx~b#D^*Hihf%BAm#x*98pA zvNgX(eX*UpVmiOf{=>eFCeG6~f0Gz5ThU4~U+KP)+7lDTWNYdS?wHHHX@Hx);g2DZDC#zbw3B6Bja?_C_qhoioZ}sUDZs=XBaH?sCf;$sbLMiIag0(@sJt=6hn7fA#AExsbV1pL!-pSrriIyL`8b`+%mTu#s zgGSr?eHanP+!mWL^yiK*7*7CydNDqJtFAZ^&Znjv6!9D`M?p1Bv7=O0EA6cU2F#V6 zYdUC$tY+F_T-#Y>gD7av)srak&&{1MMT)?k6&qjhcOD!t`lAXt&FL7~T&kbD46L_4 zCV`&3`i2X#xM-9ea-G?B^NfKuxcNe$PXwJaVzGO?QDEK#Q0%@V`qkB{DM2TOfwFzf zcqEKYPVC}=F|UE6kLVyCJo%qE9Q*dbC@*K@oIaxyZvMo)v;7eRJ54(ua++iIG{<+G z)>c;P#IU9kv1WkWjX|~BH*aW|z5CXA+xT^}>-7l7{iOu#P1Z?Dqsnv#O@0TX8tE^D z1A=G&i=CEjtL6Sg?7W#4Fu zqjSW13%nJ+AVUvW)=wKTieqfB?gf zPR=>&hKhKJr`klJqTlspltZ91ckr<^v^ zj-r@oIoGstvkJ?jtcQK_5nH=#Ph#b(V;qS9bW=cExn5n96lD^RuEuD^F&~}SSqFcV zKb=qXwQT`Vr;GVoi?yqVe%M9j#CuE=b4YJJi3ePBM48`C6W{TmPmAMZAN$06um124 zzwqj3e&(nDJH#*BGphj8;$?eo75KM*`lo;W^I!PwZ(;j7tbyNnquL_cAEEgP@@xZ0iuLXt0&39@Ex{G%-&W#Lh zW3(u>XyKzCbC2Fs-es5W+BJ3&?WPl^aW*(lmr?V(H>CROB2pwg^3wvW!p%6eXJdnl z!Z|x)s*R*f+rmN`kvw(wPLeY_S+mI8s~*+Jg>2w1JZC~?ab?@#d`RaayCObS-O7G z>r(-1K)V~aSZ76_V0_Qr9K2xa4AQaKoLQc`$x4siuDgz<0f!~+gIXL$>!{J-;5_L9 zj&!#4=1|Vcnj^?cA6%jJ^LXp8-DdRiDN!ZXu?TxJ$u|(NXf8R$cEi02dZT{-$fRV7 z4Ms7zL4Qq6EQd1&2&O1A(*Aaecs_UgXu*5H$1kp}i7z z#*tI}b4AF>u=wPFve9bNIEHAL2Yxj)BJ@4w0ud)UF~I5uwmCdb`&36{;&Rv@7n20I z_;u4sSLNo7k#Byq;bOJbbt5H9X}HtJInaaH;TMUW7RE)(!L!P_*v+r;TuZIr;D%rV zEO7f9Zq`8LOldm?Lo9AN2hWV$WJ)Br2D}vGXgGn&uc6PV8dk=l7z=!3+*)X+@0^bw zyMw>q<3B7(-uUuuO=wM|ovqlzcg)RM%WB&qSe6PUpMjFzH0J1>Y3Ep=?*%v@-WZEx z`n^~`X1-{Wm2UpJK=JaOxjs>l+q>HOFa+2%Y==$%SavRN9o+mh=1o|u6Qp!o=UW8e zXT4P$%iI(psEoW@??Wyt)5Kx?#qIOMiB zD$GaA;;2y1Ce7Q`-1~~oF_r!PkeFkxVfOGJ-U$)14)hJobBqz#ZHHt5wCZ75#o0G3 zXfuym29O`3xwz|hqZzPp!vv$cTXe&qbD@3sp1RY|R}N?1dfb|9t~nD<(9n0Vu0D}9 z4&h>B%>9&`jB;hH(6X_|FC?+G_e4P6n=32e%rplagvG*MKkMaj+^9XBIGA31UE39E zrHol+H?N=(TGzDO8CUz_%d}D1x{wNQ?alY#%HpJ3?fUw>I)8+samO~s6hc}Uhi|K{ zRy3@F64z zj6Yt%YZb=oCjR_9W6w9DlvSJMEvD%G>Z0yE+BYv3)Opp~hf4VFCnJMi-+XAHY^@Vf zJ9Wrzov$6@o2p=OyZ1fnqG!>#hBkMV84pXZL9R#N`_6Y?edo{q>^ndClRx=~YF@VI zRRO=f_p&{+3h;kA|2e;S|F{0)U;6RCv?YlX+r`ccHYc?=u5K+9UPaG^rat->Mi&ok zI2`LWwivURu;92zrQKckvAtefhU|%dzN|S=k&&tE2EzFAQmnnPYtukj7;V+D-GwqY z`zW+g?}b2*7BUq5&LNBJ7I|W};0o@OAMfm11icl%v0PAX&!&RdqpMRlUc{*Y$&}7V z3%B?bL)43d!lLn`yZAj69h}(HzhiZ+!pues?dL7^@md|^5uns z+Rsp(m$00=@##&J)B&J5n08NKn=^?GV&~W}idK_M- zxEIN%khI(uZ>VBS>VC0oV53~F&Taa#L%`3yX_3zn8JA)ivr6AL3{)Sp`69q>aa3SE zNRct|M#ENFii$Zl=6LS5d|{!<=BDQI(?={g(q1JGj)#d=(Z;S+6w-IM;ytB}>fY35 zs@aS?7{@mcatD0zxrrfwGogT#aQgLSwYhWT#5A|+>qHx={jX*{`SIU2X)^vpYp)Ii zN5$>j;ucSNca`;4M{SzJ)c201OPFcT0xpi9;Es0=8lX4NTOSoBh~f3i^z>Bstk7%A zFk55O*$WL~bjl1xC;dFX9|<47UPOWy?jc*~;rAF`<(Q|x@+%`VDG-1~%E#h%gL)SAWj`4%s8++1sI zd!Btiw}XiOj4wq~cF%`;2%^;~hD5>)h<1HCY&Rc6R4Dr9Wo6yyVXzXwq2fpcO?*(6 z=jOE|VkI4Kee4&_AoOxP8oTNZ5J%@Ae%ePf&Y(DHw87hdg?6dk%ah1$;^8QBuep!& zLvYBJQ88sfLxJ!d-W(UP(>y)lPye6|gLf2WN%s!8@Dg_H%9qf_F8^`v znlGet$|!@EV$GA8A&>4>WYPB5;^)`Qp#~Z*qr9HxLfG2&vbA~`r1wn}_t2dfhcFpS znDYrtY>JV|nUKzN>|HKTAdZ5Lxzla)2$^jI>Z%_{{jwy?V$QC8#LRbCT8iL0+3*l& z9~LRb3ZC(`jj4X2$47j-Dy*(;yI5iG<$LoydMPzGjyRY++{{7UgRk+hyXTAO>gQm~ zUw5)jKC6gTwXFE;<(eE)$%kjUS<%PNGXXO9Z~ctGFw`5->}}hXZ-B1LzENU8gzvrg z&Z}?!({HgpefST4^EZFP`m|oSP_WQ=^K>VXI&V_8=q*w}*zh4W?%vdS zLBXO`2APXHeNkJMuxpXx*v%Z^UgV9|&4AYW>caEJ?ouZ)bY5Q7Y1T!^h6}2nbe7CG z6q&-HBIa0ZRXnK02{e9U%3J(Fjx$OWE}R<7G=%)qh976u#=_KvTcW)=1FwB!K`3Ot z#cP~+Wr*px>{kcY)~b)EXaLCuCseyB0n-ZHn+&fSXMD6Z7qfQJHxo&9(Xk1hJhRu$ z`pFfMfvO{&TQ_yiuE&KbGJB&ad$c-(V5BzNPRM>|kr?Kz+x`s`W1eAFZ+JyXLZ{&; zLCp^O!V&|JkNV|tv$_$@leRhy9dv8vNueLy`nC)5y(ke!OO^NLVQAJs_b%$G=@mwEAWhVgTZ6qc6_7FDAk|>d;!}09Zh$zicuNr<)MpR`Z;SCs?C;qM)L0 zbj4OyJ2$almt3u4?c}Y6vcAvIxrv=&PoB3P`Y6x7L05qDnQPYOBR{gfd1MT7uPtVK zZ-RDPb8p_9Y{T_)THWlAd(jY>v%z|)l0p1X_Xgnc7r#$&oZre;H0(z;pOdi@_l{># z4#sSkb@S2r(%Q$I*eiN|`(yf@tElB+g$06q2lg_qhk;bQ*^2|1p8M+TJ2pE3=jl1z zxrpAm;Q4G>HQqUD6UR6^j)#Npf^kH|ZvJ<;6on&h;{*_mW<50>DrNY+M;_M#}IgVo||eu;bC=yjC(@{wzdG@qMO=IC2HV=?nt zF3ysZVJrB$CFqAhP8ggZHh6Yg0KYjS+pmRDH*(j+r(@yp9`?XRC@zLVea;(FW=O*5Jx9Z%-P&tXZ1Nb~8T;zvj*)st+wm!1?Bykrd;OZLC+0&qM3w#^&eCbm z(sl6iN2#wy9M$+{QtORxj7mBLv z^@7*#+w@{{5P}Q9P~^g)iTMT(`D5-IHm+B&3Hz=M@A99-KKHrLzWVu}|M~x3*q813 zRlxJ^WqXDd_@!U^rT^`VU-&{Vj$Zr1inF?}$$QGXAh1Tb6u9`h2()6*a=qG`<>J7_ z!L}N-Dm`+8b>nbiBJW*IS4&xN2tzvSwg|d< zLE{Kfuz4e}OF0<&w_0+bDK`;Y)Yho4-Tj0riYJCw-B7g+hdzQXR$_G{R5iMA%h4R~ z!@CaKE~9RvVp~g*_v~f~4zoRCYvaNkA!W2lb}uH3jV%AgtnC{aiizhK*b_#G&^e|z zmgDCOF$)8>EZy5z)GQ##+OrvQ=F#awh)`d-<_6Z}MVkNpD%m0w<6?O<6>rc=iAfcg zz~ExuDzn=xaJ-g>(#08X+VChhak|?zcQy**CpKMN3Dixvk#Iq?xk)VRb{xvXIqr*% zDhQqonqQBx0SupBs2Lk)YvysubF7b}brv6OHptz4BP$~Z%X7g4Pv=FrcErt3+du|i zNtj{BJ}A`17YN2X-!f><77d4Lq4*6lj={Loa6`9fV^F=$yv`P7=kKxgO`2}fR7Qq_ zvC^|Y=eaj*BKDxj-K;!pIH!m^E~}aT%SkW)(3?jjVfRuvi((e0YQ?`1b1vX<=TeN$ z?=#9l>$`Q1(XFP+;k>tDkO8A*)=PoT<8(B`Sr_S863<44oDvq4h*eIso%B9P#{9ux-DowrTwgTJr~DRD=1LuroY&?f1# zCABLFPhtqmFfXSZ3dI4u#>!vUA z>V<_SU7wM5Z_)hG0^4{HAIG;ipQYTBoqT+V=C>)d-}>s@n!hpZxyXm7UxP@GpN+-N zv+@!GeywpBm_qS%_UQ&_(Qr7KJZ$!@tF**5S$=kCY+5InqYaV4c{Zkkll;a~CoxWN zLlzh@Hq{LQmtFW(tdr9*vSuX{^L!sIxd-NTLwjTK!&5isuC=a$bo|x2S)pJP)@toO z$l5w)2&cA}P1o5QnV8nx+{|h9$I>DYqxB_hK)~vOec$qsqhf3y|M@{)k`arC*Y|y! zM^8Di$;U}-c@XyIyta0FR{9V$E)3=(hC+N-ZTlCl#=&q53Q-Z4v$n=={V$9X-{cKF ztlk(9+jWWu$*&%obkc=@3~g!?STN%iv;Ct(<7}5y-QfzG{~HxE%YKZZnw@Og;P3$# zr4K&%@H4;iE5Gv9319ZltpfY-<+)|`lFzH({_WrXfK&M={^cM0v48CyzX!$E^46K| zqLAQ?8vz%oyXdLQx{He~b$zv~wpcDkfQjRdRjNg8tCd2o!CMHuQWdY*SnE2AK}0Xa zyZj(`!NbTHTpYc(^8z~h;?kpcdu+inOpjmzllC1=q#XSR9O^ zT@m0R!`x+4(P&Xj9v9YdIrQELY)JaXi zC?VCiRVqg4Mp$5ui#7joL`KZo_}n?-oSNR)z&aH8JN9VoM9i?=FwWrQDW>xI?u`ud z#4j9tg8(mZbs>hm7&FbMTs%YE9?_dV)&@$O0~%nfxu1H7ee3P1EpvQic&H*f zU?S8ufD_Nu+@!9};fJQTo(ikYF0(Z@*%;xRex6wHz0ZcbEK_?$u7nuySNs}x7!tvj(A8_fY9BV8q_z0uUs z_uMzo*($fHEU=v6@RDl?cd*RSZC_8eE^5Z(xFInnx29WM=-NN^EKkn|#YUtdCl3O% zxfW-(F19LEA-7o*Yk{1-8Sp^M-X5->MzV5LA0U zn2(#aY}2jcaMY$9b>V)hTdHehGH%%R_kHMItT~EFg&^us#5sF0<&VQ1_wFC3#55-F zeeC0}{`gB@diCdjpiX7g&~!x?3n(h`fMOKiluv&ej(ble$9a+Jc%M8)fP_X2!u~k9!Mr!&B!X zbxmC8T1aueG48OnI3{F@p2aE@F3kOLIl}-nzJ_#(<6TyT!~+>Oj?QqN>Xk-ZU)sX1 zh0YaRo;*>Gk2l5Iu)8-d_ohQ?;fq@h#L`cUT-fc1Y5q?!eK%Mo@-8C25634?7DIo5 z;=YS)Imp3<8k5Jy1x)*Xwc*I~mVn?TGR6135U?iXR{hEYffowCBGN^k*pA!XYJ(RfqF@1!QwM1YE|$93R3jxsQjNZdmavY%KRbPhZq>;XEOItb@h3H-1B%mLMc$%{v_{iTm3>SAqmT#}4SvLP4n@Y?J->iG5scFO(L~v@ z#?K*!w{Q3=-F3@6>asgqdTi~g#OLuTvH7Sk82o6;c^G2BMWKRDh?DuTZ?5`^2=vZ> zqu5o_1X>pE%U+#dfjl;gYX#lLC0k7l&6q)Vn}bYko3{rbHV^))9Upz?w2Etm+%dMW zZhjldpi#L2=o`a;nsEqPk@?dRNtsdJV!{_Km9;{Kt&YmO_SvQ1I!k9c>6EL)L+@k=JwI3aY zGCHanT0bO5V}D$Ck8;jCg#>Jl>C?RKDqmdDo!plD&PiWLug0h_=+ABnzYyTj&_;qL zLs!+t79>vE?q{+vAb)DKLTTl>s+$>j?ig1I^Fg?9=<`#(k#x;Q-ZzL?Tl;5T80Hu8 z0GaQnDu>h725Y{D6pYl_4*2*lCc>T1=ugtGKK$^5SMTxl|IdH%k6!(azwsabFREU) z=UD-#(aZKsEAVqa_jCW}``>>5PhNB3d-q)ybyorYx^dS>Z*UoX*C5voC&F8PJ&u*n zPk-#YDD7)$80myiA7Q4nEx?hbXn9HAf{g4T(bQmEz%=!yTKwGzureKrD)n)Z{OF?( zj5a@lzxCF;j|==3>yrnGv~VgEfP=pW-h$hrM<3l;5=Gn%M@$O9&J<;44fGci{4iaZ zZ=!F+$X*|!TALj0i<0!MR@@UMaD$R&rt6O}iMw3|F1LVv6XGNzk}2W}MH~suu^RtwW!1 zqRig>`RVJ9&2#%a&nDu&3Rx7L!rRvhk?C(8csA!9Lu>0(Qb|$Ttoq`?NjrU#qb}sQ zD@!g%$@Q#LYp_=I=BA%!I9dZt*S0G;`#imgEDk0(`8V|8duZ?y$>pJ;k z92y?VJI8~mgMK`XcPpXT-nP!N&L7Xe`|0x|w_fZlhBs}u-o5yfdr0g{LLL{_oFz4j zINZFITN{FOvro$xySG+vv1U5&H1pdeHmn}qY`m4+c~a z|BN_v<4>_@w&`0h8zLzn@5MQpYuxjAVIx1ssror=n6%|@0*mp{TOS;JYlVxGG-A#V z`grcesQou;CjZKl3CrvAV=m)2zc3nFfu*F^W;K~8sh92Mjx=H4f)g1 zM{cey8(x*$wnpLS+L?G6ZXTPPcIM{VA^!2z*dGj**K3w($J(b(j57ygdv4w9lGjdM z2OEGEZQ<+01Tl{F@!2&4;vc;OD_jJH3cL9gpZeAn5^-Z96nLWwQr8FZbmj>!5ASd- z=+}1u|JcVq@#_8ezsKgSB}t-j}&*-K9T8o)$~W91Ia?NNz^i)vITbQ ziqLHyTI{_MOL*)JO1~FDwgT6Bj=Q15qlIfy*-BKTz33W-#NOoHqGwKruozjgJ0jw^ zfz_(IUJEx!=CMx`w8!QwmR<;eip&siE?ex&!3DY$;1peh@)HN_!HWwru~t_a!s4LT z-XupxN8M_aH+p&H<&N!oop?$jYvXV!F>5vR@X8OgTz0qw@J_iL zecT@|#nj5VHtbug<>=cGUgEsR^TMfAYv-yn)X;6WVGg3jfpO4czy&pq7qzORt#`^L+8*>QN1WtlFnM^SkA zZErYfelWq->DJd?@HW^A7yd$McYRZmZ>CSX`n%Z)q58|u(R}=yCxe7LaOn$i&!1}E z>Qf_Z?|CIg^$E`1 z(zwa>wWW5Haw-}wt@_=2pYJ!8pJl1 zKC@ffymYWWJCSodr>>4Xv-hz1brLCl=Tq|8%E{}%W6tQmpGjC_-dNgG z%bEfzvg5O2W)cm@%C*PodgFMsldD+ft^|QC^JvomKPsB9a(NtOGA*-iPKn8Sa3_-* z$}7C%=COImvET@rd*MFmHy@l`V&_zl^_QUa6J|RbZpy~+P)}wox$EQAU`3)$(T34} z+=R)RL!l1~4szXjWIn@dKu)k;(2|pR#$~Y8*QnO9YmC6*jqU2951Uv7%8okjx*`9g zL$)c2FMOIu*3npEvJ-X3C%mWY2S%JrnkNIK7n36`0ApsN;MAt?;GNC<7olK+%m=+z zB5%HUynXq|b$pwv_N7t~6$r4J*RfNiT<5mKiS{|uk_LfcQLrCm5ov=+Q(dRJ^K@X> zaJ_Tg+qs|xcKG@xeQUrZ;Vi%n?ZFtZB_*%&|@%)@1Y}GltdXM;>3dNg$FvjjHLxpo7XvmH25r6il;zxe+bv~6ZFuXzX zalSG9e|_fnUj2K2{jdM~zxR8;_ooAT*+0h$Y_>he%wBS;0{_Y1{k#9;AN;}p!-dBi z3YTgZF@~M1o0sdeOx%@Ys}rk_3lKurk0%@cmcZ7hv-tQ4wstiw^Q*brvuC z2mx-v?hkVr!^LNo_Ke%jh<4)IZ-Fw-S+E{=Mv}`w&#rl|B`9g0zMS*MEu64GIIFD} zcYW>!RSH_p6-@2v_y=la^4hoGA1gJtB$#A%F0}We*a>wxX(JD5%1Z|V8^;$L>?cBA zTfZZd$G#mQd;MX7Ub{(-4+8P;r%M|HJ+MNNuU=gc$i>7?pBP&EslNexC!Ut?c=wL@iR%Qj;#!+aejb$w@TjL> z-$S4Q&uRm7H(%KIg6Hu97DfTAe#YMqN4{FOI$a*dSi3(?6zRzphjz&w1#_`~aYteI z$BCSO+Po1%;@EX_a9_MsCw3(W?_=M5G#mu2qhEvfb*VLW?t0JDd4Rn)!=11W6iq+T z&6tC8e>C7nV?#~6zNRfV9rX8Dz#Ua7Mej|@{A9zqq^^mt!|-?R8z85h7qzkFXkI%v zLS=q%MDB>HZ$z?R-&7FhW7}~xt_(ydfV}H)unCZFY--z(pY@?Xe<60yC!gJztLS7>dyM zbzq~L+}x|55wWFTmdIR) zE5>p4zyPba>^rxSVY>OVc@uUuKYa{5zf2)x?6)ac{6yNs5i{DG^J=i;T3ik2wq7IB zyK_me2UVh<>s(ev@VyI5wIXuOIZ&b<`yXe3}HrBCzvmZ0J+3 z*6PSjENUFArLE@du3gqscSmrMBp|-L$KC%tvrk>=2efD!noTeMlG8VL@!}yOo3-@l zdEx_`trOT!5w3aXeq;h|42(A}!W7*cMCB(=O$^Z$krbObG)J7MZs+{=8u9l1-*?Af zKW)3y!tZz#O3)9`jRQ6s8ZXuBR99ZSATgAn|_meR^oKu z)<0gx+ht1Xm)sa*+uES*Vg1gta7p7i#kgKk$=>d=HNH#kMZQq;HPqF*gl_<)}|R zH3#-C9QIHAY}$e0QS%WjKl|n>M;CD;^~b_7``-Wi-B;iE`q#ho8^7@zzfbpNd&U*m zTzbX{zNGf*)h9ps$^Z6W{VRXvKlKlLv=*>PdNFgM*$XzS#1h~lq0mDM0}U67D|Z2O zffa?tsRfq4@3{7#TDnp2atpTlFx?wVAaOe=32PVC0-RvzG4diJH?L*hi0tC;5xsE) zV&}r8gS@ga~S z{fhjG2A1c%YHb`K4!vlm(deBTEDP&8?w1EnI)~x7#DT%q-sJ3iGrj4P))4u0U}tVl zko-}mkCbu^P7T2mNAv|k?~O_vwVj>zJ(TA!Mli#sd&NteoK6f>tFwW4V`On0C&pqE z8~s!K)}WPTV{}FG;I1*NDIjy#MgnNF2n!(By|FlM;`U7#e9DibaqN_f7_38DkEb|P@07|l z!^dlW8F`a{S7z6lN_#tqLc$eSC3x`Xgw_AEC51 zAFD@uL}(gFHp_kM(wz2!|C)e>i7|B0!c)HYQoA{t&+ukdB*fiM7(!ob=!-#{d|2QM z?paUo3VV?7^E?;jP~>QIpR=J=g5+Z!2kGi})Sq!Ug>Y`>)BHt#NXnr>(6cWm-p-_8ekNrNx_1*wJN?+@ zrw$uaax}&B=aNKhs&sAPW4`d%8x)9+yn0Nf5b2w>SwCn46?;LpM+RoMPvHT8Letgj zbQB(aw>cUR_~zeoDST2fHYV@zZ^OU*M}PF{hd%kqzxBJn`@6r6=4E@v6<`{^Y|p&{ zKl`&k`(J(T4?q9vA{kGeog=@xb2^;?uK z!qqK7Z)|A0H!QTYV5kdJa+r{Zeqyi;CqDIZWhIkz3%aF ztoG{%*ewhS>}7}V>`_#FvBY&}@b%5ddgV@Di@*qMF~AgqHZk$N8*RHvk^(NDYJYuD z*j!xO)=~hhJkobKJ1@?Qm?fr-{Mi-$fM6Yd}S~~FOg)16i}{IaVyGDFl~*(#aaQc^fw`GBR8bHU?c zn=Mb}{(#*dXB3Tg0C4ALgI58_8T)vuz^{#_FP0l8{5gZ^fq;#&@H582K1D4rosEBh ztb|!-b99dD#vVKK$6goL`F*NJhaioz@j@^!0oaJ;7$PqJy;-$C=%tNmc$S^nUQ3Fa zdvxsTl$>pI(|()RPVF87diTYPbHjSQ%|aJ1ZOuJ!j~n~Px~Q#uB?ZpDZLxaiqqK1t zM(Ys@Ya&l^Le!mDQXVnl3gFsk;lOZ=eM6!CE{d}y_H5;$znGg}{yL3$8q20_VIN0x zL$0-A4pVAhlOv$xsX(c@@N~kQdYl41VKjiM!7Y#Bo1ZFUi8+U%YB3HT&BeTQz%D0b zG#$&W<#y(yuQ``V=ea@5K@8GS>N`Uo{Z)~tnvvMoaPT0 zT;sBnL-6dd=^Vi`kvwfgnFhz^a7t~MW5{d?KVws;Bdjtpnm;(1pJP)!@~VCBA)Bk% zJ7!|{ttAGtp4@abA6sHIe-$B%`-q_-j^{0NbHof*Ze}38o%>=&NP*zYoubqam+s>l zY%W-7D$TdSufL(IEn6($=2P1&YNKFNZ&b3PSv@AAbJn^btUy)3+!g8Me}*d#U{8u4 zVRDm&3AP;{j9rs9-%)tqgQTan{^XWbeaCQPYsDCK{>Ii9zw+f*zxa#4_&*=;m%}rz zfV1{xd;S%8>l2^&#P9yZU;V59x)%gL8F1m+n!u{yEMSrA1|FeHL z(L$H#EQl^_W~76QICd_iw+7w3&_|n0YQq$Jau?Nl9=RdfCr%i8Va;>6m(LqC!?-}k zIHkbND7(|kYCr)LPu6W2 zy`m4LJgdY)3Y5&!HHz3dS2$28)As^04`glfq~S&Vd<}VWIl%q+c4K?&O|KgP#%SvC z64gqJcHsN8`=yZjnb#hyT-IS|;Y&h*J%yK+Q72j`wEO$8wK1u|j z?OgIN2>Z2VZv-4aUqCyrnoHh57n=i3eT8v%;Eq5~Fg&ra*R~f{m!$SO8zCa->Dw&d z6Z_6Du~QiP1a{s$V#HUQ6A#?DUq7(-<{eVGBxg(=^fOLR^U#`)pl>|fn~LS(n&bR& zBfp(|s|5v;yJK=^VPO)ML0<0cT`?~6cP>Waa3ojb7|*S-Hh&yFXDfqsg5b_IZEl=U zFL3!>eX__aDIVrty(iMZ%x3ZITb>kxNN8KK3(n&F2aOI9p&sN z*3RnAVq9hEPTtx{zhy|j3M2+A0&GV}PtYlAk#NvF!9cal_I7P`R9)V%4I#M#s6UUC4BovLuko zYsb18x*=BTiQAk-u=Bm>=#0Qxic{-+yyjzhdh@aPj=xBYe&_s&nA}*Fd$ojXff258 z@}vIb(ab$_Nf*dv^~4=#$Fo3iwKxm5gWPoUQ((tlf9#9)0J%owD3)4us;jxO91w%G zIdkL8HXmYF3u_pk&4+4Z{V(CVp<>n*0zRr$uOz}f!j(%Oa zxlUiS-k1nfTZM;nZM%$-xlxzRPmQ&lT}zs_beoL?8pP^YGGkTze1&K&8+P1u0!LNz z$e2mk6|I?$5~<;GIx_o5elbMRe>`yv9~$|9KVHK-%YHQ-8F9rk z4nd?CKd<9^1IW2|DVn;YPDMK|#{9$)z;P6>z^7KWcb>xcaFY2+YqoUu4Hli<(pI0| zfC5C1%{tTvd-cH<-b+C%_*FN@(sS_CsmXl^6vNPmi4f(g9)J2VpethMRo4UY)c7P@ z^%)M(zJgn;>BdQw&wO*t_{@n$q572L8<=`D_9S@E@6CeFnzZ*Cd`Dn!PE!eRgep%u ztpVimwr3tme(M|GeD$@j{mFm%m9Ks69~kFld)^grZoO>JzXBiq)KC4?FMRO}UwHM2 zkAIv+WiM#%Lg1p{O0?g%qsd~_62(=~4@q6YTdcI<*&^oAhbtQkY7@!T+{KkcHz&Au zKM9`eZi^3?-LTW?;%Du#Z<9ysp7AFZS3INbr$}4)AM)4&Gd|sfpu3-jxcIjTfxAyP z^Ds^qwPldNi=Q6lARi2M>BiOC(Dx$b;%Q9nySc!|P2Bt=d|C*N)y55)5q5*%9vx=- zS)_DyTF&WvRY`@VmCg=A_vUr!VdDkqM@*pod zj#=aDy!8nI_pu9DlxE_j)T_M2(3XxlS|CFtCAP-_qebm(?oEr(h@!Eca)=Z2m-ybG zw3Y5`n7dSd(fV+F}`p);U!)AK&Ko zF!RIz*1PcLjID3j6~sLXvnsE0WI%ROW0=7;?d{d(X5MF9X=@f&Yy2PsqHVZ#UfX8# zT-qDqj_dJl&Zwo5Qgh4KYShTqH;DSAHO@k#!@f6kX0*Oz26d<{WxT!Dgql*QvhZ|0vV2+;}^#O52dpHJ#U~AO$rN_>XBf{x$ydUtK7fqWb zbtwhTfPP49ag03_=ZGUcS@#ImE;5M^2lUke1wWkwf=kUgz*FDWobXlcfp~VVg<_Wo zx#Rz+@tx0)!9BLkGIIpQ`s1^V$y1Z?7HjdNmyPpt=a7bSpnvd#nn-r&v1yaDwPRzR zR*zI(gYlBgmhS^2Ru8X@;FL>%)Jb8Zdv^&)EwEWJTjtT~5{ zm=s3a+QvqeyV-*2T}N2(C6~hJuf1RA^N5Mr`69T5R((OdM$$ldF;&?qcT}saRY-x( z7=ZGwtNG259K=(MUfa@H;Ny!dKa|iuR^unPi0Tf00NQ{HMk8RY&Rv$U#uKAAE>J8Yg6$q;Ou*y#4zk&3^y7Ss&w^V<5KW%>h94K56nafdNNbyH;YoGVIvvWV*W{w}Ci~dlT9XSbXK& zd;yK44`v(jakBV0$BSm^xpRYK4=GwZAIgM3#SA(uY%tQ^8%}HPo1n%YMj3(6QQ?WD z{|B^L0r1DNf^mdef7?t(f*C{OMHnILgs;Xv8Ykxj7+V+Cwx*4vd0@M5{p|cvu`w#a zy*P4gswYS5gF)wnIER5e`V}pTVQe{CRad0(lx!f=*F!gD`Kj*P2*+-pAYC`=h+dhs zkoWx~a=ows-kdB=@vT91f)}!0NSGT>=g7`a6mA-vnALm&g;skSZH&t!*ps_@>*W@< zM{k7dV=oUn&XLx6FxotxHKkhSQMC>qqVS_xXa;Q%$RQSs5tjIJdn6@Gg=%Huo(j;k zw-+ik;^u5~>jk0~j&}&Wn8K-iN#Y%AuA4_~&b;l{UBWjm8%D;_&sxVo?&^$ygO-hbZs2+3k#BW$U0w{wPS3pooAZU? zC@P-SHIU-5J3hADwia0s3$m>z0>;g8W6r_*fnj}B zE5LqjjZu`}g3TU0ZLA;XuBliL?W&s23CB%O{hX`Ar^f=e`!kjD6Yq?r4)|9m)$o42 z(Ry-nFvRQ=UysI-jazFp@~tP< zNUT=;)gS@|#UGV#u-?s>&VwkJ+wAm5!4sM0iWTvsdGetlC%+xz$tQ@h6`8KCtv#Vm;a+5`3pa~Wv0JSdN07N2`*?ZBV6J7 zV93Rx!nInk@US$o=!ah=Hg7=f^p)+}chTDdIzIC=h;h1WbOv|I zIipj(oZyhZeg{D9vB7M(fhe3!Z>oSL&d^-&>#VDN0ajf}+!_Z332*xLNr)VVbL;8_ zEdlQwnoD?I+uQxXkl&cqg zwZu{#VGoIqcQ;#;IX4^&kLJIeIA3fFV2!<55YxfBxXM-W(2t+`HSC-^gojGo4nTGT zRLul1FE!?L_=QTp7|az6!pCa1;!8*WBTS^f^MsCgJM@RY9$&Rplh6FMzvHfba|Z5v zL17K#D>p-CjUyem0@N4=dS@T*XA_Vg>vs6qnh-nktFj8g>cC!DIT}W)ZFGcbZfm13 z9*0$2#?$?~>NNtunmJCpX-25Lk7S40={nOl0v6%)X`dYQwO;X(GyTp>{F^HZu6f49 zy_rmflt#oP=gqrlcYfO?gW?@Kw9T;x4))zCgJhvd@FU)#YaMjXur+le2bOVlQ68J! zbn2J2dWx{GgfBOJo~{{7-*P{4@0<|P?T&#>9GhTr=lzjqY6PD>%`HfsN-=x$6UzRcp@N zI07ZTdg^<0x-35gY?p;NN?La`P(hr|MGd{fGl)8aw{Xu#zliH}ikB=zm24)ao`X%t zUF@ek;CNni&i2?dcG_C6s;QXP)&sr4TyiR~q;D8i1#6E;ISAdUs@z#7Tx;Hj)h!QtvHfF}n(zMj`JNMOT;baJVd#=u8jz7e_v z5h%xFOorKfHy>@mZe1_-(I0#P4ZlyEC3F-xHWx2f#7A2k<6V-~1ZAdR$-Om6P7*+8 zX0)o8y*f&9+BH}wO#gplu%OmC$PFzvabXP#T;0JBLn$PTy=x%`yYY$RUTkaWEX8wR zbYre}8Wr*H*I6)<1R-5Li$^!#^^o&VtP{LSC_o!|L}CNJCbufUzD&p*?b3}5}dzyJ4s z`zv4l<8Qk7e8@@vHh=KLn>ClI7Iqf|di}-TE=0;#IG4D`<-rB9WyA~B7L`_>Rumk( z*sxIc@FLIVL{nT(6CQ3|G-ml|YpAVTaWJjxjRuMjaq`8FeEjz@UF=*;(V2@rE`HIm zr!%`=CJ3x<;41c`jAxs+#L6_;$)&4DP>X@Dn%C6Oi=zv)4*}&;Q$RVw*_gd4Vm>}G zc`c?FAo?xv+Tbhq#ox@BIc;cYfvae(MVh|knVpV&`gMeM@Y$J># zdTrYo5ESdWPXqHTmJWNNh>ZB&k1ZlTy79~%-MEUsoc%gl_ZIS=x`r=k_JUohJmjm& zUAKhNxf*QfS~TMwY(I(Z9wxCbI8|;FOXG4CQ=Ma_AeBzUbbKTarb+CkD1-RE&5I&$ zFm$gr4S;bsHeSZ9wc)zi;n*j{VD07*0pr}iFC(<>8kb=cp^@6Xr4+>B=|>KGv&x=N zc*v2a<3InCOXq9s(sh$?^OAV{`- zP3(9(R#gm{Am%nU0MK?+J`@MQYj?RS(8^42P3zws$NNd)DSzA=zqvi;K{Dd(1wM}Z z!3t!#o&5Lh9~hX!v}eA-6hr0Z-?hMcTs!z-EBYJ3 z?H8>voi8#P`fW`x(mW+FghiV#N{N8Kyj&j^*RMv8za|fBz|b4R@nKO~+IK!<=LAD| z=OyNJjhq<&wM&En$j&*UA+Bn@gf)(>i%JwoQ+_72aj&g5LMk#lRk3dJ5trvss(tv< zj4R0-9}8`VDbld``}@eiDN#8rj{A5?%*~unb0T~k!_4=>Sw0UYgq_Oktvo$!4%|=K zgDbYWIbcbBZcT6sWd71v?Wm8nGq{IHtZn7Qn4H51(LLjR@@Zx{K49hnhUd$^PqK40 zjILQCVp#!dh1<1;P@7;L!SW=RkM%kn_p2Gmv$aO6Q^CpmFrKuNA$Fpja|1Zqj*%MS zp?boOkKLPV1CCvMF|Ixiz%eko)7x71MwrggW~ZM~&fL_^W-{$HF*k8#B^fsk-tXil zPaP57Ibm1MsM>s!zcs5dKpBp>8>es;YYka57oqU9l1IidGsk1+GyVGQ2pI{vNq-{*~h5C6%p{_3xO7R}4{15$y`yC0BT zf8f*n^rt`l;a~pC|H?0X>C0bw_1=5D_-AqGCeX!;7dbxMS{z(a+3o_jn>6%i)pNnj zo{OMfXWgN7F1ao+3O%-SG2DVWEahzITTGAc2E6O=l(TbDEtVwYoS67th#}pgw}7?1 zpp}udyF-;vgpAC#n->>%?Y!2xRGRwJrKDGiLeN%5!z|EJpXWZW`?}7xj*tKGvDSI4bzawX z-*=mJH!W>U%=J+FDlay+nKv99U^D1r5(3&HkIEfeQjEOZqHGLpcD(U}r_CbP6tg>4G9T*=~nLtGVSuf5egCxY2OVE{b61YuK95 z7e$@JoQs=AILJnGrl_YybLoiaAIX#^IC6W#O8-*gj7~cpaq<@2IlpUHdm-y;ZC-2Z zRVcb_e($)2eMI#cJVR+irUFMtGOI@BLS^9D4~ggFfZb9R$f5QCfq#GZ{8x~FJdj% z@Ex>tj|{x};RGNEIpKw$;@1CkB;cZ=npA@UB0HVc11}g}dyL_% zn2%uYQ)9RY`V8APx(&yb1-hJsWXHrK$ju;VwKv9w0}3R;5(8_)EV@!BR{fKtEQfFC zq?+Ha`4pUji1sF>4d*uVIUUDAuGb!PuQ{D(ayY)m;#fU~MIGPl(82h_7NI_`)jR!C z_W0-c+HO9W_YG2U8rH;+hh@NxFFog$W5JKsdO)Oea)J0n^i^fRwWBec#JO4}GPuc% zXEXo8qmv(6@$h;#H~Ny~1Z+>~oFOB(spQwfsYk96g77T2s5C!wO{`($BYAkNDR5Fc zie*D_>^WBZ7DndKSx$DlisKm479;h*P`?=PDSrl)u6Zzp!NasR`KU&ne4vf&od>k> zVP(b~EkN%GYma}<4%FQD~kv9?Y?D-XGAZr2aUISi&)`8wprA;E;2hK++Z zn$IF?51(Is^(!y@*MGn9sZYK5t`C3sKgH(pVhGQ#|9ofQ&g*}^qkN9;k%7PScmDSO z{9B*>?3cO$q>w!p?*4zQrHMzyfo&~scHhleyVOUaISaD&PFWt7CfF4s>IxK|6h5no zulDVDuEHqnLB|4WnODzx)+!kvy`#`#m4X-p8f!|!{KNQfy>#jIrU?%K&sW<#)V0?2 z?*=b!0W6xDN+Q$Ouar?&Lt%j6kAkdbZfM-Jv}i2}552kmplTvcPL9 zoeBDIpzEXZI&5(C^Jal2Uo5>Df&g|viNDb^iKFQQF*NoP9#+NV*^A^Y{^4SBqU6I( zYKcIEvvKz(2rBx7)&b%Kt^9F5;F>EAbDnkeqm|A0&gqhkQDe-uU@CClFk%bEcg>69 zfoU(WWHc^)$O6!2?(}Jgj@$3#2EA^vdCzK$jRpCV)pdG|h<6X;%YWnHnJ+oWJe7vF z{N13^5Zf1JJqd(?#*%Xt!*6B?thO61Kr@}7Wao%VHRm85v3x4~)_zGifi_2p6O8eD zGCrLZ#{-@F3AFC@YabW$h$vH&m&a7Ym@&>_TJ(bjvJehN3Ml!>kv=-Vy2iYg#nT_j zDx=8jj{3jBYH_q~R&7_KnSG~Cbkss}!@zwvKlZu!}0V#Ae+Km`2^VgdK zXRO8A&6+B7NKRv5JoqC?nQmg7vzMlwW2<-&%N=Q_oiG0P3i`!opl~Q5B^q!e+ry)akM4W^$^xF{i`Q;^^{j<-Y4s*Ls>Y%Z z`qcH3e|DhtPMr8ix`hEV6gD%{#lu0%DsG{dC;C8BadRXn`0FDfr;|qYiw{IFEe9@HM-GZOpD*x@p7| z1JB2W>~-&)bABr7(fA{{KY?b@rB?2!pDF7$AA2n0DnH- zpbR*_o{u*u10VX(hrasF-~6xr=b!oQ-==6V{1!3Z+P!6C1r8F`-kR(JVyR+9UqP+V zOii*A2ocUhxWE5tMcUUQk+rR`g4nd@?#rUJ7wuTbC|a%}}eLzwg&uz%0B@M?j*~@R~N^@XC zp09TUSs1NkNVti3fKJIzFx)VRr+#CZj5&hNyoi_hVr;>2eB|G#iyul@<21~`USAvw z568o2hY&oqrf*K!Mu>Jdj>_^VEej^QhQbG$JB&d-xk*&Saz$H^Wto*bvh}|=L792V#I4d7tAe~y*XF}$|#0>d9Y(HIMGLo zLoAc7qp}rnT(mM5k2U;PalPI+^J2!QhSr>=A55rT;$rx^&93-R!lxykJmk!y`3(fX zHiODuKM`Hr*f){-d5;3p1~T~fs$X$9&WNmNa|D}^s}Es_=~ID&E5XBo^T>?tBg3iI zp$B6CB|+Y5NOc6YecM9M2cfN}zw|5!8VBnpurWa6Ww}NJK20Qf&^B+{b==!zN4I(* z1J6p)8bN>8{WI{ihccwKL0OMfF)(y}6RLhMLO}N>BAJ=51Hp0W+D%s(a|a49<-3~W zSffl1#`C(+7qG~^v^t1HgRmH;qxY^IPTfVQ4|xv?huAha31E!+T>U=dVy#$K@wf3- z#^KbG?UWm{u({D|ER9diYyfj+}s# zsUwW=PQQ83OuGT02ZGAR*qC4)In!}qPzLGNT77bkG$6D|j4?DoU2j|~-uWW8Q6z_& z$3}Ptd!E`TR=?S#ZxOJqJGA>l(#ZJFC$CTTx^_f$P_$3`e)G!Nq@Hu#%vKk*@1b1} zbsRv~r<~HmGuE`azWsEqfsDY5ef(CrzQ9p`;_5ayLG9>m?)6x-Laf z%U;nii-}&!-=YNb}nNG;HQ83r{DRG z@A!`2{R@BoFMPd=+`AN>iV_9m57hvR1m(j|L9N(+o^CC+-Qme$HsxVSz zcBAYPtBA64_u4y_(WsKzuGq>verT5NQLt6JGNsAOa1n>Nr4B|ui8;#jD1 z#(Wrt523Mt$EO=}ybc83wz?-ifxQ9EnhVX@1Z7MAW~mJiQen;+a0cnTa1G8dtJr&O z`pq#5Eism#JlmkTdGl{d`pLKdj>v^)dEO^FTEttxdD`F{ycV-9lfyvU)XPT;kSmvU zL2=BDi+OxfP5MBuZQc-`q1vYjwfI67oo5@l*@JLwKFPPB=PEDy2%faNY1B@iq>%2% zJWB%~_hhIJ6TQJ;w0LL)yNaIYHAKJMqN%Dn5wl zHp!NB?Y;EPKRWHryk3vH#hb4P2I4rf={WrPa-0CC`T;ZjkVR(>sz6SbGtTA0yy=y` z`g8o0C**OWCcIK5Lpr`Z#+&vmDNy zgYWfp^LZTD8wg?}!=taF(%Y3smi5;oJ4PdKLs?zqdhyzeb3~Qtrhs!d7gHO4OaxC3 z1l}jMlD3Hr1|6?at6z0uDv0A*KEW}#sTI@Y=t5Rl|AA zx$8d(XdXj*YMCqL4qa@U#-lVZt37m__vHJPf3pVS(MaBz?4g&B7C`oOy7||b&A?)h zTHZIAk#(2;>f8jR`xvFkf#`-=Km3&~xsL-{Ucom{g{3#A;}$&Cn=67=do*`h~5?<=Fbm-dQA-aM>U9hJb0#_r~j^_j=-Q0!Q%!W zd=rgvCCp)IjkkU_og>Xd*O7xI8@#R)Y~F7k=<&7ZoNG-wwlZ?AX$>hUf7e$YOm%_} zB>g%LqTxKIOa9A`I0ky==Db@x&W#wU-+bzxbe`h2aJ-f@*^)VD2rCa5Re0QSX;hRo zrmF<@a{7l{R){CU<`NuQ*=XE*VRkvEVk|ekg1hTbLsRTcoSa&vFas}Fs$Dbl8nb;Odbw1+K!o~ov`$AY+0DX+3bA#)8; zbO)_lnbN;@A?_#P-m$m1Dtph!Z{eR8-OyA3UzdYNeUltZ%j#1&S0?q3MdWcsxcF;C zv-(e?k8pEixe$Y6h2Lz^0<1#Kt2e})2sj66Z!`EP#=)yigN0|^>)@~sF-zfJw!V0? zSq_jbFzq4G%|C$sYIQj1uN9Ee z(2eKJzteZa;(PkqDwXitD8-+w&Dl1O(sX$K#3Q&iUYdYqy!_axT?^2z1F6axi~OF( zQ^>tK5B1bHDP$|vLDyVs!a?+*h?+~)Q+{+HeR;@rGm30CgyKkZegv>6D#>LMBb*wmc1~Sq7)2e#y^!30vRG!Uw1x*`}74@%)d-M$j2-7j%S$tTAkz|v~ z83%t!8G}i#t!)Z3NpE->ykB89bf2?-J<=Ak^0bo>cwQ^!1zK|kfhP~uL8f0NQP+H$ zFEM#&0|^iH?G!hI*{565%K2q_zBFrw#-WL$DRDj5W+c&d&dyhbHr{>9MEgACD{q?Y z2jJIr#dz4BZ2FKJFUNtp^|o)2jhtL56egL^p+NUJ2XY2#Aw`y(xWdgrr!LX1MYk_fXRl z=B#C5JJK{L%6n7WIf?WG^t3g4X)&+Q>Sm-E%9!J=Fe=9Ew=BE_y%~0H7(m`RzI0DO z;m-^?hB2?zEhXsR;?|-l+>=3QMja&qg(1*Qb1|L3yNouXC?gg`0CqD45XZ z?<1!D&kwG3d~oe<%sJ}=^14_Y^!;GpH^78${!ZK;Gz4YDR7c@)4ykS=S`Djg6ci1? z`QYQ^g{X#s1Gd#Qm$$}LYf)Bfdl=;vP|cYL-EneEqi84;XLUXzAz(h#R#f|K3YD>s z4KTVc=4>#*(hqT)AhkhM$$Eg;j7QbA7Bko`5%bCGH6vXQs;aX3x7-+gbYsXE0?h$z z<6wC>Q(sIynh(DEMh!nH&R1m$j9Q$&Sh^sFhdwCM3z?T~jyT7>F((5_{uv?1)xPtu z_>E0$K)h0S<1R@f&^;1Kh&E^aimcA$?Awbk&-t2^TX*v^4fF2EJAP7>w=Oqtap?W; zfB%b5efm=`zWd+*@LxekqtC}1mH}tZ^YI2|;CsLKd;hn8^gF-vnaX3=J}v8wb;VD7 zq200~eV_W$u##CSB;F!aO|Cpa8DFjdD=R6W!kGd)8$-8PW(mP`l;PPs(peauH!IRt zENN@WllIg_&3?~lb2E1!=9h~q{_oxCDXgdlv|@V}0DTD`H`~-{iesA(eWYVy^^Q!t z6BOq~hsq1Co%eUeDvSVoNNc&w=Pg| zG2GXfB~OMHvdh)6h7bCoQy$I@Wb8}wskq%UU-Z|n7AmQ%$4e)eIXJ!>=zXI^A>%ae z=6JFwrM!jG|7k42jgBUrpKahx7)NtSz7I(A(w=R&HX3+hDS0yB!w}E$^HCpyo5>i( z4S{!#98>?-x1joQj}!jJ4PsW|H|3Czuih+$W9-)r$#T;(FKh8C?Hmcf!QZF6>WEMD z!%&FhY(C17SlOCT(Uwa5GAZhF_J+YWJ)Zik9vNY-E3!Y>>Dw%3i5t$;)Is5#Y`#sM z64q^aIXZDSNA-(w%uBJ6?`Dws*@ja9srdzud0@Pm&_C(o(li7Qxgu`LXk{|O1Sf{O zJ#q9!WxC46AFCLRSFbc&Bs2;lTWXNXbIVe1v}VrZqnR0!Oom6KQhRr zjxmm}d_xo6FwTWn&4OwiWl&#~N61eTcRj(0q%Kb}6@cauqM!!Ck2vNmJBC{9_TTnx zK!J}ckr1~q_nREnGVyny>`$h&5+XL>8=B8%%kVZ%w2OTRqDM~^J#E|O$WHc~Da${= z{XGR_%%}J|m`dl(cjrRa6P$hJ_Xy?tt~bZm30O=siM0L!=}U)(B4h(=$S>A1!x-4_ zx>BCDkLIBeuSpuK@2zSJqxn&4wCiwBiAsFyEA z(LjD{h8Aq|V*O2izC6|x=TUS8S8L3lMZ&ZCd!cOtJn+5yrTjajV?)_2=&HCL=E!+L zLYzz6e>i`UbgTqMRK5G=#^Y~`=sc>%Y*WNWz}(WyKX9doU?WU%>6qu^cXO|{ITAp= z@t*@Q+q-7%Fg9XzXjOkzp`~G-o)BXuZk!b;w{g4{J)|kCPeqAOPk8&rU|eM3D}QB&8xec^foEb6Y4QFAOR&TSph-(A?{BBptg&qFZd z@!O68)6Nz6viA+Z`Q`Xl9<3e6k;Dp6udrnR=3~%dtm9mv%d;06YUoAlDNeZavF}x# zQ^`**;Xutf2mb2ME8eJ8?+^aqi!c7!7ryY(pZJNN_?YbHF5EUZPj^rGuonu>=ry>Sxj0Ok0kbrMcwiymPF)OK^!xG&A~WwjhAim z%!N1)`|`r%>`{Qk>b%f5NR81FZMfm7?-75^>_!*wSn%G#rrVeNf5bGI*w~s+DT41d zQf!Cb96Uih~U^Ab5%U7KkvQGJmPEUOR_|r!6)SJoJ&?Z zIV?U0rSvO=9XF|YsA>}7b<-@Iu!3-SP0g7)>27cj*}4JC{D~=V_|ndGm2*gS$1o{% z+GyXMD|Y6K`uZ?0ZVj=2uRh0I5AUmImC6|%%;wLPxWOc6E|Q-Od(9#*B0CCYpA&w^ z8ws8Q_X<3mY6mpN-taDnTpd!y@S7u|L1->(2%Z`04{s`wunI~87N}9EH(#{i8zJ31 z6es;h?&H>f`(EezgsH-M&3~-SRj!NH7VkcEsj(W*=3x*!T!>VifO;_x$BL z&o&Yo^Tn}VB3@gUVyeA^am+3+n8F|Q^#eQMnWiVv=yrW`L8JwREPeBAey!W{WxBeh z-?}otIAO$JUfLT*2dRm8#yh`^8eVK|h_`BMc=~DIL;Kp$@a8q`lj8-5Pw~<4hVE^$B%U_l7R~NKg zLs+p+E&@$&{#Vzn?*@Sb=QQKFd$0u+vN^sGvf?;F_eNhEQzZX6I=QI62mnled-i$L z2&=ICPfqf_dg95on@ zf%f(Cw$-IqJrtO@PXeox&ZBVP<@@w!%(*C{Sd9W;9J=N)S4xjjnNQIQn0_?+{=ctg z^Qp=H%J0{I?bp5d=+AxZ#b5eM-|?q^?8koW*MxgM-ns} z(Qo~hZ~3zy_}~X#y!-A~$;yhC@8YkR+>&X+C?wS|%8~Wz=CC5@0&!$6hvId=_GN8r zL2FwFp#9&jM|9 zZN!G4%f9RGr?LI_0}HwpG#K&A#_5^|k)``;`>s+nJxmZ7o;;MOlTG|L4tSOKv%ylx zxV^2^wBX0g@-dY0Xm2q^?kp(!^z<&b*m!;HtC|mvN!a#m;kWsgrf<;3zhfgEE@SK; zhH_7%PieGxi6DWkjAAI;_r7cg4d}x-6!F06cidzi+2PuOri@4s#dPx_MV=m(f8BL6 zgJDx}+@x_Z6kn2|F_UP!3F_0xfeA2wmG#hGwh$1;K>DQF&h&-!h{f3j-4Pqo@@sw? zAANBU{HwnxUU&Kw1$rA(^~r~Cwz*L6IQHR*t}ktLXp3V%PI=0+{FpjS<5PyS&HI}D z2x>z$UIOWNfb~@c={x1G0QFNqB)=yhsT+i@M?;;zLOt}9!P;!mg6AJn>eHoTM+xVy zn|o!QL&`tw-U*dKZP@V57k_tq^VaznnX%}bCt^511mRgtvx%5aXzKcTbKDJ>&^{I9 zd^cgso#SgP@;Ao$h;SIbO2{rU^!@3`a4+*q_a8S;0Vam`4I|z+b&3nlu6J{SYR z*Nmpi9yyuFiSywre&XB7f~fi1oF^d<4vjYaje!vtE?ux$c>szdZ081x*EyQXPfQERw4=nXHyC2HGo@p_}F=e3Yi zYjubCq9eeO8T`5PIhx8aaGNtP;UgoCME%Jx$Ku-BHKP6wWF&dtV_vd3-%prF4*2q+ z^V-s|KetkW4@o!p$!_D9=fl3uqq=%iOr%g9gVbk1I3DxgSeV3*Z?YxmrQhIp9IvMe59eZnWt?6ZXc>W%avzy(YVl5 z%xm(vIJ$jvp>X>ECwC+ntheMDlBmfjmc6S(e$;g?CUEjlJVEtRJSRq^q0N!7o`Cah z^?ka>J?t|o$mIvG(o0Bh=045Y2VViF499Xw5Xkn%AO0#Qh^A{JN3>kq5UeNZajh_s z-_|6X{zb6g+{l*;50^Ogs{0%G77AAH|m z{`7DDgNn1CF5DW^A2c&b7LOh@Ei-L;C|avaCyg%IRiX81cDUK2D6P0!*o5=U)8bm` z8(pSF>Mk1TD{q6ByigZ`K4pV)i*^Dm2u@~;t6Ku~T&#;noa~+H?(T>`+#}iyaAu*k z5UaN}hE}UJ1Z^fvy;T6Z5A)$`ALZXX<;3jCQsNpZe-QVzEjNd#0%#n|vEXMxs5v|0 zc<7(cZn6l>ZvnJ<)j5841F9s4AxZ>W8Am8@TsRq*zNVQ3i10Tq$k2At3)i*f^rgS+ zrwC%hUyi4z_jLDeAN;=>>L(TX`QCtdEHl_0$Ev@sod^S<$z2rZ)eO z_c8ak2-5~$x2F%DW7i*u!(M(VKeTk;YNrKHU3u!~qcjN0x^S;$ikmClJY(NaINYq7 zTLIkoX#SNqLh!dhr_A^uhXKb^%-#GBTNC;qEt@&B4bbKq)WgdZ*lqQX;v;Y1mn-o^ za_l*mt>V%;bOSMehikj!$&=+F$x}8QYuY?@qd$TL*N2fd*EX$E-6qp`)baXVF2uWs zSlTpC-q7dSF19%s9~m9$`a>coxQ@-E*cXg|id*rn5jfc0n`2qPl%Ar*d2#!yZGom; z9xt#o?Bu_aUc4R$=WdR5Pe;P9!}x0nxr-awu?Z6h@^4M^VQ!4Wyy}YY5!m<8{(PWa zZuCdA)jMU9wFqi+NZC2fQl{M0iP`uWQ++Uo4uei9UutJz)EWigG-P`-}=>P+a+mlQZ8hqzS*MtA9hFHTAIEH$l$}*8XXFou#Ko%$4VJWQb zI=R!g2}_&?_00o*$L5cJrN2YnwD1ypyY}G)rwl$aM!BMK=({!tZ0>X$zmfs~06+jq zL_t*Qsm6#w^aN3yTrXK_5H>=iEcSrs$#7A%Cg&~~g{55&eS+WLYrro@&U{1yT1@q!u(kDX2Bd+Y7sDAkG^en)dN9DL2-E0RMvHgPSCtMK0fkKy1)Y$ z_4uruc=U~f;8u66vqEZthYrV!nk~zaPQJ?r5l*FF{rjJM@dH2bU;bxGJRfgv1~zlv z-1y)4$bbHqe*Smg_x|^P)7O6BYyORQ-uFIg#EOeWW<`x^(PBfYPN8i9q=?F2aa`ME zR>V{o7aZuxI(*;SV+5sD??G@1$?d*eh&MLL3R9{^&)$z>}&J|>9bor5g>?s)r4QSh3 zv%n#udN}IP+OoHHulY$!ePvI-qHc#|Jr+TE#}Au(nvb>g(H*9ney+pUSzGRs*du#jDwlSVvo_IIvZ;|}dOkd~T}dC6}#6wr#o z72SClZ*pWa?vb9(d4&Oa{N>>2=)}dK-G=CF&M%z4D|Qc4*RN76@sVen=9xd{RCXI2 z?H(JR?;O+*vu9L#c1`TOymlSNlRE!tYz_$vS$H{N(|6u9^)6kW5(tZTf1Ss;w4!1; zF)$A>-1BO2H@bcs1zG2cGy>a=C!KNtot!xw9r-{%xLq@u-*5$-UBSAzIp-TEbiBST zHExUaH(;^a2$3yGdV;_6&%RMZ(gzw{2ZQyM=RMheiu$oB7Bo3g*6iPBLcVc0?{Z@j zFiKzoW@zFoZTA462r% z=3Wf=XFQfXg&`dt@^>b}$d;@-H2p`9G~%Y$26u@TD*K{rh(<_1|6y~suT0}A6#cy! zGe2(5%(Im8?woWoB%+sYc40R5G%N!+>)h-HLtVs4vk08&AKgv?$@5rJXBVzN>T-Be zUR}=~=DN)_*m>yYT0;paxG{coMUf0h^)?Xuf@x#(sg47tQJ?y*OzJdp0%>{D5hzEe zKOgf58FAE^Y~lzw<25nkvYRE@(wuk=o|qh7Pt&r)?1tW&h7)0G1I469NKei3Ij1sr zG)Bi##B5B~ylStpa=bmxsj=6JHjD$iwkPJ8)2Ub){}^2p*T3p9`O*d^%!4XUDG$X| zoBbz(3GKCY!SKT-C&6GVOeKRH-`G?_-* z)rDy1ndWr|u2WLv@jmL+vhiZ|M5TkM|{im?Z5StKlzhCaiO2jZ)OJ0 zbbK>|e~x+?_@Te~H~;!4Kly8~-uI5bEkseFY*?G>_?>Sz`O+*(%^1$Be7o;~=6%_t zEWxwwrqut3e(6$eZ?S@3rLwA7@zg)Xj>_dWMWQyn=~&U#59z0FN@PV4$EJ(DH#c@r zt2oYt%slFxzKocDDbO|@^!$VO^Fu1OweJJ$TZ*^LsiX}&^691!U45|_hw0a|_JOd8=mMd=ZqxErW#6qI=ir0w4bL3mtXm2yD}qg* z$Z;a2ST3QeNKu(M&N#IS!;c3`x}PF=EIQgpHnLbDCsH2Uh)L#1^RyklhA_qI0-ehU?9np1_>wZ?~SrZ*pg;oVkyhW zcg^Uqw4)49uXeMbIJ^!7$g92%3TTsJC|l4xc^-sy$%q z=5jWD`*Dr?JOq8DN;R(KgKi1{osGOtaU#Rzou{^*-4L>C0(S1!PpbeuY- zxF#MxVBU6qD7Nt|590Hji^$N9uP4V=e-}aJ>?|&wyV;eIeO=ge)|&$wV6sOu8`_(r zj*Hks(}tlo-|*p%)9NxZM=saQB@RpLYr*)mb!qcor}}lbW;^ka$kuBZ<>L6zuYA!q z55;lfMyiLN=@UO(1A3(0#VSzk5@%b60f=PVF-G3&SddQ8brfFT205Z$cUrl564$w0 ze#AOXlrFmaRU)oruRAXm<;@u;yS^R6Fsv9Nn=4x2PB=V_iYN~;40smLQwXOWU+eP$}d|67}or#dy-XsCxn7y zvbmqS-OYD%0hVF07ujsc^1+wRSjJb>#^lSTbJIYy2XE^He8-?UqQ3SglJmT@uZf`^ zvhXLUFirZq&g9`$gYy)x*UjXedsxl>^a5b6W98Tu!|{t#yEy0?&aQL)@Q25V(Oj5` zKxEVOcPQsXz6X zDK`JZH~cH#@Zw$mH|Se0DOeUO3uNVHMaHj5KfdNvrCG(XfbTEeBJHe#T&0q2CAY{z zw<*g)C%+Ttif7eo1q&vJ%BZ62r&0PNcAo~owTNpfkk@ow{KW;m#jphu>ATUJO$C(b zemL3HX8m#r8@6ZnBRVf9`}k9gYMUPbi_I5B&$CaaTzoZ{KIcS~FcYc|Ya6|3x`c{r z(e@t5U@G7`XqSgy7Hn||Z+7pZm_!if*G19u{PIb zsXkb}L-S+o#@@J)y&Fe263HU!Cz4Oz3Z;E$oc*iy&I64*UWKLw+a^OtJsdMmn}4*z zkdB^h9!ct&nmR44C&xBmIS^6{3Q8$mX zem0=A_1Dz8Y3>@Q-Dc7`MO(Mc%e|3qd`HZ;#>$NkDB?IKZiH{H^me(1#6#3JJ-F4T zP>S&#%;ma(MN|Fwqy7%x^zc6k=wE-`-|IDO>`K&nn6NUi(iHCv#vVjjq2g(f2Bxn>hhW*t*6wIk%lKj0sfVewimMRt+n1H${3#gnhvo4ge!fbZZh{$+>)J;y4E$g`)(PbwhxogxG=f(QxbtH(x{*9_nJICP?!zYDm6AV9 zFW%$1r(F-6!Ei3y@KjRtuZ!l?0ion|(BY>!Y#-m|u|!9Rd@Ij{599hLorft>@YW1P z%Lhu$<%6dYc{8i-+RXxqDI&wUOTOHLZxBn(Vl*$kbEAUKGk(bDC*omTF7ms!F998Y ze4^tUiv_V{K?m=Q-<^rKW-9l%u7K-cJeLW50ZN?WIq|x;vwop_UH4+qYelO${d4=2 z^JQxg{aj6wCP~2Q>sxQQ(4WKm6i00 zzs+-0?yNQxxr#DHyYj;fx|>xO3K#GWi@6P6ELIj$Iqn6*wxEV}idY4fwzPwh9U;_3 zNjx!jc7sDdKq?7zR@hJECJF6}Z2={bMc7rP{ z8u_MN$b$+VxzrpF*;i&j>P^E98EyDVy!oKW5yZses|A=GbnZ7yV zDP3``VZ+7u>WSfb2o zCK|(=oDAo!;`0bc^%snEpf;vRw1^|?UTm_Sq^ANtxIFzN&WQ9`*K*IZvQ2VxEGo9Z ziVyE0k8L;1yY{3yf3*b#`TRktH-nB}ZP;`~w0pMM=47;aqwU2j<~AnS#@=S3Nuuo? zlsMe9C_Vcr_yP+VcwJevPZY(VljRpVIl^q7Ei2-8z-aSHKS}cgVxJUFAW{EplzB4O z4=-)25BZ=e(3^`ErF{bdP6CrybFku~C2;#RssvOFigbpUZ&uusb+p=ozTG z?BG?CCfg1Dd)!GoM*AdH1UcICRU69ivdIdMgZVZ1%Lh;S=x8y2FtNV#7G^h{3h{)* zp>7SvL3!HpWc8}-ZO4L*430TYUaR`&SOkNt-^l2jThY-`R{K1frwTeo*y*|yM;bpJ zV$d#Lj8F45p8lY(m&HH=9p3=d4c9kY^lzYD7n-o^4M%a(=4|u+alYvar688Ua@=%x z7Nd@zjASs7JINp>XynDCTyZw;u#8o@ef0N@9VtSJv((&wGd3?yoOJFSGF;90s$OFO z-*x!DX<5Dz^6aZeE#!NH!0|r%f|>8q;-qWh?biU($ykF1iXEvD^Eo7)H&ftP3xa5D z=0Sq6)NCFj-#3Xc8XD?*bIAU%%k9m+y66NSjW%MwNi==>jyJTll_wvzjY}DyD)qQR zVBY$0Wp*7hh{*_G;|waT<^!bWHhs&EZ~mGGbyBmh%7GE0u_7_MmbW$sHnlkzxS-@m zdSG6==EXT9ElYfBQ5!tzHwR&wkJ(3%!#Q>_r1%R;NS90HbTM7eJe8RxdP{b6b0{+( ze1+#j@WzX?>y1Ahs^=F{0^1%^?B&o^;bK}3XbUMZ{Z7M#_85P7)!Og{z=xB6O%)-$ zfusnWz{btlylFrg4XVT0>s7x_*&w$^*8md3Dmvy&IW732FRNE3-`ouPNFW5EZ>p(~ zh#IlY;p)W6lX1w>COom42h;R34VcEU4=9Q|-|(Z!p@0q;3nRjW_|g~u;Ke`uC%^OQ zkAC!{|C8M3<2eIef6r-n&cF}<@DIQG&hPrp@A}nW{lts+z5ksS6#sRMAMaR*tWyd8lL!UGFqJGN4;D;L&y-W6d^(-4tK5lW$gV|hQ*)tE?(bo?8kESV8qh==1@d-QO~8z2qp zmpJNbMvHBv?AUjQV+8s~JiIEz**?dGGakrog%;C+b*!$ZBWSO>U&Icmm{y?d1 zMs1%>Ko)@LCNQs{sF$?d9sN-^I=X3W(v2aP6Twq_>2dOt-%uR$wnQljVgn=P@?Cr4We#j|)J7k=;lj*4-+K|rjcP8??HV*G)@S=^&;rcs*bmga zZrW_aU#?O-A^Wu%`ir3E>i*(uh8Bp}=6h#N&MoEUpZuVNha_-v`XnQ#7uyK>CY}eL zGjH|ZIDC%tgxRJVz_)y-%?lSiW4pH%&QUcT4V&S+{2-DO4||=Onh1QB=N9bs$QwZC30)ER6-vS)%@GjN){3%7aAvYZ%oW=T$7nQ9e)X zTl@1GySpKsx;ac<7~16+dGY&}9XV+(4VG)cnt$TYXVejZG#&BKZLZlW&-&r#qV)04 z$Ncy0Xb<|xIhXd$CTufz4JB^lM(bRM*@~z4G|cb8mE83OXJ1LD@2AP|ciriek{A_%XD7^g7!zlg1SN%*&C0gWm&f*9psoOuemdKYKnjAF}%6oZIjlUMTB?1vM)iJ%9-aN0uKsoWkl`d|!Xg4Q4>uXFAeEKR4qEEW_uieLU zk2;aVMmEP0g)U1*s^UV}oVBBj`7w!;p^;B9ZHJ*!(!)OUQ%tYd*_q!~bFucu9s)T) z+~$SAmWjo4uiG2Dxfe`Rua_w8_>6zs8Qt}ht&%{7wdcIbuDN=}r~mJ~`1r4U{Ka4Y zYk%!KiI-rXkLL{dM&R@DoPm>pPkiDNpMU3_cmBlJeehrSH{bs?{5^mQ5rqoAi`#Ah zE%P2$A&QLO1bA)ZZ;?{q%GRO(*c_-_A>%|bOuR)^Oqjybs)$k0t`D+Q^>{k#+j=dG zLxTdH0$9;}$XgHvvG9tap|h-NFkN2{qJ{bt)GWM~3#F3}ZY2DvHy)kK0Db-!BoDaggAzh>+ zID=?C?AYF>D%au}hI37KB{T+W%HheeL95w+1JJPZl4Bt(kDHgXdEk<{hEiip=i>u? z7{`J9Rm)9>CQ#bKe{cc5^FLfCk&_wy;xz{KJ&yZJy_?@{+9j)^pk`Db~&^*K2o&rmE#fW zXg0~!<}>Y}s73f>uFIb{Cp>0Pk$Sv>6yUi&#RA)K`sR*u#I!e+f?m4kM8>ABXXZ-z zP!Gm(7A)lDT1hC1ool?d2}>~8n#sd_$)LLt6yNt=Q)XO5zd@;PSNn<3749*A3O>qI z5HdrO_o<%>=!&ss0LNP>&=)}M1tXDY(le^nHqUuc?#`T5l&8Eq0q+e_@&z#el$)$5 z(CqLMW_#4lgM3v*dv0tMx z&D)JZ!ycR-ek9OHoGkZs=UOt`++3Svw&>cdjw4|6D&lkM>xh2u52YIb?HTpIWl}D zJ*H?*Zya6+cP}$~g4jV;@oTG{HG1?fTA%{=6i5HbSTZA9lZ@M^j$Pf>)7wM7{f>D* za5`ST?5oOieu`TM4o7pym;tfNvo%#6YpKe%UW8%$rq*5ZEkapJ@AXdnAkB!BL}6Q7dD1cyd&G6`n@i`6qG9jf-Uq96D}+PPtTRE=v;#&i!I% z_MvOPMZse4^GI3DTL>SHjMuK}6|23AMaB3ct^Vt3vH4GIq&cJiGiE7Z2(2%k-k_db-JITxK$Hrj(Dce$-MfP zhab905eq!HbE(cY-0AD8iy`#o6JPpR(8_}OlfPXG|D@*V@ROBN5u3i`lkMUbJ8A#vqjE;L9{T zeBjqI%&cwaM*@zUJwZ_LuQqt@TN43!?I(?%$MpMDLqh5z+B_24r+wU@y=y*G=@6d^ z38pU&?lTj`p zKd)8SPcuC?KDg1G6U2kNceB!%Wa#2ZgHR0WB1=B~1o4^)!88Y?>@p|+5aaY~CBo9X zV_$E{hQ6p3cD~<~UWLJdeejcpm>hXbU!xi3>Rtb|Hvbh^?y8O4 zwMZ{Fhhtsa*g*(y5{r8A!+GFs+)dE&9sS%?=r&3Sw#f6jx&jFPHL+B9QKm!F1hQIV zN62}iM8D<|buu<_qg?d{@0pKf5531tq5cf+XxsHMd~3;SJ?6a-N~X-kVBa_B3QwpR zeY$y&r!U2;Kj!(&Yx951FFlLa@lFmM$EKTk`82eL@srvmpdCqCqvKMWa%_Le}L@bJ|2bsUoP znsW}#Blk{K<#|@~j4>+;9XaisI-_9rz+YDJC;OZa)^PN3JYMk?tb90rXL7c(U0+8u zyK_n}2da(bFIN|?z9g&0Y^W`2^sZ@o=E$7IQNj^}CW}_<8~iLtlW|<=yDIekB)dHa zfxOMSk#Fdz)_8cM&iHgNHXdH%S}2}u;e#abjLO5Q&ripGe_ysl=Vu<#7LWO897-Sh z>aX%d17RWoz$_=co0qqvfbjEQy6_liG6{h7~x z=8ON`-~GG)MfRUxo-=SV;5G1kJZIpMf$#gi@B8w%{@FkGZ~r=fEad(C(UAVPIot{* z1(cSx>KqifTZE*@pztW>*D>uTYvJ|Nfy4JEvdZQQM2lUEkeiHSV{jJ93#;An#i`); z@q~-P;FR#-zUm1d|L)EH(Sk{3)b3UQWG%EuJ8%F!T*$!m|F^eV&`t6>C0qXv--vTx z?`TNwYhPkxtRjw%z6TA?zUFxXfa4QB4)i-h-~NS@x!6eJ_zMn6j{rg z+b`M&qxg^6B$<&%a}!aX0FH>Zb@3Qq%gfvjTKGNr_5dr;xR49t3qVH~4aB zqtt`*W7egn9?vuR%TGA!%x6-ZOya(w=mS5KRfuD;&7*Rm3;KsIi&4)9&u$o`VQTm` zRX764s0bgTahJ0zpqyR@wx+I0N~?}+qA?jAp4WCBi)*d!ng(YaqbJ|S0JT6$zYIa% zKACDHkVy*$3fjp3gWg4-#rdIc=8&^rfxR2t#R2W)B*T7X7qB3a@68m>ifr8wEGSIt z3p^dvBhQS3`o3nISLe`dBs?rJ&1HTetGLjejczm5Phs=hILV*lrcqH@ir1sj8%mBX zjQG|E^OuOf5@UaJQ-6dsuhPP?pY(wB!nm&$)B5P{T?Gh>;o{_zfN81E9CU7q#iO%6 zX_FUeqS^}G0BIWiP$279Y+m?L_Ecu@Avm=1p?kXRuH7ie8}GP?FOYoY4YBa(_;)T8 zx%#3N3bb-(0(1($ca3m1kk=09O}`2wi1A^!`c@(xRWzli=F6!m<}n;-$aa0(gcj%2 zIeE>cIhjAshw_kfkOcRq0Ru}{vLA7n;%O&e0-&wnWOSt!fiB7q--6*qa&rs$YN2!O z&^Na-sxnWo=o;&6O4W|pvMv5>D~(iCgp0*+Hjn2=_VoSaPU*%pKY6p*VM3|vK)dMo zCX$W((8{bDZ!yu=u6s#sd6Y)uQ-{v}cq%*+zHdjsF!|@m-h_uPOROVue_uZWa*8^C z^ki>B7?bv(a{t@bC%D_UQ<8O%qE#qxTN&kAM8{zxbZ-`JV6o@P|MA zB`iH3&lz}Rpegm}JvW{+a5C_9AN=4)zUiC(^xyaw|D~^g@s+Q9wXZlEhC+<9=bo#D ztcAD$#1RZV^?BU<*vcPu^VPz>T)E!RNhf)XW?L|1T?E`*DBY*W7Z*Km*c=P}F%2HU zd*-P-H(xY0lOMJ_jN2fQ_SJ!~)pDq9@fC?WIrF1bE0=W0eyur8&K(*OS7%fcf&Oxb z4&;f7<;bE9!JG2(FGU$&1PDrkG+eOgluX3V!IF4C{(0??Gqu!O7 zWS=_d-&jY_$-{V(^vx_r#w47hmbr@tj{Y|I2OCn0`HhXdEY2KD{%&maVjDwz5b8K$ zt_l_(+HcU`jqv<1zNb7%TaHU|J@g=ljcnJ8UYR$??Hp<~7Zmn7V$)gXn!7sD#iu6p zoPu`F`7od5l@0+t=p>~~-6E?Uznsixy_u}8A>Z|UM%5T(a(K-|rN6KL;~Nm>CxkF3 zzYvY-_@&6>0HAmwgzXEmXKvaH66R*n6x(>32i18x9!*2AynbKe%+1#9ngcv8G4s8e zfUNS>WX^sHYJN*yz}mZkbFQZ+jMT}wj+d8%J*OH;SU1<4r(Pr`%Z<75^~c(bX5%qGc`c?R0Q35-Eiv@fTv|ek)4oknX^UR{ zhA2HTudn^@J9Ljf@?qu&lHp5GC#T?JZgW>b;l<|w&yBVgR<~!#SA9rFc|ADsrU-Ad z<4!yz#KicNgb6Nh7rRtyCT)UoX6kLy6BN;(!_d_+meT23m`yDoe>@}Snj(xxd}y+VT1kz6y5wiue@sqL{By)kE-H_fNi?1F}*L zR8n~{E-3v#>4Oy)`Q89)JY*}4pT((lWW`7x4Z2+eo#pC1SdE$I)s1YXODL)lIo7+)4~f^lzChn8dJaKryiOv@uP^j>L$B5X+!zJJK5MnDYtERF=@WDv z<9Vu*=-JflVxQ~=4XX0l^UQOEbH3}+a^rr;BUfpgx5RbNx*T^`zD6gn2giS99AkJ> zJ@r?^XY3#L72rrtwqp?G(7|%A$q)M*_zH$Azd(`}uc>!1B+h^RQu1kx{Wj6(KmYj` zzx%tN|6f1%xzBwU*UO9@r03If2Aprt$8!e$KNzd;j=d|IHI$0rZn4 zi~XtVG^i!*nmJQfTI?)JmKbX?hedRyYch7+;`*vsMbL$Tp5@R@h4dCg6?uvm7r8C? zGN8f8O!|~P`c7v3^Qo?fHo}OhAER$GBe*ng0%^*kp}llYVc*xp5JOkGD?ze?LCBuE z9OCL(OAR)A13|Hvo@rszTI>VUYQfMZl=7+wDDZY8gIqet9{A}^ZF;f#^E1QMj5Oon ztcf-%((2+TJU2#QmJeZxno=Mn8%Jn>pWP!{vRk7qt@hpFJTRqb=F0t$w5aZR?mo*THGXv8`NlVe8 zF<#D2J#T!#anAXMgV8nyVk#(pqHaw*Hg@~34{YFrl|~HhfG6)JsQev(x~^g#iBJedpG$~bldB_wC2c*Y4|o@b8zeTMND z`|>|7qG)czqTA#KjK?Q2Ctm$3hgYeyw)BT5_H{|u068q`q~nohh$!p zp8Fus7#*@c*`IVVI{rNfr)i72@$1L^b(CrNx-q|iH(m`o#!sGV{nRv?XwlOz=}x1si%~ux}XY<|shHZOmz>zh#G&NO?IdwstzLB6|vDEYpBVzAOR48f{ zg%>gWea43D1`vqZEe!ASwVzpJ4o`U1?|eBSZlHydppJQK^X;USOaSFYK_%%)9H2AoIqYzlcCXN91l^5VyV!1LeS|)0|VpM7RMpWHk5Wc zg**GH3Zk56kZHNh{QME5#$M)XxEgxE%*W&JLksUE~Z4QzKC;tMhcq<21 zYS!>9@-8yr0`Xth>QT9Hfb4LX!Chx{cCAfH6dsc*Imp^KKd;!k|LP}x^(#O0LqGH% zk@tg^@bk-a2Fk$Z&T}%JGw?@b;M>0K+rIf%KK}7feCxmdt$YjMORRbP7BF8^-iDJx zWtHLRm6s+HMe{Bo(pR8d_>_-OG4qqIcPr?(fLXL?AE9n?1h23U$4c4PIkgMDO|C0~ z9$4)4U_>?hw8uZrwBio0!d`hDzgOs1a52T;=i#gifOKuB%SzxP>lQ(iZq{_K#k!56 zPSJ0bJy^m*ayd}bo{ONgY(kqy${%6Hw2jT=l=aXLu|3CJk(ukiDm;xR9&N|U!+e>|@`l|!;Oupur-L=CLO}BKqv77GC?xc$iN}oG*sNGMW4hT$%md1nmuX-qR7&?`d|O=S{I|U;R$J z-ujcR>o@1FS0{rPJiotL zee~(~O%Y`TrLdN}g3$x1(5`PVedxZt%~kSu2+BGTuuD&4A1&dDGdcV#_|6GgK=XpX zgspspoKR619HIlMkfEKGV3yV*4+ewq`;Q;f*bo}RkqtO%=H z`XsJ)?|3R@+c~l}`sg5rFk~9_(?`I(ioN_zN8W0X^WJ)+|Kg};Pig<# z0bhqd_z>fVqhUpRq5azVVm(MXAQdTJn3In^mVE^C9lU!D7_~tqpB=rDANpfjt8IYQ zbDId={Htb3%w&3#l~TTV$&Rt=ZktqV4h!|Gw}b)fUCmu@tlD_G6SFa%xC`TJ1^h& z)(5`k{eSTTANXK5|BpWac8f9P&Hy??Pgsvxr%|A?+pmc0;k@ z(Rp%P*hbzfbc8FwVFJpAbH9ho5$9+AV*K%^`kK$8h4o<21dGtfBPk! zFvKS>iQt7kwxOA?EtSZ<$IkT;8LD$rz#X%Igs}RG&SXF+tTK^sve%BzKoH3|Z$6Y> z=ZuG7(nKF{eQ!0hcWB!8Sp4ATPZU+#hTXkUJgY9HAIvW3W$Uv~6y6z=Pmz_FeVv04aWxZlo5AqTpZ0A z#LT1kyf_z2!jUmwF`YZ|Zr3wo(71r67?kpBoZ435s1&(wh@qb~j*W>+%h<6)KlwwK z^*~eo0@8s@UA`|#A<9D?AbRe#EREq-=)BE;_E`Q93cL}}LfusD z8L#<(+9_Y@Mj0O!UD>rJY(6S+;<4GG?;N}=o_Y~%>FIpZmvda(b=3lslgP~j{qZ8t z0kn2;Y$)k=4|Z~VsyT zFZbHaru4-U#!wa9{K{37J@e=dMO%0Bj}`aIIDNEbD)jTdX$vH%N9S#Im0Z=hZgp zM#+$lNX1KI##4 zKDlIa3UKnal}ICjt%135d05cpD+2n|CcX5`&+;H;(hOX z|KIx9$3FJoCj0#IoPj?g19zVN5v*s@a|T|#_`TozzyIrB{(Hatb1&cVqXG&YA7MTf zDvOo%l-EoF(>__XiL8apYN*T_t7x~t%Hoh^p;PoO#uDsDQ^->r-wnEAhSAN;q zqVPIYekrR5q*IgbPCRcTVt;tDazc>N`Yxic1ti_vd*=C`znUVSF`Y*c5$+H2AfzvC5TGTA@2dJTRJA$e)9@>^ zHawLgSjI$`r!m08zPkOtaq^>{Tu*MQ_Xh*T;m4DHaDMn~0BIo4nB)frol9Lqm#>23 zb#*Tt8`|0?YDkDclj@<~&f@Lsrt5Fhuf#x&(>Ez@KY!aOp{ES_Z_{hsV)8Uzn;u>H z_RW9h!2+b1yZKRSN+S6yM7?44kOlqDJ7ZPaq1;y_ri*?a77L_n-F#^~?({a7vF9I{ z(+|0HHMQj@{qL?r+A3u!_L~pe>-pUDWv{AWX2Og-!@K2D-*61<#+h? z;OZLPHnFl8EB)QLYia&m+q*U)LNF(npU);7LLB)iDWoK)bP#CF{dNO%bYwgHtIvz~ zUOz%y7=0I`@({+b6li{dQ>_x{4U9Rbwi(|i=LuJ*Io*_D%d6g5dvGMvRNss)Y zludO4P{5c4^Ji~f2BTK+e)kO~!B!9P1BIS`OMn)BNGqF&T|ewYe;g+cY-I)T&3kjh zxMytNP)kc-zh$KbhX^9WY;?4-aq8_%&_7D0gcoJ?tKW^*mVV?Tf)j!UpS6GAP#JFa zpdSsoN@*Bb^-;fbLd<<@1{pi>su9x&KVru5Nz?ivocXlJPdBpaT>gyIZ!MRnAR4|0 zG5}r)@+?DseWP)d);e;>Qcx1;_0jxke=E$5BKnkpp=pA3v+$VeO$ zoty+mp*}dqEL>^`o2_PKZgzEF75o{Vv6n`rwGV>&U6<+uo+WEBt=2{rq|1o8JZVOP zu2tC-i0FCOfhhizlL8dLej;d>v~X7s(8!Q{JusgF_(PB83Ao&b=8I$2c-|N|8%%5I za$|xO^;z8)i%$6?i0TQ1*+lg^nVj*EUD_K{r33;3yZMFY@BhIUUVQemzx4|r`N&89 zE7{M-a|Zsn44hK%$FZO3Z+ZqOfUmy)`@jF+{n?-WKfm-gUn}yHRg2XYX0ubN;%q6h z;7Oxpm9p<5QDt<&v+QwLD0y^vs&CPII=UHvU_rh$eTcItf)zX-ausYBJsy_W3aNno zLZG5{?vuTcV04w$mkOw>2$(Cb^Tqn>Cagd9L~wr9ZA-$@4_Asoi2t_bwQFiraC29N73SGubmS4vFNr>K&mi>~apN)W^_6Jofr02xkjm1* zb~aTUd-jK*5iF&9@LtJu9`u($;3Sp=ZHkY5X}cF#Y_mE{wbSnvg$N=IS#G3qb8IP#C?85p3SaH- zdHg?$y-DyI22OT3o!D-Zj_Mif(W; ztPu}&k@KWGi%K#%M`lkLi7Ts+xyH8L>pB!^0WoL&YQJdrtfKQivzez|k^5 z2kCKhA(H-J>zeEA*s5hmd~vkVg=IW&8$ zKhbo25YLpgSJjE`RU8QV?wkxWd*;M0*IWu3m~0GrHq=qS6W@vGh6y?P{48(Al&=sv zrf`3SM!D5dYo&HP1a$0`J@G?ZJvI%T)DQu9%sJ%dF1*HtzO_O+VAltI^;dL% z{^x$-i$C~-KlpF)AHCd`JRi>)_+vBR?0Y_*Gw}bLfj{{tzwys~`76Kwv)}kl|LTjc zewkk$r0{rSVWD&L@KzsbS8l0G5T$cioLXOQv4uCb6%Qep`z!KJ#pzqTD~=p4A+ShPA1 zD1%216*m%u1MDz^f~~%}tj|j3;_pp|e}!DC3vUY?GS~0oP_e`FHt^!RK$Smr4eVfM z;k5zCE|NpRi2kXgS*OU%(*UEv65Q@K#oe^{siO8&^2Uz#_3FhdlH^wz(nr_BoYCCe zxp?XajDT`mY6YqnJ`vf;X>IDBa*+CD(@m%DB}9v}LYzkNb@I#9#@NpZ^>`0uMQ{wJ zpO!E-Je-fd+11!YxEgfS1IbOR50u2!t9h|z(8lYadLx6MK5p)enRd4^+BFRMTd9@=57$vJ34s` zuaJ;y{5CAik*Su36g*YjBJaj2^U-=k%#OC@X8)BK%%L`B9|=WtAL_>W_-bprRKnG| zx`ysad8^Zul*DbuTkl^3L(XqslXZPAJkpdBi zc{}6PF0&WS7gCQS+AeMj zbRavbbx4HRp7}LL&Unoaw)Fv8PLYDtG$L6K;oXR8K3s;Ae1u;MO-18 z=V&l(7gGR2Unh}kzo0K_KLCh@PV*r_qIR^oN=9I+r{0?jUdwYn@?#I7?Q*R=w(dBv z5#(ZSJU4+$-!=1SYKs7{u%7q2!-bbGU%q(rjW=F=@Pi+E@%3N->%Z=={?)(wgTg)^ z&l&iqWuRwH|1^PT-g5@bz$ZTO@gHN6{oH%s^CiFIOW*sYY&5umYJ%kCB*a9u%>zwH zO}eZgE(Xk=AZ)^Hd2r5h5|6vZqe*f~ThP&Qu2Xdj)Xb?BUD=qKhUg@%y(c|4F9DrS8*Uj-3Zr98qZ-2TyhN`K&q+X+dAxGGr^c)2f+0>DoiM4T_R7IGY#Z&)>5M z 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 { + it('returns true for hrefs ending with /', () => { + expect(isDirectoryHref('subdir/')).toBe(true) + expect(isDirectoryHref('/')).toBe(true) + expect(isDirectoryHref('deep/nested/')).toBe(true) + }) + + it('returns false for hrefs with file extensions', () => { + expect(isDirectoryHref('file.txt')).toBe(false) + expect(isDirectoryHref('archive.tar.gz')).toBe(false) + expect(isDirectoryHref('')).toBe(false) + }) + + it('returns true for hrefs without extension (directory heuristic)', () => { + expect(isDirectoryHref('subdir')).toBe(true) + expect(isDirectoryHref('/docs')).toBe(true) + expect(isDirectoryHref('/electron-app')).toBe(true) + }) +}) + +// --------------------------------------------------------------------------- +// parseDirectoryListing +// --------------------------------------------------------------------------- + +/** Minimal "Index of" HTML fixture */ +const SAMPLE_HTML = ` + + +Index of /files/ + +

Index of /files/

+ + + + + + + + + + + + + + +
NameSize
Name
Parent Directory
Parent Directory (dotdot)
Current Directory
Dot
Root
subdir/
file.txt
archive.tar.gz
Out of scope
another.mp4 (absolute in scope)
file.txt view link
+ + +` + +describe('parseDirectoryListing', () => { + const pageUrl = 'https://example.com/files/' + + it('extracts in-scope file and directory links', () => { + const results = parseDirectoryListing(SAMPLE_HTML, pageUrl) + const urls = results.map((r) => r.url) + + expect(urls).toContain('https://example.com/files/subdir/') + expect(urls).toContain('https://example.com/files/file.txt') + expect(urls).toContain('https://example.com/files/archive.tar.gz') + expect(urls).toContain('https://example.com/files/another.mp4') + }) + + it('marks directories (trailing-slash hrefs) as isDirectory = true', () => { + const results = parseDirectoryListing(SAMPLE_HTML, pageUrl) + const subdir = results.find((r) => r.url === 'https://example.com/files/subdir/') + expect(subdir).toBeDefined() + expect(subdir!.isDirectory).toBe(true) + }) + + it('marks files (no trailing slash) as isDirectory = false', () => { + const results = parseDirectoryListing(SAMPLE_HTML, pageUrl) + const file = results.find((r) => r.url === 'https://example.com/files/file.txt') + expect(file).toBeDefined() + expect(file!.isDirectory).toBe(false) + }) + + it('skips .. / dotdot links', () => { + const results = parseDirectoryListing(SAMPLE_HTML, pageUrl) + const urls = results.map((r) => r.url) + // None of the parent-directory links should appear + for (const url of urls) { + expect(url).not.toContain('/files/../') + } + // Also href="../" should be skipped — it resolves to the parent, which is out of scope anyway, + // but we want to confirm it doesn't slip through the filter + expect(urls).not.toContain('https://example.com/') + }) + + it('skips links beginning with ? and links with query strings', () => { + const results = parseDirectoryListing(SAMPLE_HTML, pageUrl) + // ?C=N&O=D is a sort query parameter link common in Apache listings + const sortLink = results.find((r) => r.url.includes('C=N')) + expect(sortLink).toBeUndefined() + // file.txt?view should also be filtered + const viewLink = results.find((r) => r.url.includes('?view')) + expect(viewLink).toBeUndefined() + }) + + it('skips the root / link', () => { + const results = parseDirectoryListing(SAMPLE_HTML, pageUrl) + const rootLink = results.find((r) => r.url === 'https://example.com/') + expect(rootLink).toBeUndefined() + }) + + it('skips . and ./ current-directory links', () => { + const results = parseDirectoryListing(SAMPLE_HTML, pageUrl) + // "." resolves to the page URL itself; "./" also resolves to the page URL + // The page URL is the base — it is NOT a child, so isUrlInScope should reject it + // (or the explicit "." / "./" filter catches it first) + const selfLink = results.find((r) => r.url === pageUrl) + expect(selfLink).toBeUndefined() + }) + + it('skips out-of-scope links', () => { + const results = parseDirectoryListing(SAMPLE_HTML, pageUrl) + const outOfScope = results.find((r) => r.url.includes('other.com')) + expect(outOfScope).toBeUndefined() + }) + + it('deduplicates identical links', () => { + const html = ` +
link1 + link2 + ` + const results = parseDirectoryListing(html, pageUrl) + const matches = results.filter((r) => r.url === 'https://example.com/files/file.txt') + expect(matches).toHaveLength(1) + }) + + it('returns an empty array for a page with no links', () => { + const results = parseDirectoryListing('No links here', pageUrl) + expect(results).toHaveLength(0) + }) + + it('handles absolute in-scope links correctly', () => { + const results = parseDirectoryListing(SAMPLE_HTML, pageUrl) + const abs = results.find((r) => r.url === 'https://example.com/files/another.mp4') + expect(abs).toBeDefined() + expect(abs!.isDirectory).toBe(false) + }) +}) diff --git a/electron-app/src/main/__tests__/utils.test.ts b/electron-app/src/main/__tests__/utils.test.ts new file mode 100644 index 0000000..e605806 --- /dev/null +++ b/electron-app/src/main/__tests__/utils.test.ts @@ -0,0 +1,155 @@ +import { describe, it, expect } from 'vitest' +import { join } from 'path' +import { homedir } from 'os' +import { + sanitizePathSegment, + sanitizeFilename, + isUrlInScope, + safeJoin, + defaultDownloadFolder, + normalizeExtension, +} from '../utils' + +describe('sanitizePathSegment', () => { + it('removes path traversal sequences', () => { + expect(sanitizePathSegment('../etc/passwd')).not.toContain('..') + expect(sanitizePathSegment('../../secret')).not.toContain('..') + }) + + it('removes forward slashes', () => { + expect(sanitizePathSegment('foo/bar')).toBe('foobar') + }) + + it('removes backslashes', () => { + expect(sanitizePathSegment('foo\\bar')).toBe('foobar') + }) + + it('removes Windows-illegal characters', () => { + expect(sanitizePathSegment('file.txt')).not.toMatch(/[<>:"|?*]/) + expect(sanitizePathSegment('file:name')).not.toContain(':') + expect(sanitizePathSegment('file|name')).not.toContain('|') + expect(sanitizePathSegment('file?name')).not.toContain('?') + expect(sanitizePathSegment('file*name')).not.toContain('*') + }) + + it('removes control characters', () => { + expect(sanitizePathSegment('file\x00name')).not.toContain('\x00') + expect(sanitizePathSegment('file\x1fname')).not.toContain('\x1f') + }) + + it('passes through normal filenames unchanged', () => { + expect(sanitizePathSegment('hello-world_2024.tar.gz')).toBe('hello-world_2024.tar.gz') + expect(sanitizePathSegment('normalfile.txt')).toBe('normalfile.txt') + }) +}) + +describe('sanitizeFilename', () => { + it('is an alias for sanitizePathSegment', () => { + expect(sanitizeFilename('../foo/bar')).toBe(sanitizePathSegment('../foo/bar')) + expect(sanitizeFilename('normal.txt')).toBe('normal.txt') + }) +}) + +describe('isUrlInScope', () => { + it('returns true when candidate is under base path', () => { + expect(isUrlInScope('https://example.com/files/', 'https://example.com/files/foo.txt')).toBe(true) + expect(isUrlInScope('https://example.com/files/', 'https://example.com/files/sub/bar.txt')).toBe(true) + }) + + it('returns true when base URL has no trailing slash', () => { + expect(isUrlInScope('https://example.com/files', 'https://example.com/files/foo.txt')).toBe(true) + }) + + it('returns false when candidate is at a sibling path', () => { + expect(isUrlInScope('https://example.com/files/', 'https://example.com/other/foo.txt')).toBe(false) + }) + + it('returns false when origins differ', () => { + expect(isUrlInScope('https://example.com/files/', 'https://evil.com/files/foo.txt')).toBe(false) + }) + + it('returns false when protocol differs', () => { + expect(isUrlInScope('https://example.com/files/', 'http://example.com/files/foo.txt')).toBe(false) + }) + + it('returns false for invalid URLs', () => { + expect(isUrlInScope('not-a-url', 'https://example.com/files/')).toBe(false) + expect(isUrlInScope('https://example.com/', 'not-a-url')).toBe(false) + }) + + it('returns false when candidate is a parent of base', () => { + expect(isUrlInScope('https://example.com/files/sub/', 'https://example.com/files/')).toBe(false) + }) +}) + +describe('safeJoin', () => { + it('returns the resolved path for safe inputs', () => { + const root = '/tmp/downloads' + expect(safeJoin(root, ['subdir', 'file.txt'])).toBe('/tmp/downloads/subdir/file.txt') + }) + + it('throws on path traversal attempt', () => { + const root = '/tmp/downloads' + expect(() => safeJoin(root, ['..', 'etc', 'passwd'])).toThrow() + expect(() => safeJoin(root, ['subdir', '..', '..', 'etc'])).toThrow() + }) + + it('handles empty parts array', () => { + const root = '/tmp/downloads' + expect(safeJoin(root, [])).toBe('/tmp/downloads') + }) + + it('handles a single safe part', () => { + const root = '/tmp/downloads' + expect(safeJoin(root, ['myfile.zip'])).toBe('/tmp/downloads/myfile.zip') + }) +}) + +describe('defaultDownloadFolder', () => { + it('derives folder from URL hostname', () => { + const result = defaultDownloadFolder('https://example.com/files/') + expect(result).toBe(join(homedir(), 'Downloads', 'example.com')) + }) + + it('handles URLs without trailing slash', () => { + const result = defaultDownloadFolder('https://example.com') + expect(result).toBe(join(homedir(), 'Downloads', 'example.com')) + }) + + it('uses subdomain in folder name', () => { + const result = defaultDownloadFolder('https://files.example.com/data/') + expect(result).toBe(join(homedir(), 'Downloads', 'files.example.com')) + }) + + it('falls back gracefully on invalid URL', () => { + const result = defaultDownloadFolder('not-a-url') + expect(result).toBe(join(homedir(), 'Downloads', 'index-ripper')) + }) +}) + +describe('normalizeExtension', () => { + it('returns lowercase extension with dot', () => { + expect(normalizeExtension('file.TXT')).toBe('.txt') + expect(normalizeExtension('archive.TAR.GZ')).toBe('.gz') + expect(normalizeExtension('image.PNG')).toBe('.png') + }) + + it('returns empty string for files with no extension', () => { + expect(normalizeExtension('Makefile')).toBe('') + expect(normalizeExtension('README')).toBe('') + }) + + it('returns the extension as-is when already lowercase', () => { + expect(normalizeExtension('script.js')).toBe('.js') + expect(normalizeExtension('data.json')).toBe('.json') + }) + + it('handles dotfiles', () => { + // .gitignore has no extension — extname returns '' + expect(normalizeExtension('.gitignore')).toBe('') + }) + + it('handles filenames with multiple dots', () => { + expect(normalizeExtension('my.archive.tar.gz')).toBe('.gz') + }) +}) diff --git a/electron-app/src/main/downloader.ts b/electron-app/src/main/downloader.ts new file mode 100644 index 0000000..c1e9af5 --- /dev/null +++ b/electron-app/src/main/downloader.ts @@ -0,0 +1,244 @@ +import * as http from 'http' +import * as https from 'https' +import { createWriteStream, mkdirSync, unlinkSync, existsSync } from 'fs' +import { dirname } from 'path' +import { EventEmitter } from 'events' +import type { DownloadItem } from '../shared/types' + +type PQueueType = import('p-queue').default + +interface DownloadCallbacks { + onProgress?: (fileId: string, percent: number, speed: number) => void + onStatus?: (fileId: string, status: DownloadItem['status']) => void + onLog?: (message: string) => void + onFinished?: (completed: number, total: number) => void +} + +interface DownloadFile { + id: string + url: string + destPath: string + fileName: string +} + +const MAX_CONCURRENCY = 10 +const MIN_CONCURRENCY = 1 +const MAX_ATTEMPTS = 3 +const BASE_BACKOFF_MS = 1000 +const PROGRESS_INTERVAL_MS = 500 + +export class Downloader extends EventEmitter { + private queue: PQueueType | null = null + private queueReady: Promise + private concurrency: number + private controllers = new Map() + private _paused = false + private _resumeResolvers: Array<() => void> = [] + + constructor(concurrency = 5) { + super() + this.concurrency = Math.min(MAX_CONCURRENCY, Math.max(MIN_CONCURRENCY, concurrency)) + this.queueReady = this._initQueue() + } + + private async _initQueue(): Promise { + const { default: PQueue } = await import('p-queue') + this.queue = new PQueue({ concurrency: this.concurrency }) + } + + private async getQueue(): Promise { + await this.queueReady + return this.queue! + } + + async setConcurrency(n: number): Promise { + this.concurrency = Math.min(MAX_CONCURRENCY, Math.max(MIN_CONCURRENCY, n)) + const q = await this.getQueue() + q.concurrency = this.concurrency + } + + pause(): void { + if (this._paused) return + this._paused = true + this.getQueue().then((q) => q.pause()).catch(() => {}) + } + + resume(): void { + if (!this._paused) return + this._paused = false + for (const resolve of this._resumeResolvers) resolve() + this._resumeResolvers = [] + this.getQueue().then((q) => q.start()).catch(() => {}) + } + + private waitIfPaused(): Promise { + if (!this._paused) return Promise.resolve() + return new Promise((resolve) => { + this._resumeResolvers.push(resolve) + }) + } + + cancelFile(fileId: string): void { + const ctrl = this.controllers.get(fileId) + if (ctrl) { + ctrl.abort() + this.controllers.delete(fileId) + } + } + + async cancelAll(): Promise { + const q = await this.getQueue() + q.clear() + for (const ctrl of this.controllers.values()) { + ctrl.abort() + } + this.controllers.clear() + } + + async downloadFiles(files: DownloadFile[], callbacks: DownloadCallbacks): Promise { + const q = await this.getQueue() + let completed = 0 + const total = files.length + + const tasks = files.map((file) => + q.add(async () => { + await this.waitIfPaused() + await this.downloadOne(file, callbacks, 1) + completed++ + }) + ) + + await Promise.allSettled(tasks) + callbacks.onFinished?.(completed, total) + } + + async downloadOne(file: DownloadFile, callbacks: DownloadCallbacks, attempt: number): Promise { + const { id, url, destPath, fileName } = file + + callbacks.onStatus?.(id, 'downloading') + callbacks.onLog?.(`[${fileName}] Starting download (attempt ${attempt}/${MAX_ATTEMPTS})`) + + mkdirSync(dirname(destPath), { recursive: true }) + + const controller = new AbortController() + this.controllers.set(id, controller) + + return new Promise((resolve, reject) => { + const isHttps = url.startsWith('https') + const client = isHttps ? https : http + + const req = client.get(url, { signal: controller.signal }, (res) => { + const statusCode = res.statusCode ?? 0 + + // Follow redirects + if (statusCode >= 300 && statusCode < 400 && res.headers.location) { + res.resume() + this.controllers.delete(id) + const redirectedFile = { ...file, url: new URL(res.headers.location, url).href } + this.downloadOne(redirectedFile, callbacks, attempt).then(resolve).catch(reject) + return + } + + // Retry on 5xx + if (statusCode >= 500) { + res.resume() + this.controllers.delete(id) + if (attempt < MAX_ATTEMPTS) { + const backoff = BASE_BACKOFF_MS * Math.pow(2, attempt - 1) + callbacks.onLog?.(`[${fileName}] Server error ${statusCode}, retrying in ${backoff}ms`) + setTimeout(() => { + this.downloadOne(file, callbacks, attempt + 1).then(resolve).catch(reject) + }, backoff) + } else { + callbacks.onStatus?.(id, 'failed') + callbacks.onLog?.(`[${fileName}] Failed after ${MAX_ATTEMPTS} attempts (HTTP ${statusCode})`) + reject(new Error(`HTTP ${statusCode}`)) + } + return + } + + if (statusCode < 200 || statusCode >= 300) { + res.resume() + this.controllers.delete(id) + callbacks.onStatus?.(id, 'failed') + callbacks.onLog?.(`[${fileName}] HTTP error ${statusCode}`) + reject(new Error(`HTTP ${statusCode}`)) + return + } + + const totalSize = parseInt(res.headers['content-length'] ?? '0', 10) || 0 + const writeStream = createWriteStream(destPath) + let bytesDownloaded = 0 + let lastProgressTime = Date.now() + let lastProgressBytes = 0 + + const progressTimer = setInterval(() => { + const now = Date.now() + const elapsed = (now - lastProgressTime) / 1000 + const speed = elapsed > 0 ? (bytesDownloaded - lastProgressBytes) / elapsed : 0 + lastProgressTime = now + lastProgressBytes = bytesDownloaded + const percent = totalSize > 0 ? Math.min(100, (bytesDownloaded / totalSize) * 100) : 0 + callbacks.onProgress?.(id, percent, speed) + }, PROGRESS_INTERVAL_MS) + + const cleanup = (deleteFile: boolean): void => { + clearInterval(progressTimer) + writeStream.destroy() + this.controllers.delete(id) + if (deleteFile && existsSync(destPath)) { + try { unlinkSync(destPath) } catch { /* ignore */ } + } + } + + res.on('data', (chunk: Buffer) => { + bytesDownloaded += chunk.length + writeStream.write(chunk) + }) + + res.on('end', () => { + cleanup(false) + callbacks.onProgress?.(id, 100, 0) + callbacks.onStatus?.(id, 'completed') + callbacks.onLog?.(`[${fileName}] Complete (${bytesDownloaded} bytes)`) + resolve() + }) + + res.on('error', (err) => { + cleanup(true) + callbacks.onStatus?.(id, 'failed') + callbacks.onLog?.(`[${fileName}] Error: ${err.message}`) + reject(err) + }) + + writeStream.on('error', (err) => { + cleanup(true) + callbacks.onStatus?.(id, 'failed') + callbacks.onLog?.(`[${fileName}] Write error: ${err.message}`) + reject(err) + }) + }) + + req.on('error', (err) => { + if (controller.signal.aborted) { + callbacks.onStatus?.(id, 'cancelled') + callbacks.onLog?.(`[${fileName}] Cancelled`) + resolve() + return + } + this.controllers.delete(id) + if (attempt < MAX_ATTEMPTS) { + const backoff = BASE_BACKOFF_MS * Math.pow(2, attempt - 1) + callbacks.onLog?.(`[${fileName}] Network error, retrying in ${backoff}ms: ${err.message}`) + setTimeout(() => { + this.downloadOne(file, callbacks, attempt + 1).then(resolve).catch(reject) + }, backoff) + } else { + callbacks.onStatus?.(id, 'failed') + callbacks.onLog?.(`[${fileName}] Failed: ${err.message}`) + reject(err) + } + }) + }) + } +} diff --git a/electron-app/src/main/index.ts b/electron-app/src/main/index.ts new file mode 100644 index 0000000..dd3775f --- /dev/null +++ b/electron-app/src/main/index.ts @@ -0,0 +1,58 @@ +import { app, shell, BrowserWindow } from 'electron' +import { join } from 'path' +import { electronApp, optimizer, is } from '@electron-toolkit/utils' +import icon from '../../resources/icon.png?asset' +import { TaskQueue } from './task-queue' +import { registerIpcHandlers } from './ipc' + +const taskQueue = new TaskQueue() + +function createWindow(): BrowserWindow { + const mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + show: false, + autoHideMenuBar: true, + ...(process.platform === 'linux' ? { icon } : {}), + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + sandbox: false + } + }) + + mainWindow.on('ready-to-show', () => { + mainWindow.show() + }) + + mainWindow.webContents.setWindowOpenHandler((details) => { + shell.openExternal(details.url) + return { action: 'deny' } + }) + + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { + mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) + } else { + mainWindow.loadFile(join(__dirname, '../renderer/index.html')) + } + + return mainWindow +} + +app.whenReady().then(() => { + electronApp.setAppUserModelId('com.index-ripper') + + app.on('browser-window-created', (_, window) => { + optimizer.watchWindowShortcuts(window) + }) + + const mainWindow = createWindow() + registerIpcHandlers(taskQueue, mainWindow) + + app.on('activate', function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +app.on('window-all-closed', () => { + app.quit() +}) diff --git a/electron-app/src/main/ipc.ts b/electron-app/src/main/ipc.ts new file mode 100644 index 0000000..f760dad --- /dev/null +++ b/electron-app/src/main/ipc.ts @@ -0,0 +1,242 @@ +import { ipcMain, dialog, BrowserWindow } from 'electron' +import type { TaskQueue } from './task-queue' + +// electron-store v10 is ESM-only; use dynamic import for CJS compat. +interface StoreInstance { + store: Record + get(key: string): unknown + set(key: string, value: unknown): void +} + +let storeInstance: StoreInstance | null = null + +async function getStore(): Promise { + if (storeInstance) return storeInstance + const { default: Store } = await import('electron-store') + storeInstance = new Store() as unknown as StoreInstance + return storeInstance +} + +export function registerIpcHandlers( + taskQueue: TaskQueue, + mainWindow: BrowserWindow +): void { + const send = (channel: string, ...args: unknown[]): void => { + if (!mainWindow.isDestroyed()) { + mainWindow.webContents.send(channel, ...args) + } + } + + // ------------------------------------------------------------------------- + // Task management + // ------------------------------------------------------------------------- + + ipcMain.handle('task:create', (_event, url: string) => { + const task = taskQueue.createTask(url) + return task + }) + + ipcMain.handle('task:remove', (_event, taskId: string) => { + taskQueue.removeTask(taskId) + }) + + // ------------------------------------------------------------------------- + // Scan + // ------------------------------------------------------------------------- + + ipcMain.handle('scan:start', (_event, taskId: string, url: string) => { + const entry = taskQueue.getTask(taskId) + if (!entry) throw new Error(`Unknown task: ${taskId}`) + + entry.task.status = 'scanning' + + // Fire-and-forget: don't await — let scan run in background + // Results are streamed via IPC events + entry.scanner.scan(url, { + onItem: (node, parentId) => { + send('scan:item', taskId, node, parentId) + }, + onProgress: (scanned, total) => { + entry.task.scanProgress = { scanned, total } + send('scan:progress', taskId, scanned, total) + }, + onError: (errUrl, err) => { + send('scan:error', taskId, errUrl, err.message) + }, + onFinished: () => { + if (entry.task.status === 'scanning') { + entry.task.status = 'scanned' + } + send('scan:finished', taskId) + } + }).catch((err) => { + send('scan:error', taskId, url, String(err)) + send('scan:finished', taskId) + }) + }) + + ipcMain.handle('scan:stop', (_event, taskId: string) => { + const entry = taskQueue.getTask(taskId) + if (!entry) throw new Error(`Unknown task: ${taskId}`) + entry.scanner.abort() + entry.task.status = 'cancelled' + }) + + ipcMain.handle('scan:pause', (_event, taskId: string) => { + const entry = taskQueue.getTask(taskId) + if (!entry) throw new Error(`Unknown task: ${taskId}`) + entry.scanner.pause() + }) + + ipcMain.handle('scan:resume', (_event, taskId: string) => { + const entry = taskQueue.getTask(taskId) + if (!entry) throw new Error(`Unknown task: ${taskId}`) + entry.scanner.resume() + }) + + // ------------------------------------------------------------------------- + // Download + // ------------------------------------------------------------------------- + + ipcMain.handle( + 'download:start', + async ( + _event, + taskId: string, + files: Array<{ id: string; url: string; destPath: string; fileName: string }>, + _destPath: string + ) => { + const entry = taskQueue.getTask(taskId) + if (!entry) throw new Error(`Unknown task: ${taskId}`) + + entry.task.status = 'downloading' + + await entry.downloader.downloadFiles(files, { + onProgress: (fileId, percent, speed) => { + send('download:progress', taskId, fileId, percent, speed) + }, + onStatus: (fileId, status) => { + send('download:status', taskId, fileId, status) + }, + onLog: (message) => { + send('log:message', taskId, message) + }, + onFinished: (completed, total) => { + entry.task.status = 'done' + send('download:finished', taskId, completed, total) + } + }) + } + ) + + ipcMain.handle('download:pause', (_event, taskId: string) => { + const entry = taskQueue.getTask(taskId) + if (!entry) throw new Error(`Unknown task: ${taskId}`) + entry.downloader.pause() + }) + + ipcMain.handle('download:resume', (_event, taskId: string) => { + const entry = taskQueue.getTask(taskId) + if (!entry) throw new Error(`Unknown task: ${taskId}`) + entry.downloader.resume() + }) + + ipcMain.handle('download:cancel', (_event, taskId: string, fileId: string) => { + const entry = taskQueue.getTask(taskId) + if (!entry) throw new Error(`Unknown task: ${taskId}`) + entry.downloader.cancelFile(fileId) + }) + + ipcMain.handle('download:cancelAll', (_event, taskId: string) => { + const entry = taskQueue.getTask(taskId) + if (!entry) throw new Error(`Unknown task: ${taskId}`) + entry.downloader.cancelAll() + }) + + ipcMain.handle('download:retry', async (_event, taskId: string, fileId: string) => { + const entry = taskQueue.getTask(taskId) + if (!entry) throw new Error(`Unknown task: ${taskId}`) + + // Find the download item info from the task + const downloadItem = entry.task.downloads[fileId] + if (!downloadItem) throw new Error(`Unknown download: ${fileId}`) + + await entry.downloader.downloadOne( + { + id: downloadItem.id, + url: downloadItem.url, + destPath: downloadItem.destPath, + fileName: downloadItem.fileName + }, + { + onProgress: (fId, percent, speed) => { + send('download:progress', taskId, fId, percent, speed) + }, + onStatus: (fId, status) => { + send('download:status', taskId, fId, status) + }, + onLog: (message) => { + send('log:message', taskId, message) + } + }, + 1 + ) + }) + + ipcMain.handle('download:setWorkers', async (_event, count: number) => { + for (const entry of taskQueue.tasks.values()) { + await entry.downloader.setConcurrency(count) + } + }) + + // ------------------------------------------------------------------------- + // Settings (electron-store) + // ------------------------------------------------------------------------- + + ipcMain.handle('settings:get', async () => { + const store = await getStore() + return store.store + }) + + ipcMain.handle('settings:set', async (_event, data: Record) => { + const store = await getStore() + for (const [key, value] of Object.entries(data)) { + store.set(key, value) + } + }) + + // ------------------------------------------------------------------------- + // Dialog + // ------------------------------------------------------------------------- + + ipcMain.handle('dialog:selectFolder', async () => { + const result = await dialog.showOpenDialog(mainWindow, { + properties: ['openDirectory'] + }) + if (result.canceled || result.filePaths.length === 0) { + return null + } + return result.filePaths[0] + }) + + // ------------------------------------------------------------------------- + // Preview — fetch file content from remote URL via main process + // ------------------------------------------------------------------------- + + ipcMain.handle('preview:fetch', async (_event, url: string) => { + const response = await globalThis.fetch(url) + if (!response.ok) throw new Error(`HTTP ${response.status}`) + + const contentType = response.headers.get('content-type') ?? '' + const buffer = Buffer.from(await response.arrayBuffer()) + const base64 = buffer.toString('base64') + return { dataUrl: `data:${contentType};base64,${base64}`, contentType } + }) + + ipcMain.handle('preview:fetchText', async (_event, url: string) => { + const response = await globalThis.fetch(url) + if (!response.ok) throw new Error(`HTTP ${response.status}`) + const text = await response.text() + return text.slice(0, 200 * 1024) // 200KB limit + }) +} diff --git a/electron-app/src/main/scanner.ts b/electron-app/src/main/scanner.ts new file mode 100644 index 0000000..7415d50 --- /dev/null +++ b/electron-app/src/main/scanner.ts @@ -0,0 +1,384 @@ +import * as cheerio from 'cheerio' +import { isUrlInScope } from './utils' +import { getIconGroup } from '../shared/icons' +import type { TreeNode } from '../shared/types' + +// --------------------------------------------------------------------------- +// Pure helpers (testable without Electron) +// --------------------------------------------------------------------------- + +export interface ParsedLink { + url: string + isDirectory: boolean +} + +/** + * Returns true if the href likely represents a directory. + * Heuristic: ends with '/' OR has no file extension (no '.' in the last segment). + */ +export function isDirectoryHref(href: string): boolean { + if (!href) return false + if (href.endsWith('/')) return true + // No extension = likely a directory (e.g., "/docs", "/electron-app") + const lastSegment = href.split('/').filter(Boolean).pop() ?? '' + if (!lastSegment) return false + return !lastSegment.includes('.') +} + +/** + * Parse an "Index of" HTML page and return links that are in scope. + * + * Filters out: + * - `.` (current directory) + * - `..` and links starting with `..` + * - `/` (root) + * - Links beginning with `?` (sort query params) + * - Links that are out of scope relative to pageUrl + */ +export function parseDirectoryListing(html: string, pageUrl: string): ParsedLink[] { + const $ = cheerio.load(html) + const results: ParsedLink[] = [] + const seen = new Set() + + $('a[href]').each((_i, el) => { + const href = $(el).attr('href') ?? '' + + // Skip navigation / query links + if (href === '.' || href === './' || href === '..' || href === '../') return + if (href.startsWith('..')) return + if (href === '/') return + if (href.startsWith('?')) return + if (href === '') return + + // Resolve to absolute URL + let absoluteUrl: string + try { + const parsed = new URL(href, pageUrl) + // Skip links with query strings (e.g., "?view", "?C=N;O=D") + if (parsed.search) return + absoluteUrl = `${parsed.protocol}//${parsed.host}${parsed.pathname}` + } catch { + return + } + + // Scope check + if (!isUrlInScope(pageUrl, absoluteUrl)) return + + // Deduplicate + if (seen.has(absoluteUrl)) return + seen.add(absoluteUrl) + + results.push({ url: absoluteUrl, isDirectory: isDirectoryHref(href) }) + }) + + return results +} + +// --------------------------------------------------------------------------- +// Scanner callbacks interface +// --------------------------------------------------------------------------- + +export interface ScanCallbacks { + onItem: (node: TreeNode, parentId: string) => void + onProgress: (scanned: number, total: number) => void + onError: (url: string, err: Error) => void + onFinished: () => void +} + +// --------------------------------------------------------------------------- +// Scanner class +// --------------------------------------------------------------------------- + +/** + * A fetch function compatible with the native `fetch` API (subset). + * Accepts a URL string and an optional init, returns a Response-like object. + */ +export type FetchFn = ( + url: string, + init?: { method?: string; signal?: AbortSignal } +) => Promise<{ + ok: boolean + status: number + text(): Promise + headers: { get(name: string): string | null } +}> + +/** Generate a simple deterministic node ID from a URL. */ +function urlToId(url: string): string { + // Use a simple hash-like approach; collision risk is negligible for a scan session + return url.replace(/[^a-zA-Z0-9]/g, '_') +} + +/** Format a byte count as a human-readable string. */ +function formatSize(bytes: number | null): string { + if (bytes === null || bytes < 0) return 'Unknown' + if (bytes === 0) return '0 B' + const units = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(1024)) + const val = bytes / Math.pow(1024, i) + return `${val.toFixed(i === 0 ? 0 : 1)} ${units[i]}` +} + +/** Extract filename from a URL path. */ +function fileNameFromUrl(url: string): string { + try { + const pathname = new URL(url).pathname + const parts = pathname.replace(/\/$/, '').split('/') + return decodeURIComponent(parts[parts.length - 1] || '') + } catch { + return url + } +} + +/** Derive the relative path of a URL under a base URL. */ +function relativePathFromBase(baseUrl: string, targetUrl: string): string { + try { + const base = new URL(baseUrl) + const target = new URL(targetUrl) + const basePath = base.pathname.endsWith('/') ? base.pathname : base.pathname + '/' + const rel = target.pathname.slice(basePath.length) + return rel || '' + } catch { + return '' + } +} + +export class Scanner { + private aborted = false + private paused = false + private resumeResolve: (() => void) | null = null + private fetchFn: FetchFn + + constructor(fetchFn?: FetchFn) { + if (fetchFn) { + this.fetchFn = fetchFn + } else { + // Use Electron net module wrapped as fetch, falling back to global fetch + this.fetchFn = this.electronFetch.bind(this) + } + } + + // --------------------------------------------------------------------------- + // Electron net-based fetch (used at runtime) + // --------------------------------------------------------------------------- + + private async electronFetch( + url: string, + init?: { method?: string; signal?: AbortSignal } + ): Promise<{ + ok: boolean + status: number + text(): Promise + headers: { get(name: string): string | null } + }> { + // Use Node.js built-in fetch (available in Node 18+). + // Electron's net.fetch can force HTTPS upgrades on HTTP URLs, causing + // TLS handshake errors against plain HTTP servers. + const fetchImpl = globalThis.fetch as unknown as FetchFn + return fetchImpl(url, init) + } + + // --------------------------------------------------------------------------- + // Pause / resume support + // --------------------------------------------------------------------------- + + pause(): void { + this.paused = true + } + + resume(): void { + if (!this.paused) return + this.paused = false + if (this.resumeResolve) { + this.resumeResolve() + this.resumeResolve = null + } + } + + private waitIfPaused(): Promise { + if (!this.paused) return Promise.resolve() + return new Promise((resolve) => { + this.resumeResolve = resolve + }) + } + + // --------------------------------------------------------------------------- + // Abort + // --------------------------------------------------------------------------- + + abort(): void { + this.aborted = true + // Unblock any waiting pause + if (this.resumeResolve) { + this.resumeResolve() + this.resumeResolve = null + } + } + + // --------------------------------------------------------------------------- + // Fetch helpers + // --------------------------------------------------------------------------- + + /** Fetch HTML from a directory URL. */ + protected async fetchHtml(url: string): Promise { + const response = await this.fetchFn(url) + if (!response.ok) { + throw new Error(`HTTP ${response.status} fetching ${url}`) + } + return response.text() + } + + /** Issue a HEAD request and return { size, mimeType }. */ + private async headRequest(url: string): Promise<{ size: number | null; mimeType: string }> { + try { + const response = await this.fetchFn(url, { method: 'HEAD' }) + const lengthHeader = response.headers.get('content-length') + const size = lengthHeader !== null ? parseInt(lengthHeader, 10) : null + const mimeType = response.headers.get('content-type') ?? '' + return { size, mimeType: mimeType.split(';')[0].trim() } + } catch { + return { size: null, mimeType: '' } + } + } + + // --------------------------------------------------------------------------- + // Main scan + // --------------------------------------------------------------------------- + + private maxDepth = 20 + + async scan(rootUrl: string, callbacks: ScanCallbacks): Promise { + this.aborted = false + this.paused = false + + // ------------------------------------------------------------------ + // Phase 1: Discovery — recursively collect all URLs + // ------------------------------------------------------------------ + interface DiscoveredEntry { + url: string + isDirectory: boolean + parentId: string + depth: number + } + + const discovered: DiscoveredEntry[] = [] + const visitedDirs = new Set() + + // Iterative BFS discovery + const queue: Array<{ url: string; parentId: string; depth: number }> = [ + { url: rootUrl, parentId: '', depth: 0 }, + ] + + while (queue.length > 0 && !this.aborted) { + await this.waitIfPaused() + if (this.aborted) break + + const { url, parentId, depth } = queue.shift()! + + if (visitedDirs.has(url)) continue + visitedDirs.add(url) + + let html: string + try { + html = await this.fetchHtml(url) + } catch (err) { + callbacks.onError(url, err instanceof Error ? err : new Error(String(err))) + continue + } + + const links = parseDirectoryListing(html, url) + for (const link of links) { + discovered.push({ url: link.url, isDirectory: link.isDirectory, parentId, depth }) + if (link.isDirectory && !visitedDirs.has(link.url) && depth < this.maxDepth) { + queue.push({ url: link.url, parentId: urlToId(link.url), depth: depth + 1 }) + } + } + } + + if (this.aborted) { + callbacks.onFinished() + return + } + + // ------------------------------------------------------------------ + // Phase 2: Process each discovered entry + // ------------------------------------------------------------------ + const total = discovered.length + let scanned = 0 + callbacks.onProgress(scanned, total) + + for (const entry of discovered) { + await this.waitIfPaused() + if (this.aborted) break + + const name = fileNameFromUrl(entry.url) + const nodeId = urlToId(entry.url) + const fullPath = relativePathFromBase(rootUrl, entry.url) + + if (entry.isDirectory) { + const node: TreeNode = { + id: nodeId, + parentId: entry.parentId, + name, + kind: 'folder', + url: '', + fullPath, + size: 'Unknown', + fileType: '', + iconGroup: 'folder', + checked: false, + expanded: false, + hidden: false, + children: [], + } + callbacks.onItem(node, entry.parentId) + } else { + // HEAD request for file metadata + const { size, mimeType } = await this.headRequest(entry.url) + + // Heuristic: if HEAD returns text/html and name has no extension, treat as directory + const isHtmlDir = mimeType.includes('text/html') && !name.includes('.') + if (isHtmlDir) { + const node: TreeNode = { + id: nodeId, + parentId: entry.parentId, + name, + kind: 'folder', + url: '', + fullPath, + size: '', + fileType: '', + iconGroup: 'folder', + checked: false, + expanded: false, + hidden: false, + children: [], + } + callbacks.onItem(node, entry.parentId) + } else { + const node: TreeNode = { + id: nodeId, + parentId: entry.parentId, + name, + kind: 'file', + url: entry.url, + fullPath, + size: formatSize(size), + fileType: mimeType, + iconGroup: getIconGroup(name, mimeType), + checked: false, + expanded: false, + hidden: false, + children: [], + } + callbacks.onItem(node, entry.parentId) + } + } + + scanned++ + callbacks.onProgress(scanned, total) + } + + callbacks.onFinished() + } +} diff --git a/electron-app/src/main/task-queue.ts b/electron-app/src/main/task-queue.ts new file mode 100644 index 0000000..bc7e76d --- /dev/null +++ b/electron-app/src/main/task-queue.ts @@ -0,0 +1,43 @@ +import { randomUUID } from 'crypto' +import type { Task } from '../shared/types' +import { Scanner } from './scanner' +import { Downloader } from './downloader' +import { defaultDownloadFolder } from './utils' + +export class TaskQueue { + tasks = new Map() + + createTask(url: string): Task { + const id = randomUUID() + const task: Task = { + id, + url, + status: 'idle', + nodes: {}, + roots: [], + checkedFiles: [], + downloads: {}, + scanProgress: { scanned: 0, total: 0 }, + downloadPath: defaultDownloadFolder(url) + } + this.tasks.set(id, { task, scanner: new Scanner(), downloader: new Downloader() }) + return task + } + + getTask(taskId: string) { + return this.tasks.get(taskId) + } + + removeTask(taskId: string): void { + const entry = this.tasks.get(taskId) + if (entry) { + entry.scanner.abort() + entry.downloader.cancelAll() + this.tasks.delete(taskId) + } + } + + allTasks(): Task[] { + return [...this.tasks.values()].map((e) => e.task) + } +} diff --git a/electron-app/src/main/utils.ts b/electron-app/src/main/utils.ts new file mode 100644 index 0000000..9ff57a8 --- /dev/null +++ b/electron-app/src/main/utils.ts @@ -0,0 +1,82 @@ +import { join, resolve, extname } from 'path' +import { homedir } from 'os' + +/** + * Strip path traversal sequences and illegal characters from a single path segment. + * Removes: `..`, `/`, `\`, and Windows-illegal chars (`<>:"|?*` and control chars). + */ +export function sanitizePathSegment(name: string): string { + // Remove path traversal + let sanitized = name.replace(/\.\./g, '') + // Remove path separators + sanitized = sanitized.replace(/[/\\]/g, '') + // Remove Windows-illegal characters: < > : " | ? * and control chars (0x00-0x1f) + // eslint-disable-next-line no-control-regex + sanitized = sanitized.replace(/[<>:"|?*\x00-\x1f]/g, '') + return sanitized +} + +/** + * Alias for sanitizePathSegment — sanitizes a file name. + */ +export function sanitizeFilename(name: string): string { + return sanitizePathSegment(name) +} + +/** + * Returns true if candidateUrl shares the same origin and has the same + * path prefix as baseUrl. + */ +export function isUrlInScope(baseUrl: string, candidateUrl: string): boolean { + try { + const base = new URL(baseUrl) + const candidate = new URL(candidateUrl) + + if (base.origin !== candidate.origin) { + return false + } + + // Normalize base path to end with '/' so prefix check works correctly + const basePath = base.pathname.endsWith('/') ? base.pathname : base.pathname + '/' + return candidate.pathname.startsWith(basePath) + } catch { + return false + } +} + +/** + * Safely joins root with parts, throwing if the resolved path would escape root. + */ +export function safeJoin(root: string, parts: string[]): string { + const resolvedRoot = resolve(root) + const joined = join(resolvedRoot, ...parts) + const resolvedJoined = resolve(joined) + + if (!resolvedJoined.startsWith(resolvedRoot + '/') && resolvedJoined !== resolvedRoot) { + throw new Error(`Path traversal detected: "${resolvedJoined}" escapes root "${resolvedRoot}"`) + } + + return resolvedJoined +} + +/** + * Derive a default download folder from the URL hostname, placed in ~/Downloads/. + * E.g. https://example.com/files/ → ~/Downloads/example.com + */ +export function defaultDownloadFolder(url: string): string { + try { + const parsed = new URL(url) + const hostname = parsed.hostname + return join(homedir(), 'Downloads', hostname) + } catch { + return join(homedir(), 'Downloads', 'index-ripper') + } +} + +/** + * Extract the lowercase file extension from a filename. + * Returns "" if none. + */ +export function normalizeExtension(fileName: string): string { + return extname(fileName).toLowerCase() +} diff --git a/electron-app/src/preload/index.d.ts b/electron-app/src/preload/index.d.ts new file mode 100644 index 0000000..a153669 --- /dev/null +++ b/electron-app/src/preload/index.d.ts @@ -0,0 +1,8 @@ +import { ElectronAPI } from '@electron-toolkit/preload' + +declare global { + interface Window { + electron: ElectronAPI + api: unknown + } +} diff --git a/electron-app/src/preload/index.ts b/electron-app/src/preload/index.ts new file mode 100644 index 0000000..af11177 --- /dev/null +++ b/electron-app/src/preload/index.ts @@ -0,0 +1,63 @@ +import { contextBridge, ipcRenderer } from 'electron' +import { electronAPI } from '@electron-toolkit/preload' + +const api = { + scan: { + start: (taskId: string, url: string) => ipcRenderer.invoke('scan:start', taskId, url), + stop: (taskId: string) => ipcRenderer.invoke('scan:stop', taskId), + pause: (taskId: string) => ipcRenderer.invoke('scan:pause', taskId), + resume: (taskId: string) => ipcRenderer.invoke('scan:resume', taskId) + }, + download: { + start: (taskId: string, files: unknown[], destPath: string) => + ipcRenderer.invoke('download:start', taskId, files, destPath), + pause: (taskId: string) => ipcRenderer.invoke('download:pause', taskId), + resume: (taskId: string) => ipcRenderer.invoke('download:resume', taskId), + cancel: (taskId: string, fileId: string) => + ipcRenderer.invoke('download:cancel', taskId, fileId), + cancelAll: (taskId: string) => ipcRenderer.invoke('download:cancelAll', taskId), + retry: (taskId: string, fileId: string) => + ipcRenderer.invoke('download:retry', taskId, fileId), + setWorkers: (count: number) => ipcRenderer.invoke('download:setWorkers', count) + }, + task: { + create: (url: string) => ipcRenderer.invoke('task:create', url), + remove: (taskId: string) => ipcRenderer.invoke('task:remove', taskId) + }, + settings: { + get: () => ipcRenderer.invoke('settings:get'), + set: (data: unknown) => ipcRenderer.invoke('settings:set', data) + }, + dialog: { + selectFolder: () => ipcRenderer.invoke('dialog:selectFolder') + }, + preview: { + fetch: (url: string) => ipcRenderer.invoke('preview:fetch', url), + fetchText: (url: string) => ipcRenderer.invoke('preview:fetchText', url) + }, + on: (channel: string, callback: (...args: unknown[]) => void) => { + const listener = (_event: Electron.IpcRendererEvent, ...args: unknown[]): void => + callback(...args) + ipcRenderer.on(channel, listener) + return (): void => { + ipcRenderer.removeListener(channel, listener) + } + } +} + +// Use `contextBridge` APIs to expose Electron APIs to +// renderer only if context isolation is enabled, otherwise +// just add to the DOM global. +if (process.contextIsolated) { + try { + contextBridge.exposeInMainWorld('electron', electronAPI) + contextBridge.exposeInMainWorld('api', api) + } catch (error) { + console.error(error) + } +} else { + // @ts-ignore (define in d.ts) + window.electron = electronAPI + // @ts-ignore (define in d.ts) + window.api = api +} diff --git a/electron-app/src/renderer/index.html b/electron-app/src/renderer/index.html new file mode 100644 index 0000000..f51f293 --- /dev/null +++ b/electron-app/src/renderer/index.html @@ -0,0 +1,16 @@ + + + + + Index Ripper + + + + +
+ + + diff --git a/electron-app/src/renderer/src/App.tsx b/electron-app/src/renderer/src/App.tsx new file mode 100644 index 0000000..f9ae406 --- /dev/null +++ b/electron-app/src/renderer/src/App.tsx @@ -0,0 +1,96 @@ +import { useEffect, useState } from 'react' +import { Toaster } from 'sonner' +import { TaskTabs } from './components/TaskTabs' +import { Toolbar } from './components/Toolbar' +import { ProgressSection } from './components/ProgressSection' +import { SplitPanel } from './components/SplitPanel' +import { SearchBar } from './components/SearchBar' +import { TypeFilters } from './components/TypeFilters' +import { FileTree } from './components/FileTree' +import { StatusBar } from './components/StatusBar' +import { DownloadsList } from './components/DownloadsList' +import { LogsPanel } from './components/LogsPanel' +import { PreviewPanel } from './components/PreviewPanel' +import { useIpcListeners } from './hooks/useIpcListeners' +import { useTaskStore } from './stores/task-store' + +function FilePanel(): JSX.Element { + return ( +
+ + + + +
+ ) +} + +type RightTab = 'preview' | 'downloads' | 'logs' + +function RightPanel(): JSX.Element { + const [tab, setTab] = useState('preview') + const previewNodeId = useTaskStore((s) => s.previewNodeId) + + // Auto-switch to preview tab when a file is double-clicked + useEffect(() => { + if (previewNodeId) setTab('preview') + }, [previewNodeId]) + + const tabs: { id: RightTab; label: string }[] = [ + { id: 'preview', label: 'Preview' }, + { id: 'downloads', label: 'Downloads' }, + { id: 'logs', label: 'Logs' }, + ] + + return ( +
+
+ {tabs.map((t) => ( + + ))} +
+
+ {tab === 'preview' && } + {tab === 'downloads' && } + {tab === 'logs' && } +
+
+ ) +} + +export default function App(): JSX.Element { + useIpcListeners() + + return ( +
+ + + + + } + right={} + /> +
+ ) +} diff --git a/electron-app/src/renderer/src/assets/main.css b/electron-app/src/renderer/src/assets/main.css new file mode 100644 index 0000000..a3495c3 --- /dev/null +++ b/electron-app/src/renderer/src/assets/main.css @@ -0,0 +1,92 @@ +@import "tailwindcss"; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + border-color: hsl(var(--border)); + } + body { + background-color: hsl(var(--background)); + color: hsl(var(--foreground)); + } +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; +} + +/* Overlay scrollbars — don't take up space */ +* { + scrollbar-width: thin; + scrollbar-color: rgba(100, 116, 139, 0.3) transparent; +} + +*::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +*::-webkit-scrollbar-track { + background: transparent; +} + +*::-webkit-scrollbar-thumb { + background: rgba(100, 116, 139, 0.3); + border-radius: 3px; +} + +*::-webkit-scrollbar-thumb:hover { + background: rgba(100, 116, 139, 0.5); +} diff --git a/electron-app/src/renderer/src/components/DownloadCard.tsx b/electron-app/src/renderer/src/components/DownloadCard.tsx new file mode 100644 index 0000000..02e72c2 --- /dev/null +++ b/electron-app/src/renderer/src/components/DownloadCard.tsx @@ -0,0 +1,84 @@ +import type { DownloadItem } from '../../../shared/types' +import { Progress } from './ui/progress' + +interface DownloadCardProps { + item: DownloadItem + onCancel: (id: string) => void + onRetry: (id: string) => void +} + +const statusColors: Record = { + queued: 'text-slate-400', + downloading: 'text-blue-400', + paused: 'text-slate-400', + completed: 'text-emerald-400', + failed: 'text-red-400', + cancelled: 'text-yellow-400' +} + +const statusLabels: Record = { + queued: 'Queued', + downloading: 'Downloading', + paused: 'Paused', + completed: 'Done', + failed: 'Failed', + cancelled: 'Cancelled' +} + +function formatSpeed(bytesPerSec: number): string { + if (bytesPerSec >= 1024 * 1024) { + return `${(bytesPerSec / (1024 * 1024)).toFixed(1)} MB/s` + } + if (bytesPerSec >= 1024) { + return `${(bytesPerSec / 1024).toFixed(1)} KB/s` + } + return `${bytesPerSec} B/s` +} + +export function DownloadCard({ item, onCancel, onRetry }: DownloadCardProps): JSX.Element { + const showCancel = item.status === 'queued' || item.status === 'downloading' || item.status === 'paused' + const showRetry = item.status === 'failed' + const colorClass = statusColors[item.status] + + return ( +
+
+ + {item.fileName} + +
+ {item.status === 'downloading' && item.speed > 0 && ( + {formatSpeed(item.speed)} + )} + {statusLabels[item.status]} +
+
+ + + + {(showCancel || showRetry) && ( +
+ {showCancel && ( + + )} + {showRetry && ( + + )} +
+ )} +
+ ) +} diff --git a/electron-app/src/renderer/src/components/DownloadsList.tsx b/electron-app/src/renderer/src/components/DownloadsList.tsx new file mode 100644 index 0000000..572c402 --- /dev/null +++ b/electron-app/src/renderer/src/components/DownloadsList.tsx @@ -0,0 +1,91 @@ +import { useTaskStore } from '../stores/task-store' +import { DownloadCard } from './DownloadCard' +import { Progress } from './ui/progress' +import type { DownloadItem } from '../../../shared/types' + +function estimateTimeRemaining(items: DownloadItem[]): string | null { + const downloading = items.filter((d) => d.status === 'downloading' && d.speed > 0) + if (downloading.length === 0) return null + + const totalRemaining = downloading.reduce((acc, d) => { + const remaining = d.totalSize > 0 ? d.totalSize - d.downloadedSize : 0 + return acc + remaining + }, 0) + + const totalSpeed = downloading.reduce((acc, d) => acc + d.speed, 0) + if (totalSpeed === 0 || totalRemaining === 0) return null + + const secsRemaining = totalRemaining / totalSpeed + if (secsRemaining >= 3600) { + return `~${Math.ceil(secsRemaining / 3600)} hr remaining` + } + if (secsRemaining >= 60) { + return `~${Math.ceil(secsRemaining / 60)} min remaining` + } + return `~${Math.ceil(secsRemaining)} sec remaining` +} + +export function DownloadsList(): JSX.Element { + const activeTaskId = useTaskStore((s) => s.activeTaskId) + const tasks = useTaskStore((s) => s.tasks) + const updateDownload = useTaskStore((s) => s.updateDownload) + + const activeTask = activeTaskId ? tasks[activeTaskId] : null + const downloads: DownloadItem[] = activeTask ? Object.values(activeTask.downloads) : [] + + const completedCount = downloads.filter((d) => d.status === 'completed').length + const total = downloads.length + + const aggregateProgress = + total === 0 + ? 0 + : downloads.reduce((acc, d) => acc + d.progress, 0) / total + + const timeRemaining = estimateTimeRemaining(downloads) + + const handleCancel = (id: string): void => { + if (!activeTaskId) return + updateDownload(activeTaskId, id, { status: 'cancelled' }) + window.api.download.cancel(activeTaskId, id) + } + + const handleRetry = (id: string): void => { + if (!activeTaskId) return + updateDownload(activeTaskId, id, { status: 'queued', progress: 0 }) + window.api.download.retry(activeTaskId, id) + } + + if (downloads.length === 0) { + return ( +
+ No downloads yet +
+ ) + } + + return ( +
+
+ {downloads.map((item) => ( + + ))} +
+ +
+
+ + {completedCount}/{total} done + {timeRemaining ? ` — ${timeRemaining}` : ''} + + {Math.round(aggregateProgress)}% +
+ +
+
+ ) +} diff --git a/electron-app/src/renderer/src/components/FileTree.tsx b/electron-app/src/renderer/src/components/FileTree.tsx new file mode 100644 index 0000000..5d31683 --- /dev/null +++ b/electron-app/src/renderer/src/components/FileTree.tsx @@ -0,0 +1,197 @@ +import { useRef, useEffect, useState, useCallback } from 'react' +import { useVirtualizer } from '@tanstack/react-virtual' +import { useFileTree } from '@/hooks/useFileTree' +import { useTaskStore } from '@/stores/task-store' +import { FileTreeRow } from './FileTreeRow' +import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react' + +interface MenuPos { + x: number + y: number +} + +function SortButton({ label, sortKey: key }: { label: string; sortKey: 'name' | 'size' | 'type' }): JSX.Element { + const currentKey = useTaskStore((s) => s.sortKey) + const currentDir = useTaskStore((s) => s.sortDir) + const setSort = useTaskStore((s) => s.setSort) + const active = currentKey === key + + return ( + + ) +} + +export function FileTree(): JSX.Element { + const { + visibleNodes, + handleRowClick, + toggleExpand, + selectAll, + deselectAll, + expandAll, + collapseAll + } = useFileTree() + + const setPreviewNodeId = useTaskStore((s) => s.setPreviewNodeId) + const handlePreview = useCallback((nodeId: string) => { + setPreviewNodeId(nodeId) + }, [setPreviewNodeId]) + + const parentRef = useRef(null) + const [menu, setMenu] = useState(null) + + const rowVirtualizer = useVirtualizer({ + count: visibleNodes.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 36, + overscan: 30 + }) + + // Close menu on left click or scroll + useEffect(() => { + if (!menu) return + const close = (): void => setMenu(null) + window.addEventListener('click', close) + window.addEventListener('scroll', close, true) + return () => { + window.removeEventListener('click', close) + window.removeEventListener('scroll', close, true) + } + }, [menu]) + + // Keyboard shortcut: Ctrl/Cmd+A to select all + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent): void => { + if ((e.metaKey || e.ctrlKey) && e.key === 'a') { + const active = document.activeElement + if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) return + e.preventDefault() + selectAll() + } + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [selectAll]) + + const menuAction = (fn: () => void): void => { + fn() + setMenu(null) + } + + // Native DOM contextmenu event + useEffect(() => { + const el = parentRef.current + if (!el) return + const handler = (e: MouseEvent): void => { + e.preventDefault() + e.stopPropagation() + setMenu({ x: e.clientX, y: e.clientY }) + } + el.addEventListener('contextmenu', handler) + return () => el.removeEventListener('contextmenu', handler) + }, []) + + return ( + <> + {/* Sort bar */} +
+ Sort: + + + +
+ + {/* Tree */} +
+ {visibleNodes.length === 0 ? ( +
+ No files to display +
+ ) : ( +
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => { + const { node, depth } = visibleNodes[virtualRow.index] + return ( +
+ +
+ ) + })} +
+ )} +
+ + {/* Context menu */} + {menu && ( +
e.stopPropagation()} + > + + +
+ + +
+ )} + + ) +} diff --git a/electron-app/src/renderer/src/components/FileTreeRow.tsx b/electron-app/src/renderer/src/components/FileTreeRow.tsx new file mode 100644 index 0000000..15d3452 --- /dev/null +++ b/electron-app/src/renderer/src/components/FileTreeRow.tsx @@ -0,0 +1,83 @@ +import { memo } from 'react' +import type { TreeNode } from '@shared/types' +import { EMOJI_ICONS } from '@shared/icons' +import { ChevronDown, ChevronRight } from 'lucide-react' + +interface FileTreeRowProps { + node: TreeNode + depth: number + onRowClick: (id: string, shiftKey: boolean) => void + onToggleExpand: (id: string) => void + onPreview?: (id: string) => void +} + +export const FileTreeRow = memo(function FileTreeRow({ + node, + depth, + onRowClick, + onToggleExpand, + onPreview +}: FileTreeRowProps) { + const icon = EMOJI_ICONS[node.iconGroup] ?? EMOJI_ICONS.binary + + const bgClass = node.checked + ? 'bg-blue-500/10 hover:bg-blue-500/20 border-l-blue-500' + : 'hover:bg-slate-800/60 border-l-transparent' + + return ( +
onRowClick(node.id, e.shiftKey)} + onDoubleClick={() => { + if (node.kind === 'file' && onPreview) onPreview(node.id) + }} + > + {/* Indent spacer */} +
+ + {/* Chevron for folders (left of icon) */} + {node.kind === 'folder' ? ( + + ) : ( +
+ )} + + {/* Icon */} + {icon} + + {/* Name */} + + {node.name} + + + {/* Spacer */} +
+ + {/* Size for files */} + {node.kind === 'file' && node.size && ( + + {node.size} + + )} +
+ ) +}) diff --git a/electron-app/src/renderer/src/components/LogsPanel.tsx b/electron-app/src/renderer/src/components/LogsPanel.tsx new file mode 100644 index 0000000..38a6220 --- /dev/null +++ b/electron-app/src/renderer/src/components/LogsPanel.tsx @@ -0,0 +1,58 @@ +import { useEffect, useRef, useState } from 'react' + +export interface LogMessage { + id: string + timestamp: Date + text: string +} + +export function LogsPanel(): JSX.Element { + const [messages, setMessages] = useState([]) + const bottomRef = useRef(null) + + // Auto-scroll to bottom when new messages arrive + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [messages]) + + // Expose append function via window for Task 10 IPC integration + useEffect(() => { + const handler = (event: CustomEvent<{ text: string }>): void => { + const msg: LogMessage = { + id: `${Date.now()}-${Math.random()}`, + timestamp: new Date(), + text: event.detail.text + } + setMessages((prev) => [...prev, msg]) + } + + window.addEventListener('log-message' as never, handler as EventListener) + return () => { + window.removeEventListener('log-message' as never, handler as EventListener) + } + }, []) + + if (messages.length === 0) { + return ( +
+ Waiting for activity... +
+ ) + } + + return ( +
+
+ {messages.map((msg) => ( +
+ + {msg.timestamp.toLocaleTimeString('en-US', { hour12: false })} + + {msg.text} +
+ ))} +
+
+
+ ) +} diff --git a/electron-app/src/renderer/src/components/PreviewPanel.tsx b/electron-app/src/renderer/src/components/PreviewPanel.tsx new file mode 100644 index 0000000..8829c3e --- /dev/null +++ b/electron-app/src/renderer/src/components/PreviewPanel.tsx @@ -0,0 +1,151 @@ +import { useEffect, useState } from 'react' +import { useTaskStore } from '@/stores/task-store' +import { Eye, FileText, Image, X } from 'lucide-react' + +const IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.ico'] +const TEXT_EXTS = [ + '.txt', '.md', '.html', '.htm', '.css', '.js', '.ts', '.tsx', '.jsx', + '.json', '.yaml', '.yml', '.toml', '.xml', '.py', '.go', '.rs', '.java', + '.c', '.cpp', '.h', '.sh', '.bat', '.log', '.csv', '.ini', '.cfg', + '.env', '.gitignore', '.dockerfile', '.makefile', +] + +function getExt(name: string): string { + const dot = name.lastIndexOf('.') + return dot >= 0 ? name.slice(dot).toLowerCase() : '' +} + +function isImage(name: string): boolean { + return IMAGE_EXTS.includes(getExt(name)) +} + +function isText(name: string): boolean { + const ext = getExt(name) + return TEXT_EXTS.includes(ext) || ext === '' +} + +export function PreviewPanel(): JSX.Element { + const previewNodeId = useTaskStore((s) => s.previewNodeId) + const setPreviewNodeId = useTaskStore((s) => s.setPreviewNodeId) + const task = useTaskStore((s) => (s.activeTaskId ? s.tasks[s.activeTaskId] : null)) + + const node = previewNodeId && task ? task.nodes[previewNodeId] : null + + const [imageDataUrl, setImageDataUrl] = useState(null) + const [textContent, setTextContent] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + setImageDataUrl(null) + setTextContent(null) + setError(null) + + if (!node || node.kind !== 'file' || !node.url) return + + let cancelled = false + setLoading(true) + + if (isImage(node.name)) { + // Fetch image via main process IPC + ;(window.api.preview.fetch(node.url) as Promise<{ dataUrl: string }>) + .then(({ dataUrl }) => { + if (!cancelled) setImageDataUrl(dataUrl) + }) + .catch((err: Error) => { + if (!cancelled) setError(err.message) + }) + .finally(() => { + if (!cancelled) setLoading(false) + }) + } else if (isText(node.name)) { + // Fetch text via main process IPC + ;(window.api.preview.fetchText(node.url) as Promise) + .then((text) => { + if (!cancelled) setTextContent(text) + }) + .catch((err: Error) => { + if (!cancelled) setError(err.message) + }) + .finally(() => { + if (!cancelled) setLoading(false) + }) + } else { + setLoading(false) + } + + return () => { cancelled = true } + }, [node?.id, node?.url]) + + if (!node) { + return ( +
+ +

Double-click a file to preview

+
+ ) + } + + const ext = getExt(node.name) + + return ( +
+ {/* Header */} +
+ {isImage(node.name) ? ( + + ) : ( + + )} + {node.name} + {node.size} + +
+ + {/* Content */} +
+ {loading && ( +
+

Loading preview...

+
+ )} + + {error && !loading && ( +
+

{error}

+
+ )} + + {!loading && !error && isImage(node.name) && imageDataUrl && ( +
+ {node.name} +
+ )} + + {!loading && !error && isText(node.name) && textContent !== null && ( +
+            {textContent}
+          
+ )} + + {!loading && !error && !isImage(node.name) && !isText(node.name) && ( +
+ +

Preview not available for {ext || 'this file type'}

+

Supports: images and text files

+
+ )} +
+
+ ) +} diff --git a/electron-app/src/renderer/src/components/ProgressSection.tsx b/electron-app/src/renderer/src/components/ProgressSection.tsx new file mode 100644 index 0000000..7c69e6d --- /dev/null +++ b/electron-app/src/renderer/src/components/ProgressSection.tsx @@ -0,0 +1,86 @@ +import { useTaskStore } from '@/stores/task-store' + +export function ProgressSection(): JSX.Element | null { + const task = useTaskStore((s) => (s.activeTaskId ? s.tasks[s.activeTaskId] : null)) + + if (!task) return null + + const isScanning = task.status === 'scanning' + const isDownloading = task.status === 'downloading' + + if (!isScanning && !isDownloading) return null + + // Scan progress + if (isScanning) { + const { scanned, total } = task.scanProgress + const indeterminate = total === 0 + const pct = total > 0 ? (scanned / total) * 100 : 0 + + return ( +
+
+ {indeterminate ? ( +
+ ) : ( +
+ )} +
+
+ {indeterminate + ? `Discovering URLs… ${scanned > 0 ? `${scanned} found` : ''}` + : `Scanning… ${scanned}/${total} (${pct.toFixed(0)}%)`} +
+ +
+ ) + } + + // Download progress + const downloads = Object.values(task.downloads) + if (downloads.length === 0) return null + + const completed = downloads.filter((d) => d.status === 'completed').length + const total = downloads.length + const pct = total > 0 ? (completed / total) * 100 : 0 + + // Aggregate speed + const activeSpeed = downloads + .filter((d) => d.status === 'downloading') + .reduce((sum, d) => sum + d.speed, 0) + + const formatSpeed = (bytesPerSec: number): string => { + if (bytesPerSec < 1024) return `${bytesPerSec.toFixed(0)} B/s` + if (bytesPerSec < 1024 * 1024) return `${(bytesPerSec / 1024).toFixed(1)} KB/s` + return `${(bytesPerSec / (1024 * 1024)).toFixed(1)} MB/s` + } + + return ( +
+
+
+
+
+ + Downloaded {completed}/{total} ({pct.toFixed(0)}%) + + {activeSpeed > 0 && {formatSpeed(activeSpeed)}} +
+
+ ) +} diff --git a/electron-app/src/renderer/src/components/RightPanel.tsx b/electron-app/src/renderer/src/components/RightPanel.tsx new file mode 100644 index 0000000..f6b64ed --- /dev/null +++ b/electron-app/src/renderer/src/components/RightPanel.tsx @@ -0,0 +1,39 @@ +import { useState } from 'react' +import { DownloadsList } from './DownloadsList' +import { LogsPanel } from './LogsPanel' + +type ActiveTab = 'downloads' | 'logs' + +export function RightPanel(): JSX.Element { + const [activeTab, setActiveTab] = useState('downloads') + + return ( +
+
+ + +
+
+ {activeTab === 'downloads' ? : } +
+
+ ) +} diff --git a/electron-app/src/renderer/src/components/SearchBar.tsx b/electron-app/src/renderer/src/components/SearchBar.tsx new file mode 100644 index 0000000..e422ce2 --- /dev/null +++ b/electron-app/src/renderer/src/components/SearchBar.tsx @@ -0,0 +1,67 @@ +import { useState, useRef, useEffect, useCallback } from 'react' +import { Search, X } from 'lucide-react' +import { useFileTree } from '@/hooks/useFileTree' + +export function SearchBar(): JSX.Element { + const [query, setQuery] = useState('') + const inputRef = useRef(null) + const debounceRef = useRef | null>(null) + const { searchFilter } = useFileTree() + + const handleChange = useCallback( + (value: string) => { + setQuery(value) + if (debounceRef.current) clearTimeout(debounceRef.current) + debounceRef.current = setTimeout(() => { + searchFilter(value) + }, 300) + }, + [searchFilter] + ) + + const handleClear = useCallback(() => { + setQuery('') + if (debounceRef.current) clearTimeout(debounceRef.current) + searchFilter('') + inputRef.current?.focus() + }, [searchFilter]) + + // Ctrl/Cmd+F focuses the search input + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent): void => { + if ((e.metaKey || e.ctrlKey) && e.key === 'f') { + e.preventDefault() + inputRef.current?.focus() + inputRef.current?.select() + } + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, []) + + return ( +
+ + handleChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Escape') handleClear() + }} + placeholder="Search files..." + className="flex-1 min-w-0 bg-transparent text-sm text-slate-200 placeholder:text-slate-600 focus:outline-none" + /> + {query && ( + + )} +
+ ) +} diff --git a/electron-app/src/renderer/src/components/SplitPanel.tsx b/electron-app/src/renderer/src/components/SplitPanel.tsx new file mode 100644 index 0000000..14fd1ee --- /dev/null +++ b/electron-app/src/renderer/src/components/SplitPanel.tsx @@ -0,0 +1,78 @@ +import { useCallback, useRef, useState } from 'react' + +interface SplitPanelProps { + left: React.ReactNode + right: React.ReactNode + defaultLeftPercent?: number + minLeftPercent?: number + maxLeftPercent?: number +} + +export function SplitPanel({ + left, + right, + defaultLeftPercent = 65, + minLeftPercent = 20, + maxLeftPercent = 80 +}: SplitPanelProps): JSX.Element { + const [leftPercent, setLeftPercent] = useState(defaultLeftPercent) + const containerRef = useRef(null) + const isDragging = useRef(false) + + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault() + isDragging.current = true + + const onMouseMove = (moveEvent: MouseEvent): void => { + if (!isDragging.current || !containerRef.current) return + const rect = containerRef.current.getBoundingClientRect() + const x = moveEvent.clientX - rect.left + const percent = Math.min( + maxLeftPercent, + Math.max(minLeftPercent, (x / rect.width) * 100) + ) + setLeftPercent(percent) + } + + const onMouseUp = (): void => { + isDragging.current = false + document.removeEventListener('mousemove', onMouseMove) + document.removeEventListener('mouseup', onMouseUp) + document.body.style.cursor = '' + document.body.style.userSelect = '' + } + + document.body.style.cursor = 'col-resize' + document.body.style.userSelect = 'none' + document.addEventListener('mousemove', onMouseMove) + document.addEventListener('mouseup', onMouseUp) + }, + [minLeftPercent, maxLeftPercent] + ) + + return ( +
+ {/* Left panel */} +
+ {left} +
+ + {/* Divider */} +
+ + {/* Right panel */} +
+ {right} +
+
+ ) +} diff --git a/electron-app/src/renderer/src/components/StatusBar.tsx b/electron-app/src/renderer/src/components/StatusBar.tsx new file mode 100644 index 0000000..f00b638 --- /dev/null +++ b/electron-app/src/renderer/src/components/StatusBar.tsx @@ -0,0 +1,25 @@ +import { useFileTree } from '@/hooks/useFileTree' +import { Files, Folder, CheckSquare } from 'lucide-react' + +export function StatusBar(): JSX.Element { + const { stats } = useFileTree() + + return ( +
+ + + {stats.files} files + + + + {stats.folders} folders + + {stats.selected > 0 && ( + + + {stats.selected} selected + + )} +
+ ) +} diff --git a/electron-app/src/renderer/src/components/TaskTabs.tsx b/electron-app/src/renderer/src/components/TaskTabs.tsx new file mode 100644 index 0000000..fe9f613 --- /dev/null +++ b/electron-app/src/renderer/src/components/TaskTabs.tsx @@ -0,0 +1,70 @@ +import { useTaskStore } from '@/stores/task-store' +import { Plus, X } from 'lucide-react' + +function hostnameFromUrl(url: string): string { + try { + return new URL(url).hostname.replace(/^www\./, '') + } catch { + return url || 'New Tab' + } +} + +export function TaskTabs(): JSX.Element { + const tasks = useTaskStore((s) => s.tasks) + const activeTaskId = useTaskStore((s) => s.activeTaskId) + const createTask = useTaskStore((s) => s.createTask) + const removeTask = useTaskStore((s) => s.removeTask) + const setActiveTask = useTaskStore((s) => s.setActiveTask) + + const taskList = Object.values(tasks) + + const handleAdd = async (): Promise => { + const task = (await window.api.task.create('')) as { id: string; url: string } + createTask(task.id, task.url) + } + + const handleRemove = (taskId: string): void => { + removeTask(taskId) + window.api.task.remove(taskId) + } + + return ( +
+ {taskList.map((task) => { + const isActive = task.id === activeTaskId + return ( +
setActiveTask(task.id)} + > + + {hostnameFromUrl(task.url) || 'New Tab'} + + +
+ ) + })} + +
+ ) +} diff --git a/electron-app/src/renderer/src/components/Toolbar.tsx b/electron-app/src/renderer/src/components/Toolbar.tsx new file mode 100644 index 0000000..e8be285 --- /dev/null +++ b/electron-app/src/renderer/src/components/Toolbar.tsx @@ -0,0 +1,216 @@ +import { useState } from 'react' +import { useTaskStore } from '@/stores/task-store' +import { Play, Square, Download, Pause, FolderOpen } from 'lucide-react' +import { toast } from 'sonner' +import type { Task } from '../../../shared/types' + +const STATUS_CONFIG: Record = { + idle: { color: 'bg-slate-400', label: 'Ready' }, + scanning: { color: 'bg-yellow-400', label: 'Scanning' }, + scanned: { color: 'bg-green-400', label: 'Scanned' }, + downloading: { color: 'bg-blue-400', label: 'Downloading' }, + done: { color: 'bg-green-400', label: 'Done' }, + error: { color: 'bg-red-400', label: 'Error' }, + cancelled: { color: 'bg-orange-400', label: 'Cancelled' } +} + +export function Toolbar(): JSX.Element { + const activeTaskId = useTaskStore((s) => s.activeTaskId) + const task = useTaskStore((s) => (s.activeTaskId ? s.tasks[s.activeTaskId] : null)) + const updateTask = useTaskStore((s) => s.updateTask) + const [threadCount, setThreadCount] = useState(3) + + const url = task?.url ?? '' + const status = task?.status ?? 'idle' + const isScanning = status === 'scanning' + const isDownloading = status === 'downloading' + const showPause = isScanning || isDownloading + const statusInfo = STATUS_CONFIG[status] + + const handleUrlChange = (e: React.ChangeEvent): void => { + if (activeTaskId) { + updateTask(activeTaskId, { url: e.target.value }) + } + } + + const handleScanToggle = async (): Promise => { + if (!activeTaskId || !url) return + + if (isScanning) { + await window.api.scan.stop(activeTaskId) + updateTask(activeTaskId, { status: 'cancelled' }) + } else { + // Clear previous scan data and start fresh + updateTask(activeTaskId, { + status: 'scanning', + nodes: {}, + roots: [], + checkedFiles: [], + scanProgress: { scanned: 0, total: 0 } + }) + // Fire-and-forget: results come back via IPC events + window.api.scan.start(activeTaskId, url).catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + toast.error(`Scan failed: ${message}`) + updateTask(activeTaskId, { status: 'error' }) + }) + } + } + + const handleDownload = async (): Promise => { + if (!activeTaskId || !task) return + + // Determine download path + let destPath = task.downloadPath + if (!destPath) { + const selected = await window.api.dialog.selectFolder() + if (!selected) return // user cancelled + destPath = selected + updateTask(activeTaskId, { downloadPath: destPath }) + } + + // Build file list from checked files + const files = task.checkedFiles + .map((fileId) => { + const node = task.nodes[fileId] + if (!node || node.kind !== 'file') return null + return { + id: node.id, + url: node.url, + destPath, + fileName: node.name + } + }) + .filter((f): f is NonNullable => f !== null) + + if (files.length === 0) { + toast.error('No files selected for download') + return + } + + // Initialize download items in the store + const downloads: Record = {} + for (const file of files) { + downloads[file.id] = { + id: file.id, + fileName: file.fileName, + url: file.url, + destPath: file.destPath, + status: 'queued', + progress: 0, + speed: 0, + totalSize: 0, + downloadedSize: 0 + } + } + updateTask(activeTaskId, { status: 'downloading', downloads }) + + try { + await window.api.download.start(activeTaskId, files, destPath) + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + toast.error(`Download failed: ${message}`) + updateTask(activeTaskId, { status: 'error' }) + } + } + + const handlePause = (): void => { + if (!activeTaskId) return + if (isScanning) { + window.api.scan.pause(activeTaskId) + } else if (isDownloading) { + window.api.download.pause(activeTaskId) + } + } + + const handleThreadChange = (e: React.ChangeEvent): void => { + const count = Number(e.target.value) + setThreadCount(count) + window.api.download.setWorkers(count) + } + + const handleSelectFolder = async (): Promise => { + if (!activeTaskId) return + const selected = await window.api.dialog.selectFolder() + if (selected) { + updateTask(activeTaskId, { downloadPath: selected }) + } + } + + return ( +
+ {/* URL input */} + { + if (e.key === 'Enter') handleScanToggle() + }} + /> + + {/* Scan / Stop button */} + + + {/* Download button */} + + + {/* Pause button */} + {showPause && ( + + )} + + {/* Folder button */} + + + {/* Thread count */} + + + {/* Status indicator */} +
+ + {statusInfo.label} +
+
+ ) +} diff --git a/electron-app/src/renderer/src/components/TypeFilters.tsx b/electron-app/src/renderer/src/components/TypeFilters.tsx new file mode 100644 index 0000000..cc7f4c0 --- /dev/null +++ b/electron-app/src/renderer/src/components/TypeFilters.tsx @@ -0,0 +1,83 @@ +import { useState, useCallback } from 'react' +import { useFileTree } from '@/hooks/useFileTree' +import { Check, X } from 'lucide-react' + +export function TypeFilters(): JSX.Element { + const { extensionCounts, typeFilter, setAllTypesVisible } = useFileTree() + const [hiddenExts, setHiddenExts] = useState>(new Set()) + + const extensions = Object.entries(extensionCounts).sort(([a], [b]) => a.localeCompare(b)) + + const toggleExt = useCallback( + (ext: string) => { + setHiddenExts((prev) => { + const next = new Set(prev) + if (next.has(ext)) { + next.delete(ext) + typeFilter(ext, true) + } else { + next.add(ext) + typeFilter(ext, false) + } + return next + }) + }, + [typeFilter] + ) + + const showAll = useCallback(() => { + setAllTypesVisible(true) + setHiddenExts(new Set()) + }, [setAllTypesVisible]) + + const hideAll = useCallback(() => { + setAllTypesVisible(false) + setHiddenExts(new Set(Object.keys(extensionCounts))) + }, [setAllTypesVisible, extensionCounts]) + + if (extensions.length === 0) return <> + + return ( +
+ {/* Fixed ALL buttons */} +
+ + +
+ + {/* Scrollable extension chips */} +
+ {extensions.map(([ext, count]) => { + const isHidden = hiddenExts.has(ext) + return ( + + ) + })} +
+
+ ) +} diff --git a/electron-app/src/renderer/src/components/ui/button.tsx b/electron-app/src/renderer/src/components/ui/button.tsx new file mode 100644 index 0000000..4d38506 --- /dev/null +++ b/electron-app/src/renderer/src/components/ui/button.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/electron-app/src/renderer/src/components/ui/checkbox.tsx b/electron-app/src/renderer/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..f5a7e43 --- /dev/null +++ b/electron-app/src/renderer/src/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import { CheckIcon } from "lucide-react" +import { Checkbox as CheckboxPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/electron-app/src/renderer/src/components/ui/context-menu.tsx b/electron-app/src/renderer/src/components/ui/context-menu.tsx new file mode 100644 index 0000000..5aaca76 --- /dev/null +++ b/electron-app/src/renderer/src/components/ui/context-menu.tsx @@ -0,0 +1,252 @@ +"use client" + +import * as React from "react" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" +import { ContextMenu as ContextMenuPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function ContextMenu({ + ...props +}: React.ComponentProps) { + return +} + +function ContextMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function ContextMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function ContextMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function ContextMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function ContextMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function ContextMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function ContextMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function ContextMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} diff --git a/electron-app/src/renderer/src/components/ui/progress.tsx b/electron-app/src/renderer/src/components/ui/progress.tsx new file mode 100644 index 0000000..af2ccf5 --- /dev/null +++ b/electron-app/src/renderer/src/components/ui/progress.tsx @@ -0,0 +1,29 @@ +import * as React from "react" +import { Progress as ProgressPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Progress({ + className, + value, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { Progress } diff --git a/electron-app/src/renderer/src/components/ui/scroll-area.tsx b/electron-app/src/renderer/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..e38a492 --- /dev/null +++ b/electron-app/src/renderer/src/components/ui/scroll-area.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { ScrollArea as ScrollAreaPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar } diff --git a/electron-app/src/renderer/src/components/ui/sonner.tsx b/electron-app/src/renderer/src/components/ui/sonner.tsx new file mode 100644 index 0000000..1cd7d2a --- /dev/null +++ b/electron-app/src/renderer/src/components/ui/sonner.tsx @@ -0,0 +1,8 @@ +import { Toaster as Sonner } from 'sonner' + +// Re-export sonner's Toaster for use in the app. +// This replaces the auto-generated shadcn wrapper which had circular imports +// and a next-themes dependency that doesn't apply to Electron. +const Toaster = Sonner + +export { Toaster } diff --git a/electron-app/src/renderer/src/components/ui/tabs.tsx b/electron-app/src/renderer/src/components/ui/tabs.tsx new file mode 100644 index 0000000..7f2c4f2 --- /dev/null +++ b/electron-app/src/renderer/src/components/ui/tabs.tsx @@ -0,0 +1,89 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Tabs as TabsPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + orientation = "horizontal", + ...props +}: React.ComponentProps) { + return ( + + ) +} + +const tabsListVariants = cva( + "group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none", + { + variants: { + variant: { + default: "bg-muted", + line: "gap-1 bg-transparent", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function TabsList({ + className, + variant = "default", + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants } diff --git a/electron-app/src/renderer/src/hooks/useFileTree.ts b/electron-app/src/renderer/src/hooks/useFileTree.ts new file mode 100644 index 0000000..f4e7576 --- /dev/null +++ b/electron-app/src/renderer/src/hooks/useFileTree.ts @@ -0,0 +1,326 @@ +import { useMemo, useCallback, useRef } from 'react' +import { useTaskStore } from '@/stores/task-store' +import type { TreeNode } from '@shared/types' + +export interface VisibleNode { + node: TreeNode + depth: number +} + +export type SortKey = 'name' | 'size' | 'type' | 'none' +export type SortDir = 'asc' | 'desc' + +function parseSizeBytes(size: string): number { + if (!size || size === 'Unknown') return -1 + const match = size.match(/^([\d.]+)\s*(B|KB|MB|GB|TB)$/i) + if (!match) return -1 + const val = parseFloat(match[1]) + const unit = match[2].toUpperCase() + const multipliers: Record = { B: 1, KB: 1024, MB: 1048576, GB: 1073741824, TB: 1099511627776 } + return val * (multipliers[unit] ?? 1) +} + +function sortChildren( + children: string[], + nodes: Record, + key: SortKey, + dir: SortDir +): string[] { + if (key === 'none' || children.length === 0) return children + + const sorted = [...children].sort((a, b) => { + const na = nodes[a] + const nb = nodes[b] + if (!na || !nb) return 0 + + // Folders always before files + if (na.kind !== nb.kind) return na.kind === 'folder' ? -1 : 1 + + let cmp = 0 + switch (key) { + case 'name': + cmp = na.name.localeCompare(nb.name, undefined, { numeric: true, sensitivity: 'base' }) + break + case 'size': + cmp = parseSizeBytes(na.size) - parseSizeBytes(nb.size) + break + case 'type': { + const extA = na.name.lastIndexOf('.') >= 0 ? na.name.slice(na.name.lastIndexOf('.')) : '' + const extB = nb.name.lastIndexOf('.') >= 0 ? nb.name.slice(nb.name.lastIndexOf('.')) : '' + cmp = extA.localeCompare(extB) + if (cmp === 0) cmp = na.name.localeCompare(nb.name, undefined, { numeric: true }) + break + } + } + return dir === 'desc' ? -cmp : cmp + }) + return sorted +} + +function flattenTree( + nodes: Record, + roots: string[], + sortKey: SortKey, + sortDir: SortDir +): VisibleNode[] { + const result: VisibleNode[] = [] + function walk(nodeId: string, depth: number): void { + const node = nodes[nodeId] + if (!node || node.hidden) return + result.push({ node, depth }) + if (node.kind === 'folder' && node.expanded) { + const sorted = sortChildren(node.children, nodes, sortKey, sortDir) + for (const childId of sorted) walk(childId, depth + 1) + } + } + const sortedRoots = sortChildren(roots, nodes, sortKey, sortDir) + for (const rootId of sortedRoots) walk(rootId, 0) + return result +} + +function updateFolderVisibility(nodes: Record): void { + const hasVisibleDescendant = (nodeId: string): boolean => { + const node = nodes[nodeId] + if (!node) return false + if (node.kind === 'file') return !node.hidden + return node.children.some((childId) => hasVisibleDescendant(childId)) + } + for (const [id, node] of Object.entries(nodes)) { + if (node.kind === 'folder') { + nodes[id] = { ...node, hidden: !hasVisibleDescendant(id) } + } + } +} + +export function useFileTree() { + const activeTaskId = useTaskStore((s) => s.activeTaskId) + const task = useTaskStore((s) => (s.activeTaskId ? s.tasks[s.activeTaskId] : null)) + const toggleCheckStore = useTaskStore((s) => s.toggleCheck) + const toggleExpandStore = useTaskStore((s) => s.toggleExpand) + const updateTask = useTaskStore((s) => s.updateTask) + const sortKey = useTaskStore((s) => s.sortKey) + const sortDir = useTaskStore((s) => s.sortDir) + + const nodes = task?.nodes ?? {} + const roots = task?.roots ?? [] + + // Track last clicked index for shift-select + const lastClickedIndex = useRef(-1) + + const visibleNodes = useMemo( + () => flattenTree(nodes, roots, sortKey, sortDir), + [nodes, roots, sortKey, sortDir] + ) + + const toggleCheck = useCallback( + (nodeId: string) => { + if (activeTaskId) toggleCheckStore(activeTaskId, nodeId) + }, + [activeTaskId, toggleCheckStore] + ) + + /** Handle click with shift support for range selection. */ + const handleRowClick = useCallback( + (nodeId: string, shiftKey: boolean) => { + if (!activeTaskId) return + + // Read fresh state to avoid stale closures + const currentTask = useTaskStore.getState().tasks[activeTaskId] + if (!currentTask) return + + // Recompute visible list from fresh state + const currentVisible = flattenTree(currentTask.nodes, currentTask.roots, sortKey, sortDir) + const currentIndex = currentVisible.findIndex((v) => v.node.id === nodeId) + if (currentIndex < 0) return + + if (shiftKey && lastClickedIndex.current >= 0) { + const start = Math.min(lastClickedIndex.current, currentIndex) + const end = Math.max(lastClickedIndex.current, currentIndex) + + const newNodes: Record = {} + for (const [id, node] of Object.entries(currentTask.nodes)) { + newNodes[id] = { ...node } + } + + for (let i = start; i <= end; i++) { + const nid = currentVisible[i].node.id + if (newNodes[nid]) { + newNodes[nid] = { ...newNodes[nid], checked: true } + if (newNodes[nid].kind === 'folder') { + const cascade = (fid: string): void => { + for (const cid of (newNodes[fid]?.children ?? [])) { + if (newNodes[cid]) { + newNodes[cid] = { ...newNodes[cid], checked: true } + if (newNodes[cid].kind === 'folder') cascade(cid) + } + } + } + cascade(nid) + } + } + } + + const checkedFiles = Object.values(newNodes) + .filter((n) => n.kind === 'file' && n.checked) + .map((n) => n.id) + useTaskStore.getState().updateTask(activeTaskId, { nodes: newNodes, checkedFiles }) + } else { + toggleCheckStore(activeTaskId, nodeId) + } + + lastClickedIndex.current = currentIndex + }, + [activeTaskId, sortKey, sortDir, toggleCheckStore] + ) + + const toggleExpand = useCallback( + (nodeId: string) => { + if (activeTaskId) toggleExpandStore(activeTaskId, nodeId) + }, + [activeTaskId, toggleExpandStore] + ) + + const selectAll = useCallback(() => { + if (!activeTaskId || !task) return + const newNodes: Record = {} + for (const [id, node] of Object.entries(task.nodes)) { + newNodes[id] = { ...node, checked: true } + } + const checkedFiles = Object.values(newNodes) + .filter((n) => n.kind === 'file') + .map((n) => n.id) + updateTask(activeTaskId, { nodes: newNodes, checkedFiles }) + }, [activeTaskId, task, updateTask]) + + const deselectAll = useCallback(() => { + if (!activeTaskId || !task) return + const newNodes: Record = {} + for (const [id, node] of Object.entries(task.nodes)) { + newNodes[id] = { ...node, checked: false } + } + updateTask(activeTaskId, { nodes: newNodes, checkedFiles: [] }) + }, [activeTaskId, task, updateTask]) + + const expandAll = useCallback(() => { + if (!activeTaskId || !task) return + const newNodes: Record = {} + for (const [id, node] of Object.entries(task.nodes)) { + newNodes[id] = { ...node, expanded: node.kind === 'folder' ? true : node.expanded } + } + updateTask(activeTaskId, { nodes: newNodes }) + }, [activeTaskId, task, updateTask]) + + const collapseAll = useCallback(() => { + if (!activeTaskId || !task) return + const newNodes: Record = {} + for (const [id, node] of Object.entries(task.nodes)) { + newNodes[id] = { ...node, expanded: false } + } + updateTask(activeTaskId, { nodes: newNodes }) + }, [activeTaskId, task, updateTask]) + + const searchFilter = useCallback( + (query: string) => { + if (!activeTaskId || !task) return + const lowerQuery = query.toLowerCase() + const newNodes: Record = {} + for (const [id, node] of Object.entries(task.nodes)) { + if (!query) { + newNodes[id] = { ...node, hidden: false } + } else { + const matches = node.name.toLowerCase().includes(lowerQuery) + newNodes[id] = { ...node, hidden: !matches && node.kind !== 'folder' } + } + } + if (query) { + updateFolderVisibility(newNodes) + } + updateTask(activeTaskId, { nodes: newNodes }) + }, + [activeTaskId, task, updateTask] + ) + + const typeFilter = useCallback( + (ext: string, visible: boolean) => { + if (!activeTaskId) return + const currentTask = useTaskStore.getState().tasks[activeTaskId] + if (!currentTask) return + + const lowerExt = ext.toLowerCase() + const newNodes: Record = {} + for (const [id, node] of Object.entries(currentTask.nodes)) { + if (node.kind === 'file' && node.name.toLowerCase().endsWith(lowerExt)) { + newNodes[id] = { ...node, hidden: !visible } + } else { + newNodes[id] = { ...node } + } + } + updateFolderVisibility(newNodes) + useTaskStore.getState().updateTask(activeTaskId, { nodes: newNodes }) + }, + [activeTaskId] + ) + + const setAllTypesVisible = useCallback( + (visible: boolean) => { + if (!activeTaskId) return + const currentTask = useTaskStore.getState().tasks[activeTaskId] + if (!currentTask) return + + const newNodes: Record = {} + for (const [id, node] of Object.entries(currentTask.nodes)) { + if (node.kind === 'file') { + newNodes[id] = { ...node, hidden: !visible } + } else { + newNodes[id] = { ...node } + } + } + updateFolderVisibility(newNodes) + useTaskStore.getState().updateTask(activeTaskId, { nodes: newNodes }) + }, + [activeTaskId] + ) + + const extensionCounts = useMemo(() => { + const counts: Record = {} + for (const node of Object.values(nodes)) { + if (node.kind === 'file') { + const dotIndex = node.name.lastIndexOf('.') + const ext = dotIndex >= 0 ? node.name.slice(dotIndex).toLowerCase() : '(no ext)' + counts[ext] = (counts[ext] ?? 0) + 1 + } + } + return counts + }, [nodes]) + + const stats = useMemo(() => { + let files = 0 + let folders = 0 + let selected = 0 + for (const node of Object.values(nodes)) { + if (node.kind === 'file') { + files++ + if (node.checked) selected++ + } else { + folders++ + } + } + return { files, folders, selected } + }, [nodes]) + + return { + visibleNodes, + toggleCheck, + handleRowClick, + toggleExpand, + selectAll, + deselectAll, + expandAll, + collapseAll, + searchFilter, + typeFilter, + setAllTypesVisible, + extensionCounts, + stats + } +} diff --git a/electron-app/src/renderer/src/hooks/useIpc.ts b/electron-app/src/renderer/src/hooks/useIpc.ts new file mode 100644 index 0000000..f3e809d --- /dev/null +++ b/electron-app/src/renderer/src/hooks/useIpc.ts @@ -0,0 +1,48 @@ +import { useEffect } from 'react' + +export interface Api { + scan: { + start: (taskId: string, url: string) => Promise + stop: (taskId: string) => Promise + pause: (taskId: string) => Promise + resume: (taskId: string) => Promise + } + download: { + start: (taskId: string, files: unknown[], destPath: string) => Promise + pause: (taskId: string) => Promise + resume: (taskId: string) => Promise + cancel: (taskId: string, fileId: string) => Promise + cancelAll: (taskId: string) => Promise + retry: (taskId: string, fileId: string) => Promise + setWorkers: (count: number) => Promise + } + task: { + create: (url: string) => Promise + remove: (taskId: string) => Promise + } + settings: { + get: () => Promise + set: (data: unknown) => Promise + } + dialog: { + selectFolder: () => Promise + } + preview: { + fetch: (url: string) => Promise<{ dataUrl: string; contentType: string }> + fetchText: (url: string) => Promise + } + on: (channel: string, callback: (...args: unknown[]) => void) => () => void +} + +declare global { + interface Window { + api: Api + } +} + +export function useIpcEvent(channel: string, callback: (...args: unknown[]) => void): void { + useEffect(() => { + const unsubscribe = window.api.on(channel, callback) + return unsubscribe + }, [channel, callback]) +} diff --git a/electron-app/src/renderer/src/hooks/useIpcListeners.ts b/electron-app/src/renderer/src/hooks/useIpcListeners.ts new file mode 100644 index 0000000..c9f4658 --- /dev/null +++ b/electron-app/src/renderer/src/hooks/useIpcListeners.ts @@ -0,0 +1,168 @@ +import { useCallback, useEffect, useRef } from 'react' +import { useTaskStore } from '@/stores/task-store' +import { useIpcEvent } from './useIpc' +import { toast } from 'sonner' +import type { Task, TreeNode } from '../../../shared/types' + +/** + * Subscribes to all IPC events from the main process and updates the Zustand store. + * Batches scan:item events to avoid O(n²) store updates. + * Should be called once at the App root level. + */ +export function useIpcListeners(): void { + const updateTask = useTaskStore((s) => s.updateTask) + const updateDownload = useTaskStore((s) => s.updateDownload) + + // --- Batch buffer for scan:item events --- + const pendingNodes = useRef>(new Map()) + const flushScheduled = useRef(false) + + const flushNodes = useCallback(() => { + flushScheduled.current = false + const batches = pendingNodes.current + if (batches.size === 0) return + + // Process all pending batches at once + for (const [taskId, nodes] of batches) { + const state = useTaskStore.getState() + const task = state.tasks[taskId] + if (!task) continue + + // Build updated nodes map in one pass (no per-node spread) + const updatedNodes = { ...task.nodes } + const updatedRoots = [...task.roots] + + for (const node of nodes) { + const nodeToAdd = node.kind === 'folder' ? { ...node, expanded: true } : node + updatedNodes[nodeToAdd.id] = nodeToAdd + + // Link to parent + if (nodeToAdd.parentId && updatedNodes[nodeToAdd.parentId]) { + const parent = updatedNodes[nodeToAdd.parentId] + if (!parent.children.includes(nodeToAdd.id)) { + updatedNodes[nodeToAdd.parentId] = { + ...parent, + children: [...parent.children, nodeToAdd.id] + } + } + } + + // Track roots + if (!nodeToAdd.parentId && !updatedRoots.includes(nodeToAdd.id)) { + updatedRoots.push(nodeToAdd.id) + } + } + + useTaskStore.getState().updateTask(taskId, { nodes: updatedNodes, roots: updatedRoots }) + } + + batches.clear() + }, []) + + const scheduleFlush = useCallback(() => { + if (!flushScheduled.current) { + flushScheduled.current = true + requestAnimationFrame(flushNodes) + } + }, [flushNodes]) + + // --- Scan events --- + + const onScanItem = useCallback( + (...args: unknown[]) => { + const [taskId, nodeData] = args as [string, TreeNode] + const batch = pendingNodes.current.get(taskId) ?? [] + batch.push(nodeData) + pendingNodes.current.set(taskId, batch) + scheduleFlush() + }, + [scheduleFlush] + ) + + const onScanProgress = useCallback( + (...args: unknown[]) => { + const [taskId, scanned, total] = args as [string, number, number] + updateTask(taskId, { scanProgress: { scanned, total } }) + }, + [updateTask] + ) + + const onScanFinished = useCallback( + (...args: unknown[]) => { + // Flush any remaining nodes first + flushNodes() + + const [taskId] = args as [string] + const task = useTaskStore.getState().tasks[taskId] + const status = task?.status === 'scanning' ? 'scanned' : 'cancelled' + updateTask(taskId, { status }) + + if (status === 'scanned' && task) { + const fileCount = Object.values(task.nodes).filter((n) => n.kind === 'file').length + toast.success(`Scan finished — ${fileCount} files found`) + } + }, + [updateTask, flushNodes] + ) + + const onScanError = useCallback((...args: unknown[]) => { + const [_taskId, errUrl, message] = args as [string, string, string] + const text = `[Error] ${errUrl}: ${message}` + window.dispatchEvent(new CustomEvent('log-message', { detail: { text } })) + }, []) + + // --- Download events --- + + const onDownloadProgress = useCallback( + (...args: unknown[]) => { + const [taskId, fileId, percent, speed] = args as [string, string, number, number] + updateDownload(taskId, fileId, { progress: percent, speed, status: 'downloading' }) + }, + [updateDownload] + ) + + const onDownloadStatus = useCallback( + (...args: unknown[]) => { + const [taskId, fileId, status] = args as [string, string, string] + updateDownload(taskId, fileId, { + status: status as 'queued' | 'downloading' | 'paused' | 'completed' | 'failed' | 'cancelled' + }) + }, + [updateDownload] + ) + + const onDownloadFinished = useCallback( + (...args: unknown[]) => { + const [taskId, completed, total] = args as [string, number, number] + updateTask(taskId, { status: 'done' }) + toast.success(`Downloaded ${completed}/${total} files`) + }, + [updateTask] + ) + + const onLogMessage = useCallback((...args: unknown[]) => { + const [_taskId, text] = args as [string, string] + window.dispatchEvent(new CustomEvent('log-message', { detail: { text } })) + }, []) + + // --- Register all listeners --- + + useIpcEvent('scan:item', onScanItem) + useIpcEvent('scan:progress', onScanProgress) + useIpcEvent('scan:finished', onScanFinished) + useIpcEvent('scan:error', onScanError) + useIpcEvent('download:progress', onDownloadProgress) + useIpcEvent('download:status', onDownloadStatus) + useIpcEvent('download:finished', onDownloadFinished) + useIpcEvent('log:message', onLogMessage) + + // Create a default task on mount if none exists + useEffect(() => { + const state = useTaskStore.getState() + if (Object.keys(state.tasks).length === 0) { + ;(window.api.task.create('') as Promise).then((task) => { + useTaskStore.getState().createTask(task.id, task.url) + }) + } + }, []) +} diff --git a/electron-app/src/renderer/src/lib/utils.ts b/electron-app/src/renderer/src/lib/utils.ts new file mode 100644 index 0000000..d32b0fe --- /dev/null +++ b/electron-app/src/renderer/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/electron-app/src/renderer/src/main.tsx b/electron-app/src/renderer/src/main.tsx new file mode 100644 index 0000000..f4d40c7 --- /dev/null +++ b/electron-app/src/renderer/src/main.tsx @@ -0,0 +1,11 @@ +import './assets/main.css' + +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + +) diff --git a/electron-app/src/renderer/src/stores/task-store.ts b/electron-app/src/renderer/src/stores/task-store.ts new file mode 100644 index 0000000..0437fbc --- /dev/null +++ b/electron-app/src/renderer/src/stores/task-store.ts @@ -0,0 +1,232 @@ +import { create } from 'zustand' +import type { Task, TreeNode, DownloadItem } from '../../../shared/types' + +function createEmptyTask(id: string, url: string): Task { + return { + id, + url, + status: 'idle', + nodes: {}, + roots: [], + checkedFiles: [], + downloads: {}, + scanProgress: { scanned: 0, total: 0 }, + downloadPath: '' + } +} + +type SortKey = 'name' | 'size' | 'type' | 'none' +type SortDir = 'asc' | 'desc' + +interface TaskStore { + tasks: Record + activeTaskId: string | null + previewNodeId: string | null + sortKey: SortKey + sortDir: SortDir + + // Preview & Sort + setPreviewNodeId: (nodeId: string | null) => void + setSort: (key: SortKey) => void + + // Task management + createTask: (id: string, url: string) => void + removeTask: (taskId: string) => void + setActiveTask: (taskId: string) => void + updateTask: (taskId: string, partial: Partial) => void + + // Tree operations + getNodes: (taskId: string) => Record + addNode: (taskId: string, node: TreeNode) => void + toggleCheck: (taskId: string, nodeId: string) => void + toggleExpand: (taskId: string, nodeId: string) => void + setHidden: (taskId: string, nodeId: string, hidden: boolean) => void + + // Download operations + updateDownload: (taskId: string, downloadId: string, partial: Partial) => void + setDownloads: (taskId: string, downloads: Record) => void +} + +function cascadeCheck(nodes: Record, nodeId: string, checked: boolean): void { + const node = nodes[nodeId] + if (!node) return + node.checked = checked + for (const childId of node.children) { + cascadeCheck(nodes, childId, checked) + } +} + +export const useTaskStore = create((set, get) => ({ + tasks: {}, + activeTaskId: null, + previewNodeId: null, + sortKey: 'name' as SortKey, + sortDir: 'asc' as SortDir, + + setPreviewNodeId: (nodeId) => set({ previewNodeId: nodeId }), + setSort: (key) => set((state) => ({ + sortKey: key, + sortDir: state.sortKey === key && state.sortDir === 'asc' ? 'desc' : 'asc', + })), + + createTask: (id, url) => { + set((state) => ({ + tasks: { ...state.tasks, [id]: createEmptyTask(id, url) }, + activeTaskId: id + })) + }, + + removeTask: (taskId) => { + set((state) => { + const { [taskId]: _, ...remaining } = state.tasks + const ids = Object.keys(remaining) + const newActive = + state.activeTaskId === taskId ? (ids.length > 0 ? ids[ids.length - 1] : null) : state.activeTaskId + return { tasks: remaining, activeTaskId: newActive } + }) + }, + + setActiveTask: (taskId) => { + set({ activeTaskId: taskId }) + }, + + updateTask: (taskId, partial) => { + set((state) => { + const task = state.tasks[taskId] + if (!task) return state + return { + tasks: { ...state.tasks, [taskId]: { ...task, ...partial } } + } + }) + }, + + getNodes: (taskId) => { + const task = get().tasks[taskId] + return task ? task.nodes : {} + }, + + addNode: (taskId, node) => { + set((state) => { + const task = state.tasks[taskId] + if (!task) return state + + // Folders should be expanded by default so the tree is visible + const nodeToAdd = node.kind === 'folder' ? { ...node, expanded: true } : node + + const updatedNodes = { ...task.nodes, [nodeToAdd.id]: nodeToAdd } + + // Add to parent's children array if it has a parent + if (nodeToAdd.parentId && updatedNodes[nodeToAdd.parentId]) { + const parent = updatedNodes[nodeToAdd.parentId] + if (!parent.children.includes(nodeToAdd.id)) { + updatedNodes[nodeToAdd.parentId] = { + ...parent, + children: [...parent.children, nodeToAdd.id] + } + } + } + + const updatedRoots = nodeToAdd.parentId === '' && !task.roots.includes(nodeToAdd.id) + ? [...task.roots, nodeToAdd.id] + : task.roots + + return { + tasks: { + ...state.tasks, + [taskId]: { ...task, nodes: updatedNodes, roots: updatedRoots } + } + } + }) + }, + + toggleCheck: (taskId, nodeId) => { + set((state) => { + const task = state.tasks[taskId] + if (!task) return state + const node = task.nodes[nodeId] + if (!node) return state + + const newNodes = { ...task.nodes } + // Deep copy nodes that will be modified + for (const id of Object.keys(newNodes)) { + newNodes[id] = { ...newNodes[id] } + } + cascadeCheck(newNodes, nodeId, !node.checked) + + // Recalculate checkedFiles + const checkedFiles = Object.values(newNodes) + .filter((n) => n.kind === 'file' && n.checked) + .map((n) => n.id) + + return { + tasks: { + ...state.tasks, + [taskId]: { ...task, nodes: newNodes, checkedFiles } + } + } + }) + }, + + toggleExpand: (taskId, nodeId) => { + set((state) => { + const task = state.tasks[taskId] + if (!task) return state + const node = task.nodes[nodeId] + if (!node) return state + return { + tasks: { + ...state.tasks, + [taskId]: { + ...task, + nodes: { ...task.nodes, [nodeId]: { ...node, expanded: !node.expanded } } + } + } + } + }) + }, + + setHidden: (taskId, nodeId, hidden) => { + set((state) => { + const task = state.tasks[taskId] + if (!task) return state + const node = task.nodes[nodeId] + if (!node) return state + return { + tasks: { + ...state.tasks, + [taskId]: { + ...task, + nodes: { ...task.nodes, [nodeId]: { ...node, hidden } } + } + } + } + }) + }, + + updateDownload: (taskId, downloadId, partial) => { + set((state) => { + const task = state.tasks[taskId] + if (!task) return state + const existing = task.downloads[downloadId] ?? ({} as DownloadItem) + return { + tasks: { + ...state.tasks, + [taskId]: { + ...task, + downloads: { ...task.downloads, [downloadId]: { ...existing, ...partial } } + } + } + } + }) + }, + + setDownloads: (taskId, downloads) => { + set((state) => { + const task = state.tasks[taskId] + if (!task) return state + return { + tasks: { ...state.tasks, [taskId]: { ...task, downloads } } + } + }) + } +})) diff --git a/electron-app/src/shared/icons.ts b/electron-app/src/shared/icons.ts new file mode 100644 index 0000000..e7e0bb2 --- /dev/null +++ b/electron-app/src/shared/icons.ts @@ -0,0 +1,31 @@ +// Pure implementation to avoid Node.js 'path' in browser context +function extname(filename: string): string { + const dot = filename.lastIndexOf('.') + return dot > 0 && dot < filename.length - 1 ? filename.slice(dot) : '' +} + +const IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.ico'] +const DOC_EXTS = ['.md', '.txt', '.pdf', '.doc', '.docx', '.rtf'] +const ARCHIVE_EXTS = ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz'] +const CODE_EXTS = [ + '.py', '.js', '.ts', '.tsx', '.jsx', '.go', '.rs', '.java', '.c', '.cpp', + '.h', '.hpp', '.json', '.yaml', '.yml', '.toml', '.xml', '.html', '.css', '.sh', +] + +export function getIconGroup(fileName: string, mimeType: string): string { + const ext = extname(fileName).toLowerCase() + if (IMAGE_EXTS.includes(ext)) return 'image' + if (DOC_EXTS.includes(ext)) return 'document' + if (ARCHIVE_EXTS.includes(ext)) return 'archive' + if (CODE_EXTS.includes(ext)) return 'code' + if (mimeType.includes('audio/')) return 'audio' + if (mimeType.includes('video/')) return 'video' + if (mimeType.includes('text/')) return 'text' + if (mimeType.includes('image/')) return 'image' + return 'binary' +} + +export const EMOJI_ICONS: Record = { + folder: '📁', image: '🖼️', document: '📄', archive: '🗜️', + code: '💻', audio: '🎵', video: '🎬', text: '📝', binary: '⚙️', +} diff --git a/electron-app/src/shared/types.ts b/electron-app/src/shared/types.ts new file mode 100644 index 0000000..992a554 --- /dev/null +++ b/electron-app/src/shared/types.ts @@ -0,0 +1,42 @@ +export interface TreeNode { + id: string + parentId: string // "" for roots + name: string + kind: 'folder' | 'file' + url: string // download URL (files only, "" for folders) + fullPath: string // relative path + size: string // "245 KB" or "Unknown" + fileType: string // MIME type + iconGroup: string // folder|image|document|archive|code|audio|video|text|binary + checked: boolean + expanded: boolean + hidden: boolean // filtered out by search/type + children: string[] // child node IDs +} + +export interface Task { + id: string + url: string + status: 'idle' | 'scanning' | 'scanned' | 'downloading' | 'done' | 'error' | 'cancelled' + nodes: Record + roots: string[] + checkedFiles: string[] + downloads: Record + scanProgress: { scanned: number; total: number } + downloadPath: string +} + +export interface DownloadItem { + id: string + fileName: string + url: string + destPath: string + status: 'queued' | 'downloading' | 'paused' | 'completed' | 'failed' | 'cancelled' + progress: number + speed: number + totalSize: number + downloadedSize: number +} + +export type ScanStatus = Task['status'] +export type DownloadStatus = DownloadItem['status'] diff --git a/electron-app/tsconfig.json b/electron-app/tsconfig.json new file mode 100644 index 0000000..b1d115b --- /dev/null +++ b/electron-app/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.web.json" + } + ] +} diff --git a/electron-app/tsconfig.node.json b/electron-app/tsconfig.node.json new file mode 100644 index 0000000..b6dd582 --- /dev/null +++ b/electron-app/tsconfig.node.json @@ -0,0 +1,16 @@ +{ + "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", + "include": [ + "electron.vite.config.*", + "src/main/**/*", + "src/preload/**/*", + "src/shared/**/*" + ], + "compilerOptions": { + "composite": true, + "moduleResolution": "bundler", + "types": [ + "electron-vite/node" + ] + } +} diff --git a/electron-app/tsconfig.web.json b/electron-app/tsconfig.web.json new file mode 100644 index 0000000..71b491d --- /dev/null +++ b/electron-app/tsconfig.web.json @@ -0,0 +1,23 @@ +{ + "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", + "include": [ + "src/renderer/src/**/*", + "src/renderer/src/**/*.d.ts", + "src/shared/**/*" + ], + "compilerOptions": { + "composite": true, + "baseUrl": ".", + "paths": { + "@renderer/*": [ + "src/renderer/src/*" + ], + "@/*": [ + "src/renderer/src/*" + ], + "@shared/*": [ + "src/shared/*" + ] + } + } +} diff --git a/electron-app/vitest.config.ts b/electron-app/vitest.config.ts new file mode 100644 index 0000000..e646338 --- /dev/null +++ b/electron-app/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/__tests__/**/*.test.ts', 'src/**/*.test.ts'], + }, +})