forked from akai/readest
feat: add fuzzy search for searching across all settings (#3085)
* feat: enable linking to settings items * feat: integrate command palette with global state and styles * feat: add command pallete ui and fuzzy search * feat: add command palette with global state and styles * feat: add command registry and search dependencies * fix: open command palette with shortcuts in reader page --------- Co-authored-by: Huang Xin <chrox.huang@gmail.com>
This commit is contained in:
@@ -109,6 +109,7 @@
|
||||
"dompurify": "^3.3.0",
|
||||
"foliate-js": "workspace:*",
|
||||
"franc-min": "^6.2.0",
|
||||
"fzf": "^0.5.2",
|
||||
"google-auth-library": "^10.5.0",
|
||||
"googleapis": "^164.1.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
|
||||
@@ -982,5 +982,21 @@
|
||||
"Reading Ruler": "مسطرة القراءة",
|
||||
"Enable Reading Ruler": "تفعيل مسطرة القراءة",
|
||||
"Lines to Highlight": "الأسطر المراد تمييزها",
|
||||
"Ruler Color": "لون المسطرة"
|
||||
"Ruler Color": "لون المسطرة",
|
||||
"Command Palette": "لوحة الأوامر",
|
||||
"Search settings and actions...": "البحث في الإعدادات والإجراءات...",
|
||||
"No results found for": "لم يتم العثور على نتائج لـ",
|
||||
"Type to search settings and actions": "اكتب للبحث في الإعدادات والإجراءات",
|
||||
"Recent": "الأخير",
|
||||
"navigate": "التنقل",
|
||||
"select": "اختيار",
|
||||
"close": "إغلاق",
|
||||
"Search Settings": "البحث في الإعدادات",
|
||||
"Page Margins": "هوامش الصفحة",
|
||||
"AI Provider": "مزود الذكاء الاصطناعي",
|
||||
"Ollama URL": "رابط Ollama",
|
||||
"Ollama Model": "نموذج Ollama",
|
||||
"AI Gateway Model": "نموذج بوابة الذكاء الاصطناعي",
|
||||
"Actions": "الإجراءات",
|
||||
"Navigation": "التنقل"
|
||||
}
|
||||
|
||||
@@ -942,5 +942,21 @@
|
||||
"Reading Ruler": "রিডিং রুলার",
|
||||
"Enable Reading Ruler": "রিডিং রুলার সক্ষম করুন",
|
||||
"Lines to Highlight": "হাইলাইট করার জন্য লাইন",
|
||||
"Ruler Color": "রুলারের রঙ"
|
||||
"Ruler Color": "রুলারের রঙ",
|
||||
"Command Palette": "কমান্ড প্যালেট",
|
||||
"Search settings and actions...": "সেটিংস এবং অ্যাকশন খুঁজুন...",
|
||||
"No results found for": "এর জন্য কোনো ফলাফল পাওয়া যায়নি",
|
||||
"Type to search settings and actions": "সেটিংস এবং অ্যাকশন খুঁজতে টাইপ করুন",
|
||||
"Recent": "সাম্প্রতিক",
|
||||
"navigate": "নেভিগেট",
|
||||
"select": "নির্বাচন করুন",
|
||||
"close": "বন্ধ করুন",
|
||||
"Search Settings": "সেটিংস খুঁজুন",
|
||||
"Page Margins": "পৃষ্ঠার মার্জিন",
|
||||
"AI Provider": "AI প্রদানকারী",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama মডেল",
|
||||
"AI Gateway Model": "AI গেটওয়ে মডেল",
|
||||
"Actions": "অ্যাকশন",
|
||||
"Navigation": "নেভিগেশন"
|
||||
}
|
||||
|
||||
@@ -932,5 +932,21 @@
|
||||
"Reading Ruler": "ལྟ་ཀློག་ཐིག་ཤིང་།",
|
||||
"Enable Reading Ruler": "ལྟ་ཀློག་ཐིག་ཤིང་སྤྱོད་པ།",
|
||||
"Lines to Highlight": "མངོན་གསལ་དུ་གཏོང་དགོས་པའི་ཐིག་ཕྲེང་།",
|
||||
"Ruler Color": "ཐིག་ཤིང་ගི་ཚོན་མདོག"
|
||||
"Ruler Color": "ཐིག་ཤིང་ගི་ཚོན་མདོག",
|
||||
"Command Palette": "བཀོད་འདོམས་པང་ལེབ།",
|
||||
"Search settings and actions...": "སྒྲིག་བཀོད་དང་བྱ་འགུལ་འཚོལ་བ།...",
|
||||
"No results found for": "འབྲས་བུ་མ་རྙེད་པ།",
|
||||
"Type to search settings and actions": "ཡིག་གཟུགས་ནང་འཇུག་བྱས་ཏེ་སྒྲིག་བཀོད་དང་བྱ་འགུལ་འཚོལ་བ།",
|
||||
"Recent": "ཉེ་ཆར།",
|
||||
"navigate": "འགྲུལ་བཞུད།",
|
||||
"select": "འདེམས་པ།",
|
||||
"close": "ཁ་རྒྱག་པ།",
|
||||
"Search Settings": "སྒྲིག་བཀོད་འཚོལ་བ།",
|
||||
"Page Margins": "ཤོག་ངོས་མཐའ་འགྲམ།",
|
||||
"AI Provider": "ནུས་པའི་རིག་རྩལ་མཁོ་སྤྲོད་པ།",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama དཔེ་གཞི།",
|
||||
"AI Gateway Model": "AI Gateway དཔེ་གཞི།",
|
||||
"Actions": "བྱ་འགུལ།",
|
||||
"Navigation": "འགྲུལ་བཞུད།"
|
||||
}
|
||||
|
||||
@@ -942,5 +942,21 @@
|
||||
"Reading Ruler": "Lese-Lineal",
|
||||
"Enable Reading Ruler": "Lese-Lineal aktivieren",
|
||||
"Lines to Highlight": "Hervorzuhebende Zeilen",
|
||||
"Ruler Color": "Linealfarbe"
|
||||
"Ruler Color": "Linealfarbe",
|
||||
"Command Palette": "Befehlspalette",
|
||||
"Search settings and actions...": "Einstellungen und Aktionen suchen...",
|
||||
"No results found for": "Keine Ergebnisse gefunden für",
|
||||
"Type to search settings and actions": "Tippen, um Einstellungen und Aktionen zu suchen",
|
||||
"Recent": "Zuletzt verwendet",
|
||||
"navigate": "navigieren",
|
||||
"select": "auswählen",
|
||||
"close": "schließen",
|
||||
"Search Settings": "Einstellungen suchen",
|
||||
"Page Margins": "Seitenränder",
|
||||
"AI Provider": "KI-Anbieter",
|
||||
"Ollama URL": "Ollama-URL",
|
||||
"Ollama Model": "Ollama-Modell",
|
||||
"AI Gateway Model": "AI Gateway-Modell",
|
||||
"Actions": "Aktionen",
|
||||
"Navigation": "Navigation"
|
||||
}
|
||||
|
||||
@@ -942,5 +942,21 @@
|
||||
"Reading Ruler": "Χάρακας ανάγνωσης",
|
||||
"Enable Reading Ruler": "Ενεργοποίηση χάρακα ανάγνωσης",
|
||||
"Lines to Highlight": "Γραμμές για επισήμανση",
|
||||
"Ruler Color": "Χρώμα χάρακα"
|
||||
"Ruler Color": "Χρώμα χάρακα",
|
||||
"Command Palette": "Παλέτα εντολών",
|
||||
"Search settings and actions...": "Αναζήτηση ρυθμίσεων και ενεργειών...",
|
||||
"No results found for": "Δεν βρέθηκαν αποτελέσματα για",
|
||||
"Type to search settings and actions": "Πληκτρολογήστε για αναζήτηση ρυθμίσεων και ενεργειών",
|
||||
"Recent": "Πρόσφατα",
|
||||
"navigate": "πλοήγηση",
|
||||
"select": "επιλογή",
|
||||
"close": "κλείσιμο",
|
||||
"Search Settings": "Αναζήτηση ρυθμίσεων",
|
||||
"Page Margins": "Περιθώρια σελίδας",
|
||||
"AI Provider": "Πάροχος AI",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Μοντέλο Ollama",
|
||||
"AI Gateway Model": "Μοντέλο AI Gateway",
|
||||
"Actions": "Ενέργειες",
|
||||
"Navigation": "Πλοήγηση"
|
||||
}
|
||||
|
||||
@@ -952,5 +952,21 @@
|
||||
"Reading Ruler": "Regla de lectura",
|
||||
"Enable Reading Ruler": "Habilitar regla de lectura",
|
||||
"Lines to Highlight": "Líneas a resaltar",
|
||||
"Ruler Color": "Color de la regla"
|
||||
"Ruler Color": "Color de la regla",
|
||||
"Command Palette": "Paleta de comandos",
|
||||
"Search settings and actions...": "Buscar ajustes y acciones...",
|
||||
"No results found for": "No se encontraron resultados para",
|
||||
"Type to search settings and actions": "Escribe para buscar ajustes y acciones",
|
||||
"Recent": "Reciente",
|
||||
"navigate": "navegar",
|
||||
"select": "seleccionar",
|
||||
"close": "cerrar",
|
||||
"Search Settings": "Buscar ajustes",
|
||||
"Page Margins": "Márgenes de página",
|
||||
"AI Provider": "Proveedor de IA",
|
||||
"Ollama URL": "URL de Ollama",
|
||||
"Ollama Model": "Modelo de Ollama",
|
||||
"AI Gateway Model": "Modelo de AI Gateway",
|
||||
"Actions": "Acciones",
|
||||
"Navigation": "Navegación"
|
||||
}
|
||||
|
||||
@@ -942,5 +942,21 @@
|
||||
"Reading Ruler": "خطکش مطالعه",
|
||||
"Enable Reading Ruler": "فعالسازی خطکش مطالعه",
|
||||
"Lines to Highlight": "خطوط برای تمایز",
|
||||
"Ruler Color": "رنگ خطکش"
|
||||
"Ruler Color": "رنگ خطکش",
|
||||
"Command Palette": "پالت فرمان",
|
||||
"Search settings and actions...": "جستجوی تنظیمات و عملیات...",
|
||||
"No results found for": "نتیجهای برای این مورد یافت نشد",
|
||||
"Type to search settings and actions": "برای جستجوی تنظیمات و عملیات تایپ کنید",
|
||||
"Recent": "اخیر",
|
||||
"navigate": "ناوبری",
|
||||
"select": "انتخاب",
|
||||
"close": "بستن",
|
||||
"Search Settings": "جستجوی تنظیمات",
|
||||
"Page Margins": "حاشیههای صفحه",
|
||||
"AI Provider": "ارائه دهنده هوش مصنوعی",
|
||||
"Ollama URL": "آدرس Ollama",
|
||||
"Ollama Model": "مدل Ollama",
|
||||
"AI Gateway Model": "مدل AI Gateway",
|
||||
"Actions": "عملیات",
|
||||
"Navigation": "ناوبری"
|
||||
}
|
||||
|
||||
@@ -952,5 +952,21 @@
|
||||
"Reading Ruler": "Règle de lecture",
|
||||
"Enable Reading Ruler": "Activer la règle de lecture",
|
||||
"Lines to Highlight": "Lignes à mettre en évidence",
|
||||
"Ruler Color": "Couleur de la règle"
|
||||
"Ruler Color": "Couleur de la règle",
|
||||
"Command Palette": "Palette de commandes",
|
||||
"Search settings and actions...": "Rechercher des paramètres et des actions...",
|
||||
"No results found for": "Aucun résultat trouvé pour",
|
||||
"Type to search settings and actions": "Saisissez pour rechercher des paramètres et des actions",
|
||||
"Recent": "Récent",
|
||||
"navigate": "naviguer",
|
||||
"select": "sélectionner",
|
||||
"close": "fermer",
|
||||
"Search Settings": "Rechercher dans les paramètres",
|
||||
"Page Margins": "Marges de page",
|
||||
"AI Provider": "Fournisseur d'IA",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Modèle Ollama",
|
||||
"AI Gateway Model": "Modèle AI Gateway",
|
||||
"Actions": "Actions",
|
||||
"Navigation": "Navigation"
|
||||
}
|
||||
|
||||
@@ -942,5 +942,21 @@
|
||||
"Reading Ruler": "पठन रूलर",
|
||||
"Enable Reading Ruler": "पठन रूलर सक्षम करें",
|
||||
"Lines to Highlight": "हाइलाइट करने के लिए पंक्तियाँ",
|
||||
"Ruler Color": "रूलर का रंग"
|
||||
"Ruler Color": "रूलर का रंग",
|
||||
"Command Palette": "कमांड पैलेट",
|
||||
"Search settings and actions...": "सेटिंग और क्रियाएं खोजें...",
|
||||
"No results found for": "के लिए कोई परिणाम नहीं मिला",
|
||||
"Type to search settings and actions": "सेटिंग और क्रियाएं खोजने के लिए टाइप करें",
|
||||
"Recent": "हालिया",
|
||||
"navigate": "नेविगेट",
|
||||
"select": "चुनें",
|
||||
"close": "बंद करें",
|
||||
"Search Settings": "सेटिंग खोजें",
|
||||
"Page Margins": "पेज मार्जिन",
|
||||
"AI Provider": "AI प्रदाता",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama मॉडल",
|
||||
"AI Gateway Model": "AI गेटवे मॉडल",
|
||||
"Actions": "क्रियाएं",
|
||||
"Navigation": "नेविगेशन"
|
||||
}
|
||||
|
||||
@@ -932,5 +932,21 @@
|
||||
"Reading Ruler": "Penggaris Membaca",
|
||||
"Enable Reading Ruler": "Aktifkan Penggaris Membaca",
|
||||
"Lines to Highlight": "Baris untuk Disorot",
|
||||
"Ruler Color": "Warna Penggaris"
|
||||
"Ruler Color": "Warna Penggaris",
|
||||
"Command Palette": "Palet Perintah",
|
||||
"Search settings and actions...": "Cari pengaturan dan tindakan...",
|
||||
"No results found for": "Tidak ada hasil ditemukan untuk",
|
||||
"Type to search settings and actions": "Ketik untuk mencari pengaturan dan tindakan",
|
||||
"Recent": "Terbaru",
|
||||
"navigate": "navigasi",
|
||||
"select": "pilih",
|
||||
"close": "tutup",
|
||||
"Search Settings": "Cari Pengaturan",
|
||||
"Page Margins": "Margin Halaman",
|
||||
"AI Provider": "Penyedia AI",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Model Ollama",
|
||||
"AI Gateway Model": "Model AI Gateway",
|
||||
"Actions": "Tindakan",
|
||||
"Navigation": "Navigasi"
|
||||
}
|
||||
|
||||
@@ -952,5 +952,21 @@
|
||||
"Reading Ruler": "Righello di lettura",
|
||||
"Enable Reading Ruler": "Abilita righello di lettura",
|
||||
"Lines to Highlight": "Righe da evidenziare",
|
||||
"Ruler Color": "Colore del righello"
|
||||
"Ruler Color": "Colore del righello",
|
||||
"Command Palette": "Tavolozza dei comandi",
|
||||
"Search settings and actions...": "Cerca impostazioni e azioni...",
|
||||
"No results found for": "Nessun risultato trovato per",
|
||||
"Type to search settings and actions": "Digita per cercare impostazioni e azioni",
|
||||
"Recent": "Recenti",
|
||||
"navigate": "naviga",
|
||||
"select": "seleziona",
|
||||
"close": "chiudi",
|
||||
"Search Settings": "Cerca impostazioni",
|
||||
"Page Margins": "Margini della pagina",
|
||||
"AI Provider": "Fornitore AI",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Modello Ollama",
|
||||
"AI Gateway Model": "Modello AI Gateway",
|
||||
"Actions": "Azioni",
|
||||
"Navigation": "Navigazione"
|
||||
}
|
||||
|
||||
@@ -932,5 +932,21 @@
|
||||
"Reading Ruler": "リーディングルーラー",
|
||||
"Enable Reading Ruler": "リーディングルーラーを有効にする",
|
||||
"Lines to Highlight": "ハイライトする行数",
|
||||
"Ruler Color": "ルーラーの色"
|
||||
"Ruler Color": "ルーラーの色",
|
||||
"Command Palette": "コマンドパレット",
|
||||
"Search settings and actions...": "設定とアクションを検索...",
|
||||
"No results found for": "の結果が見つかりませんでした",
|
||||
"Type to search settings and actions": "入力して設定とアクションを検索",
|
||||
"Recent": "最近使った項目",
|
||||
"navigate": "ナビゲート",
|
||||
"select": "選択",
|
||||
"close": "閉じる",
|
||||
"Search Settings": "設定を検索",
|
||||
"Page Margins": "ページ余白",
|
||||
"AI Provider": "AIプロバイダー",
|
||||
"Ollama URL": "OllamaのURL",
|
||||
"Ollama Model": "Ollamaモデル",
|
||||
"AI Gateway Model": "AI Gatewayモデル",
|
||||
"Actions": "アクション",
|
||||
"Navigation": "ナビゲーション"
|
||||
}
|
||||
|
||||
@@ -932,5 +932,21 @@
|
||||
"Reading Ruler": "읽기 자",
|
||||
"Enable Reading Ruler": "읽기 자 활성화",
|
||||
"Lines to Highlight": "강조할 줄 수",
|
||||
"Ruler Color": "자 색상"
|
||||
"Ruler Color": "자 색상",
|
||||
"Command Palette": "명령 팔레트",
|
||||
"Search settings and actions...": "설정 및 작업 검색...",
|
||||
"No results found for": "에 대한 결과를 찾을 수 없음",
|
||||
"Type to search settings and actions": "입력하여 설정 및 작업 검색",
|
||||
"Recent": "최근",
|
||||
"navigate": "탐색",
|
||||
"select": "선택",
|
||||
"close": "닫기",
|
||||
"Search Settings": "설정 검색",
|
||||
"Page Margins": "페이지 여백",
|
||||
"AI Provider": "AI 제공처",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama 모델",
|
||||
"AI Gateway Model": "AI 게이트웨이 모델",
|
||||
"Actions": "작업",
|
||||
"Navigation": "탐색"
|
||||
}
|
||||
|
||||
@@ -932,5 +932,21 @@
|
||||
"Reading Ruler": "Pembaris Membaca",
|
||||
"Enable Reading Ruler": "Dayakan Pembaris Membaca",
|
||||
"Lines to Highlight": "Baris untuk Diserlahkan",
|
||||
"Ruler Color": "Warna Pembaris"
|
||||
"Ruler Color": "Warna Pembaris",
|
||||
"Command Palette": "Palet Perintah",
|
||||
"Search settings and actions...": "Cari tetapan dan tindakan...",
|
||||
"No results found for": "Tiada hasil dijumpai untuk",
|
||||
"Type to search settings and actions": "Taip untuk mencari tetapan dan tindakan",
|
||||
"Recent": "Baru-baru ini",
|
||||
"navigate": "navigasi",
|
||||
"select": "pilih",
|
||||
"close": "tutup",
|
||||
"Search Settings": "Cari Tetapan",
|
||||
"Page Margins": "Margin Halaman",
|
||||
"AI Provider": "Penyedia AI",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Model Ollama",
|
||||
"AI Gateway Model": "Model AI Gateway",
|
||||
"Actions": "Tindakan",
|
||||
"Navigation": "Navigasi"
|
||||
}
|
||||
|
||||
@@ -942,5 +942,21 @@
|
||||
"Reading Ruler": "Leesliniaal",
|
||||
"Enable Reading Ruler": "Leesliniaal inschakelen",
|
||||
"Lines to Highlight": "Regels om te markeren",
|
||||
"Ruler Color": "Kleur van liniaal"
|
||||
"Ruler Color": "Kleur van liniaal",
|
||||
"Command Palette": "Opdrachtenpalet",
|
||||
"Search settings and actions...": "Zoek instellingen en acties...",
|
||||
"No results found for": "Geen resultaten gevonden voor",
|
||||
"Type to search settings and actions": "Typ om instellingen en acties te zoeken",
|
||||
"Recent": "Recent",
|
||||
"navigate": "navigeren",
|
||||
"select": "selecteren",
|
||||
"close": "sluiten",
|
||||
"Search Settings": "Instellingen zoeken",
|
||||
"Page Margins": "Paginamarges",
|
||||
"AI Provider": "AI-provider",
|
||||
"Ollama URL": "Ollama-URL",
|
||||
"Ollama Model": "Ollama-model",
|
||||
"AI Gateway Model": "AI Gateway-model",
|
||||
"Actions": "Acties",
|
||||
"Navigation": "Navigatie"
|
||||
}
|
||||
|
||||
@@ -962,5 +962,21 @@
|
||||
"Reading Ruler": "Linijka do czytania",
|
||||
"Enable Reading Ruler": "Włącz linijkę do czytania",
|
||||
"Lines to Highlight": "Linie do wyróżnienia",
|
||||
"Ruler Color": "Kolor linijki"
|
||||
"Ruler Color": "Kolor linijki",
|
||||
"Command Palette": "Paleta poleceń",
|
||||
"Search settings and actions...": "Szukaj ustawień i akcji...",
|
||||
"No results found for": "Brak wyników dla",
|
||||
"Type to search settings and actions": "Wpisz, aby szukać ustawień i akcji",
|
||||
"Recent": "Ostatnie",
|
||||
"navigate": "nawiguj",
|
||||
"select": "wybierz",
|
||||
"close": "zamknij",
|
||||
"Search Settings": "Szukaj ustawień",
|
||||
"Page Margins": "Marginesy strony",
|
||||
"AI Provider": "Dostawca AI",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Model Ollama",
|
||||
"AI Gateway Model": "Model AI Gateway",
|
||||
"Actions": "Akcje",
|
||||
"Navigation": "Nawigacja"
|
||||
}
|
||||
|
||||
@@ -952,5 +952,21 @@
|
||||
"Reading Ruler": "Régua de leitura",
|
||||
"Enable Reading Ruler": "Ativar régua de leitura",
|
||||
"Lines to Highlight": "Linhas para destacar",
|
||||
"Ruler Color": "Cor da régua"
|
||||
"Ruler Color": "Cor da régua",
|
||||
"Command Palette": "Paleta de Comandos",
|
||||
"Search settings and actions...": "Pesquisar definições e ações...",
|
||||
"No results found for": "Nenhum resultado encontrado para",
|
||||
"Type to search settings and actions": "Digite para pesquisar definições e ações",
|
||||
"Recent": "Recente",
|
||||
"navigate": "navegar",
|
||||
"select": "selecionar",
|
||||
"close": "fechar",
|
||||
"Search Settings": "Pesquisar Definições",
|
||||
"Page Margins": "Margens da Página",
|
||||
"AI Provider": "Fornecedor de IA",
|
||||
"Ollama URL": "URL do Ollama",
|
||||
"Ollama Model": "Modelo do Ollama",
|
||||
"AI Gateway Model": "Modelo de AI Gateway",
|
||||
"Actions": "Ações",
|
||||
"Navigation": "Navegação"
|
||||
}
|
||||
|
||||
@@ -962,5 +962,21 @@
|
||||
"Reading Ruler": "Линейка для чтения",
|
||||
"Enable Reading Ruler": "Включить линейку для чтения",
|
||||
"Lines to Highlight": "Строк для выделения",
|
||||
"Ruler Color": "Цвет линейки"
|
||||
"Ruler Color": "Цвет линейки",
|
||||
"Command Palette": "Палитра команд",
|
||||
"Search settings and actions...": "Поиск настроек и действий...",
|
||||
"No results found for": "Результатов не найдено для",
|
||||
"Type to search settings and actions": "Введите для поиска настроек и действий",
|
||||
"Recent": "Недавние",
|
||||
"navigate": "навигация",
|
||||
"select": "выбрать",
|
||||
"close": "закрыть",
|
||||
"Search Settings": "Поиск настроек",
|
||||
"Page Margins": "Поля страницы",
|
||||
"AI Provider": "Провайдер ИИ",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Модель Ollama",
|
||||
"AI Gateway Model": "Модель AI Gateway",
|
||||
"Actions": "Действия",
|
||||
"Navigation": "Навигация"
|
||||
}
|
||||
|
||||
@@ -942,5 +942,21 @@
|
||||
"Reading Ruler": "කියවීමේ රූල",
|
||||
"Enable Reading Ruler": "කියවීමේ රූල සක්රීය කරන්න",
|
||||
"Lines to Highlight": "මතුකර පෙන්විය යුතු පේළි",
|
||||
"Ruler Color": "රූලේ වර්ණය"
|
||||
"Ruler Color": "රූලේ වර්ණය",
|
||||
"Command Palette": "විධාන පුවරුව",
|
||||
"Search settings and actions...": "සැකසුම් සහ ක්රියා සොයන්න...",
|
||||
"No results found for": "සඳහා ප්රතිඵල හමු නොවීය",
|
||||
"Type to search settings and actions": "සැකසුම් සහ ක්රියා සෙවීමට ටයිප් කරන්න",
|
||||
"Recent": "මෑත කාලීන",
|
||||
"navigate": "සංචාලනය",
|
||||
"select": "තෝරන්න",
|
||||
"close": "වසා දමන්න",
|
||||
"Search Settings": "සැකසුම් සොයන්න",
|
||||
"Page Margins": "පිටු මායිම්",
|
||||
"AI Provider": "AI සපයන්නා",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama ආකෘතිය",
|
||||
"AI Gateway Model": "AI ਗੇਟਵੇ ਆਕෘතිය",
|
||||
"Actions": "ක්රියා",
|
||||
"Navigation": "සංචාලනය"
|
||||
}
|
||||
|
||||
@@ -942,5 +942,21 @@
|
||||
"Reading Ruler": "Läs-linjal",
|
||||
"Enable Reading Ruler": "Aktivera läs-linjal",
|
||||
"Lines to Highlight": "Rader att markera",
|
||||
"Ruler Color": "Linjalens färg"
|
||||
"Ruler Color": "Linjalens färg",
|
||||
"Command Palette": "Kommandopalett",
|
||||
"Search settings and actions...": "Sök inställningar och åtgärder...",
|
||||
"No results found for": "Inga resultat hittades för",
|
||||
"Type to search settings and actions": "Skriv för att söka inställningar och åtgärder",
|
||||
"Recent": "Senaste",
|
||||
"navigate": "navigera",
|
||||
"select": "välj",
|
||||
"close": "stäng",
|
||||
"Search Settings": "Sök inställningar",
|
||||
"Page Margins": "Sidmarginaler",
|
||||
"AI Provider": "AI-leverantör",
|
||||
"Ollama URL": "Ollama-URL",
|
||||
"Ollama Model": "Ollama-modell",
|
||||
"AI Gateway Model": "AI Gateway-modell",
|
||||
"Actions": "Åtgärder",
|
||||
"Navigation": "Navigering"
|
||||
}
|
||||
|
||||
@@ -942,5 +942,21 @@
|
||||
"Reading Ruler": "வாசிப்பு அளவுகோல்",
|
||||
"Enable Reading Ruler": "வாசிப்பு அளவுகோலை இயக்கு",
|
||||
"Lines to Highlight": "சிறப்பித்துக் காட்ட வேண்டிய வரிகள்",
|
||||
"Ruler Color": "அளவுகோல் வண்ணம்"
|
||||
"Ruler Color": "அளவுகோல் வண்ணம்",
|
||||
"Command Palette": "கட்டளை தட்டு",
|
||||
"Search settings and actions...": "அமைப்புகள் மற்றும் செயல்களைத் தேடுங்கள்...",
|
||||
"No results found for": "இதற்கு முடிவுகள் எதுவும் கிடைக்கவில்லை",
|
||||
"Type to search settings and actions": "அமைப்புகள் மற்றும் செயல்களைத் தேட தட்டச்சு செய்க",
|
||||
"Recent": "சமீபத்திய",
|
||||
"navigate": "வழிசெலுத்து",
|
||||
"select": "தேர்ந்தெடு",
|
||||
"close": "மூடு",
|
||||
"Search Settings": "அமைப்புகளைத் தேடு",
|
||||
"Page Margins": "பக்க விளிம்புகள்",
|
||||
"AI Provider": "AI வழங்குநர்",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama மாதிரி",
|
||||
"AI Gateway Model": "AI கேட்வே மாதிரி",
|
||||
"Actions": "செயல்கள்",
|
||||
"Navigation": "வழிசெலுத்தல்"
|
||||
}
|
||||
|
||||
@@ -932,5 +932,21 @@
|
||||
"Reading Ruler": "ไม้บรรทัดการอ่าน",
|
||||
"Enable Reading Ruler": "เปิดใช้งานไม้บรรทัดการอ่าน",
|
||||
"Lines to Highlight": "บรรทัดที่ต้องการเน้น",
|
||||
"Ruler Color": "สีไม้บรรทัด"
|
||||
"Ruler Color": "สีไม้บรรทัด",
|
||||
"Command Palette": "พาเลทคำสั่ง",
|
||||
"Search settings and actions...": "ค้นหาการตั้งค่าและคำสั่ง...",
|
||||
"No results found for": "ไม่พบผลลัพธ์สำหรับ",
|
||||
"Type to search settings and actions": "พิมพ์เพื่อค้นหาการตั้งค่าและคำสั่ง",
|
||||
"Recent": "ล่าสุด",
|
||||
"navigate": "นำทาง",
|
||||
"select": "เลือก",
|
||||
"close": "ปิด",
|
||||
"Search Settings": "ค้นหาการตั้งค่า",
|
||||
"Page Margins": "ระยะขอบหน้า",
|
||||
"AI Provider": "ผู้ให้บริการ AI",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "โมเดล Ollama",
|
||||
"AI Gateway Model": "โมเดล AI Gateway",
|
||||
"Actions": "การกระทำ",
|
||||
"Navigation": "การนำทาง"
|
||||
}
|
||||
|
||||
@@ -942,5 +942,21 @@
|
||||
"Reading Ruler": "Okuma Cetveli",
|
||||
"Enable Reading Ruler": "Okuma Cetvelini Etkinleştir",
|
||||
"Lines to Highlight": "Vurgulanacak Satırlar",
|
||||
"Ruler Color": "Cetvel Rengi"
|
||||
"Ruler Color": "Cetvel Rengi",
|
||||
"Command Palette": "Komut Paleti",
|
||||
"Search settings and actions...": "Ayarları ve eylemleri ara...",
|
||||
"No results found for": "Şunun için sonuç bulunamadı",
|
||||
"Type to search settings and actions": "Ayarları ve eylemleri aramak için yazın",
|
||||
"Recent": "Son kullanılanlar",
|
||||
"navigate": "gezin",
|
||||
"select": "seç",
|
||||
"close": "kapat",
|
||||
"Search Settings": "Ayarları Ara",
|
||||
"Page Margins": "Sayfa Kenar Boşlukları",
|
||||
"AI Provider": "Yapay Zeka Sağlayıcısı",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama Modeli",
|
||||
"AI Gateway Model": "AI Gateway Modeli",
|
||||
"Actions": "Eylemler",
|
||||
"Navigation": "Gezين"
|
||||
}
|
||||
|
||||
@@ -962,5 +962,21 @@
|
||||
"Reading Ruler": "Лінійка для читання",
|
||||
"Enable Reading Ruler": "Увімкнути лінійку для читання",
|
||||
"Lines to Highlight": "Рядки для виділення",
|
||||
"Ruler Color": "Коліර් лінійки"
|
||||
"Ruler Color": "Коліර් лінійки",
|
||||
"Command Palette": "Палітра команд",
|
||||
"Search settings and actions...": "Пошук налаштувань та дій...",
|
||||
"No results found for": "Результатів не знайдено для",
|
||||
"Type to search settings and actions": "Введіть для пошуку налаштувань та дій",
|
||||
"Recent": "Нещодавні",
|
||||
"navigate": "навігація",
|
||||
"select": "вибрати",
|
||||
"close": "закрити",
|
||||
"Search Settings": "Пошук налаштувань",
|
||||
"Page Margins": "Поля сторінки",
|
||||
"AI Provider": "Провайдер ШІ",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Модель Ollama",
|
||||
"AI Gateway Model": "Модель AI Gateway",
|
||||
"Actions": "Дії",
|
||||
"Navigation": "Навігація"
|
||||
}
|
||||
|
||||
@@ -932,5 +932,21 @@
|
||||
"Reading Ruler": "Thước đọc",
|
||||
"Enable Reading Ruler": "Bật thước đọc",
|
||||
"Lines to Highlight": "Số dòng cần làm nổi bật",
|
||||
"Ruler Color": "Màu thước"
|
||||
"Ruler Color": "Màu thước",
|
||||
"Command Palette": "Bảng lệnh",
|
||||
"Search settings and actions...": "Tìm kiếm cài đặt và hành động...",
|
||||
"No results found for": "Không tìm thấy kết quả cho",
|
||||
"Type to search settings and actions": "Nhập để tìm kiếm cài đặt và hành động",
|
||||
"Recent": "Gần đây",
|
||||
"navigate": "điều hướng",
|
||||
"select": "chọn",
|
||||
"close": "đóng",
|
||||
"Search Settings": "Tìm kiếm cài đặt",
|
||||
"Page Margins": "Lề trang",
|
||||
"AI Provider": "Nhà cung cấp AI",
|
||||
"Ollama URL": "URL Ollama",
|
||||
"Ollama Model": "Mô hình Ollama",
|
||||
"AI Gateway Model": "Mô hình AI Gateway",
|
||||
"Actions": "Hành động",
|
||||
"Navigation": "Điều hướng"
|
||||
}
|
||||
|
||||
@@ -932,5 +932,21 @@
|
||||
"Reading Ruler": "阅读尺",
|
||||
"Enable Reading Ruler": "启用阅读尺",
|
||||
"Lines to Highlight": "突出显示行数",
|
||||
"Ruler Color": "尺子颜色"
|
||||
"Ruler Color": "尺子颜色",
|
||||
"Command Palette": "命令面板",
|
||||
"Search settings and actions...": "搜索设置和操作...",
|
||||
"No results found for": "未找到结果:",
|
||||
"Type to search settings and actions": "输入以搜索设置和操作",
|
||||
"Recent": "最近",
|
||||
"navigate": "导航",
|
||||
"select": "选择",
|
||||
"close": "关闭",
|
||||
"Search Settings": "搜索设置",
|
||||
"Page Margins": "页边距",
|
||||
"AI Provider": "AI 服务商",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama 模型",
|
||||
"AI Gateway Model": "AI Gateway 模型",
|
||||
"Actions": "操作",
|
||||
"Navigation": "导航"
|
||||
}
|
||||
|
||||
@@ -932,5 +932,21 @@
|
||||
"Reading Ruler": "閱讀尺",
|
||||
"Enable Reading Ruler": "啟用閱讀尺",
|
||||
"Lines to Highlight": "突出顯示行數",
|
||||
"Ruler Color": "尺子顏色"
|
||||
"Ruler Color": "尺子顏色",
|
||||
"Command Palette": "命令面板",
|
||||
"Search settings and actions...": "搜尋設定和操作...",
|
||||
"No results found for": "未找到結果:",
|
||||
"Type to search settings and actions": "輸入以搜尋設定和操作",
|
||||
"Recent": "最近",
|
||||
"navigate": "導覽",
|
||||
"select": "選擇",
|
||||
"close": "關閉",
|
||||
"Search Settings": "搜尋設定",
|
||||
"Page Margins": "頁邊距",
|
||||
"AI Provider": "AI 服務商",
|
||||
"Ollama URL": "Ollama URL",
|
||||
"Ollama Model": "Ollama 模型",
|
||||
"AI Gateway Model": "AI Gateway 模型",
|
||||
"Actions": "操作",
|
||||
"Navigation": "導覽"
|
||||
}
|
||||
|
||||
@@ -5,11 +5,12 @@ import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { useCommandPalette } from '@/components/command-palette';
|
||||
import { tauriHandleClose, tauriHandleToggleFullScreen, tauriQuitApp } from '@/utils/window';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, ZOOM_STEP } from '@/services/constants';
|
||||
import { viewPagination } from './usePagination';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import useShortcuts from '@/hooks/useShortcuts';
|
||||
import useBooksManager from './useBooksManager';
|
||||
|
||||
@@ -25,6 +26,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
const { getBookData } = useBookDataStore();
|
||||
const { toggleNotebook } = useNotebookStore();
|
||||
const { getNextBookKey } = useBooksManager();
|
||||
const { open: openCommandPalette } = useCommandPalette();
|
||||
const viewSettings = getViewSettings(sideBarBookKey ?? '');
|
||||
const fontSize = viewSettings?.defaultFontSize ?? 16;
|
||||
const lineHeight = viewSettings?.lineHeight ?? 1.6;
|
||||
@@ -254,6 +256,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
onZoomIn: zoomIn,
|
||||
onZoomOut: zoomOut,
|
||||
onResetZoom: resetZoom,
|
||||
onOpenCommandPalette: openCommandPalette,
|
||||
},
|
||||
[sideBarBookKey, bookKeys],
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useBackgroundTexture } from '@/hooks/useBackgroundTexture';
|
||||
import { useEinkMode } from '@/hooks/useEinkMode';
|
||||
import { getLocale } from '@/utils/misc';
|
||||
import { getDirFromUILanguage } from '@/utils/rtl';
|
||||
import { CommandPaletteProvider, CommandPalette } from '@/components/command-palette';
|
||||
|
||||
const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
const { envConfig, appService } = useEnv();
|
||||
@@ -67,7 +68,12 @@ const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||
<CSPostHogProvider>
|
||||
<AuthProvider>
|
||||
<IconContext.Provider value={{ size: `${iconSize}px` }}>
|
||||
<SyncProvider>{children}</SyncProvider>
|
||||
<SyncProvider>
|
||||
<CommandPaletteProvider>
|
||||
{children}
|
||||
<CommandPalette />
|
||||
</CommandPaletteProvider>
|
||||
</SyncProvider>
|
||||
</IconContext.Provider>
|
||||
</AuthProvider>
|
||||
</CSPostHogProvider>
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useCallback, useMemo, useState } from 'react';
|
||||
import { FiSearch } from 'react-icons/fi';
|
||||
import { MdClose } from 'react-icons/md';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useCommandPalette } from './CommandPaletteProvider';
|
||||
import { CommandSearchResult, getCategoryLabel, CommandCategory } from '@/services/commandRegistry';
|
||||
import HighlightChars from './HighlightChars';
|
||||
|
||||
const CommandPalette: React.FC = () => {
|
||||
const _ = useTranslation();
|
||||
const { isOpen, close, query, setQuery, results, groupedResults, recentItems, executeCommand } =
|
||||
useCommandPalette();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const previousActiveElementRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
// flatten results for keyboard navigation
|
||||
const flattenedResults = useMemo(() => {
|
||||
if (query.trim()) {
|
||||
return results;
|
||||
}
|
||||
// show recent items when no query
|
||||
return recentItems.map((item) => ({
|
||||
item,
|
||||
score: 0,
|
||||
positions: new Set<number>(),
|
||||
highlightIndices: new Set<number>(),
|
||||
}));
|
||||
}, [query, results, recentItems]);
|
||||
|
||||
// reset selection when results change - legitimate derived state reset
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- resetting UI state on data change is intended
|
||||
setSelectedIndex(0);
|
||||
}, [flattenedResults.length]);
|
||||
|
||||
// focus input and reset selection on open
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
previousActiveElementRef.current = document.activeElement as HTMLElement;
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- resetting UI state on modal open is intended
|
||||
setSelectedIndex(0);
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 50);
|
||||
} else {
|
||||
if (previousActiveElementRef.current) {
|
||||
previousActiveElementRef.current.focus();
|
||||
previousActiveElementRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// scroll selected item into view
|
||||
useEffect(() => {
|
||||
if (!listRef.current) return;
|
||||
const selectedItem = listRef.current.querySelector('[data-selected="true"]');
|
||||
if (selectedItem) {
|
||||
selectedItem.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
setSelectedIndex((prev) => Math.min(prev + 1, flattenedResults.length - 1));
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
setSelectedIndex((prev) => Math.max(prev - 1, 0));
|
||||
break;
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
{
|
||||
const selected = flattenedResults[selectedIndex];
|
||||
if (selected) {
|
||||
executeCommand(selected.item);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
close();
|
||||
break;
|
||||
case 'Tab':
|
||||
// trap focus within the palette
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
},
|
||||
[flattenedResults, selectedIndex, executeCommand, close],
|
||||
);
|
||||
|
||||
const handleItemClick = useCallback(
|
||||
(result: CommandSearchResult) => {
|
||||
executeCommand(result.item);
|
||||
},
|
||||
[executeCommand],
|
||||
);
|
||||
|
||||
const handleBackdropClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
close();
|
||||
}
|
||||
},
|
||||
[close],
|
||||
);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const showEmptyState = query.trim() && flattenedResults.length === 0;
|
||||
const showRecentState = !query.trim() && recentItems.length > 0;
|
||||
const showNoRecentState = !query.trim() && recentItems.length === 0;
|
||||
|
||||
// group results by category for display
|
||||
const orderedCategories: CommandCategory[] = ['settings', 'actions', 'navigation'];
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div
|
||||
className='fixed inset-0 z-[100] flex items-start justify-center bg-black/50 pt-[15vh]'
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
|
||||
<div
|
||||
className='bg-base-100 w-full max-w-lg overflow-hidden rounded-xl shadow-2xl'
|
||||
role='dialog'
|
||||
aria-modal='true'
|
||||
aria-label={_('Command Palette')}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{/* search input */}
|
||||
<div className='border-base-300 flex items-center border-b px-4'>
|
||||
<FiSearch className='text-base-content/50 mr-3 h-5 w-5 shrink-0' />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type='text'
|
||||
className='placeholder:text-base-content/50 h-12 w-full bg-transparent text-base outline-none'
|
||||
placeholder={_('Search settings and actions...')}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
/>
|
||||
{query && (
|
||||
<button
|
||||
className='btn btn-ghost btn-circle btn-sm ml-2'
|
||||
onClick={() => setQuery('')}
|
||||
aria-label={_('Clear search')}
|
||||
>
|
||||
<MdClose className='h-4 w-4' />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* results list */}
|
||||
<div ref={listRef} className='max-h-80 overflow-y-auto p-2' role='listbox'>
|
||||
{showEmptyState && (
|
||||
<div className='text-base-content/60 px-4 py-8 text-center text-sm'>
|
||||
{_('No results found for')} "{query}"
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showNoRecentState && (
|
||||
<div className='text-base-content/60 px-4 py-8 text-center text-sm'>
|
||||
{_('Type to search settings and actions')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showRecentState && (
|
||||
<div className='mb-1'>
|
||||
<div className='text-base-content/50 px-3 py-1.5 text-xs font-medium uppercase'>
|
||||
{_('Recent')}
|
||||
</div>
|
||||
{flattenedResults.map((result, index) => (
|
||||
<CommandResultItem
|
||||
key={result.item.id}
|
||||
result={result}
|
||||
isSelected={index === selectedIndex}
|
||||
onClick={() => handleItemClick(result)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
_={_}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{query.trim() && flattenedResults.length > 0 && (
|
||||
<>
|
||||
{orderedCategories.map((category) => {
|
||||
const categoryResults = groupedResults[category];
|
||||
if (categoryResults.length === 0) return null;
|
||||
|
||||
const startIndex = flattenedResults.findIndex((r) => r.item.category === category);
|
||||
|
||||
return (
|
||||
<div key={category} className='mb-1'>
|
||||
<div className='text-base-content/50 px-3 py-1.5 text-xs font-medium uppercase'>
|
||||
{getCategoryLabel(_, category)}
|
||||
</div>
|
||||
{categoryResults.map((result, catIndex) => {
|
||||
const globalIndex = startIndex + catIndex;
|
||||
return (
|
||||
<CommandResultItem
|
||||
key={result.item.id}
|
||||
result={result}
|
||||
isSelected={globalIndex === selectedIndex}
|
||||
onClick={() => handleItemClick(result)}
|
||||
onMouseEnter={() => setSelectedIndex(globalIndex)}
|
||||
_={_}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* footer with keyboard hints */}
|
||||
<div className='border-base-300 text-base-content/50 flex items-center justify-between border-t px-4 py-2 text-xs'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<span>
|
||||
<kbd className='kbd kbd-xs'>↑↓</kbd> {_('navigate')}
|
||||
</span>
|
||||
<span>
|
||||
<kbd className='kbd kbd-xs'>↵</kbd> {_('select')}
|
||||
</span>
|
||||
<span>
|
||||
<kbd className='kbd kbd-xs'>esc</kbd> {_('close')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface CommandResultItemProps {
|
||||
result: CommandSearchResult;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
onMouseEnter: () => void;
|
||||
_: (key: string) => string;
|
||||
}
|
||||
|
||||
const CommandResultItem: React.FC<CommandResultItemProps> = ({
|
||||
result,
|
||||
isSelected,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
_,
|
||||
}) => {
|
||||
const { item, highlightIndices, matchContext } = result;
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
'flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors',
|
||||
isSelected ? 'bg-primary/10 text-primary' : 'hover:bg-base-200',
|
||||
)}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
role='option'
|
||||
aria-selected={isSelected}
|
||||
data-selected={isSelected}
|
||||
>
|
||||
{Icon && (
|
||||
<div className='text-base-content/60 flex h-8 w-8 shrink-0 items-center justify-center'>
|
||||
<Icon className='h-5 w-5' />
|
||||
</div>
|
||||
)}
|
||||
{!Icon && <div className='w-8 shrink-0' />}
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='truncate text-sm'>
|
||||
<HighlightChars str={_(item.labelKey)} indices={highlightIndices} />
|
||||
</div>
|
||||
{(item.section || matchContext) && (
|
||||
<div className='text-base-content/50 truncate text-xs'>
|
||||
{item.panel && <span>{_(item.panelLabel ?? item.panel)}</span>}
|
||||
{item.panel && item.section && <span> › </span>}
|
||||
{item.section && <span>{_(item.section)}</span>}
|
||||
{matchContext &&
|
||||
matchContext !== item.section &&
|
||||
matchContext !== item.panelLabel &&
|
||||
matchContext !== item.panel && <span className='ml-2 italic'>({matchContext})</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{item.shortcut && item.shortcut.length > 0 && (
|
||||
<div className='text-base-content/40 ml-auto shrink-0 text-xs'>
|
||||
{item.shortcut.map((key, i) => (
|
||||
<kbd key={i} className='kbd kbd-xs ml-1'>
|
||||
{key}
|
||||
</kbd>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommandPalette;
|
||||
@@ -0,0 +1,239 @@
|
||||
'use client';
|
||||
|
||||
import React, { createContext, useContext, useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { isTauriAppPlatform } from '@/services/environment';
|
||||
import { tauriHandleSetAlwaysOnTop, tauriHandleToggleFullScreen } from '@/utils/window';
|
||||
import { setAboutDialogVisible } from '@/components/AboutWindow';
|
||||
import { saveSysSettings } from '@/helpers/settings';
|
||||
import { SettingsPanelType } from '@/components/settings/SettingsDialog';
|
||||
import {
|
||||
CommandItem,
|
||||
buildCommandRegistry,
|
||||
searchCommands,
|
||||
CommandSearchResult,
|
||||
groupResultsByCategory,
|
||||
trackCommandUsage,
|
||||
getRecentCommands,
|
||||
CommandCategory,
|
||||
} from '@/services/commandRegistry';
|
||||
|
||||
interface CommandPaletteContextValue {
|
||||
isOpen: boolean;
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
toggle: () => void;
|
||||
query: string;
|
||||
setQuery: (query: string) => void;
|
||||
results: CommandSearchResult[];
|
||||
groupedResults: Record<CommandCategory, CommandSearchResult[]>;
|
||||
recentItems: CommandItem[];
|
||||
executeCommand: (item: CommandItem) => void;
|
||||
commandItems: CommandItem[];
|
||||
}
|
||||
|
||||
const CommandPaletteContext = createContext<CommandPaletteContextValue | null>(null);
|
||||
|
||||
export const useCommandPalette = (): CommandPaletteContextValue => {
|
||||
const context = useContext(CommandPaletteContext);
|
||||
if (!context) {
|
||||
throw new Error('useCommandPalette must be used within CommandPaletteProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface CommandPaletteProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const CommandPaletteProvider: React.FC<CommandPaletteProviderProps> = ({ children }) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { themeMode, setThemeMode } = useThemeStore();
|
||||
const { settings, setSettingsDialogOpen, setActiveSettingsItemId } = useSettingsStore();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const isDesktop = isTauriAppPlatform() && !appService?.isMobile;
|
||||
|
||||
// action handlers
|
||||
const toggleTheme = useCallback(() => {
|
||||
const nextMode = themeMode === 'auto' ? 'light' : themeMode === 'light' ? 'dark' : 'auto';
|
||||
setThemeMode(nextMode);
|
||||
}, [themeMode, setThemeMode]);
|
||||
|
||||
const toggleFullscreen = useCallback(() => {
|
||||
tauriHandleToggleFullScreen();
|
||||
}, []);
|
||||
|
||||
const toggleAlwaysOnTop = useCallback(() => {
|
||||
const newValue = !settings.alwaysOnTop;
|
||||
saveSysSettings(envConfig, 'alwaysOnTop', newValue);
|
||||
tauriHandleSetAlwaysOnTop(newValue);
|
||||
}, [envConfig, settings.alwaysOnTop]);
|
||||
|
||||
const toggleScreenWakeLock = useCallback(() => {
|
||||
const newValue = !settings.screenWakeLock;
|
||||
saveSysSettings(envConfig, 'screenWakeLock', newValue);
|
||||
}, [envConfig, settings.screenWakeLock]);
|
||||
|
||||
const toggleAutoUpload = useCallback(() => {
|
||||
const newValue = !settings.autoUpload;
|
||||
saveSysSettings(envConfig, 'autoUpload', newValue);
|
||||
}, [envConfig, settings.autoUpload]);
|
||||
|
||||
const reloadPage = useCallback(() => {
|
||||
window.location.reload();
|
||||
}, []);
|
||||
|
||||
const toggleOpenLastBooks = useCallback(() => {
|
||||
const newValue = !settings.openLastBooks;
|
||||
saveSysSettings(envConfig, 'openLastBooks', newValue);
|
||||
}, [envConfig, settings.openLastBooks]);
|
||||
|
||||
const showAbout = useCallback(() => {
|
||||
setAboutDialogVisible(true);
|
||||
}, []);
|
||||
|
||||
const toggleTelemetry = useCallback(() => {
|
||||
const newValue = !settings.telemetryEnabled;
|
||||
saveSysSettings(envConfig, 'telemetryEnabled', newValue);
|
||||
}, [envConfig, settings.telemetryEnabled]);
|
||||
|
||||
const openSettingsPanel = useCallback(
|
||||
(_panel: SettingsPanelType, itemId?: string) => {
|
||||
// panel is encoded in itemId (e.g., 'settings.font.defaultFontSize')
|
||||
// SettingsDialog will parse this to determine which panel to show
|
||||
if (itemId) {
|
||||
setActiveSettingsItemId(itemId);
|
||||
}
|
||||
setSettingsDialogOpen(true);
|
||||
},
|
||||
[setSettingsDialogOpen, setActiveSettingsItemId],
|
||||
);
|
||||
|
||||
// build command registry
|
||||
const commandItems = useMemo(
|
||||
() =>
|
||||
buildCommandRegistry({
|
||||
_,
|
||||
openSettingsPanel,
|
||||
toggleTheme,
|
||||
toggleFullscreen,
|
||||
toggleAlwaysOnTop,
|
||||
toggleScreenWakeLock,
|
||||
toggleAutoUpload,
|
||||
reloadPage,
|
||||
toggleOpenLastBooks,
|
||||
showAbout,
|
||||
toggleTelemetry,
|
||||
isDesktop,
|
||||
}),
|
||||
[
|
||||
_,
|
||||
openSettingsPanel,
|
||||
toggleTheme,
|
||||
toggleFullscreen,
|
||||
toggleAlwaysOnTop,
|
||||
toggleScreenWakeLock,
|
||||
toggleAutoUpload,
|
||||
reloadPage,
|
||||
toggleOpenLastBooks,
|
||||
showAbout,
|
||||
toggleTelemetry,
|
||||
isDesktop,
|
||||
],
|
||||
);
|
||||
|
||||
// search results
|
||||
const results = useMemo(() => searchCommands(query, commandItems), [query, commandItems]);
|
||||
const groupedResults = useMemo(() => groupResultsByCategory(results), [results]);
|
||||
|
||||
// recent items
|
||||
const recentItems = useMemo(() => getRecentCommands(commandItems, 5), [commandItems]);
|
||||
|
||||
// palette controls
|
||||
const open = useCallback(() => {
|
||||
setIsOpen(true);
|
||||
setQuery('');
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
setQuery('');
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
if (isOpen) {
|
||||
close();
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
}, [isOpen, open, close]);
|
||||
|
||||
// execute command
|
||||
const executeCommand = useCallback(
|
||||
(item: CommandItem) => {
|
||||
trackCommandUsage(item.id);
|
||||
close();
|
||||
// slight delay to allow modal to close before action
|
||||
requestAnimationFrame(() => {
|
||||
item.action();
|
||||
});
|
||||
},
|
||||
[close],
|
||||
);
|
||||
|
||||
// keyboard shortcut handler (Ctrl/Cmd+Shift+P)
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const isCmdOrCtrl = e.metaKey || e.ctrlKey;
|
||||
if (isCmdOrCtrl && e.shiftKey && e.key.toLowerCase() === 'p') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setSettingsDialogOpen(false);
|
||||
toggle();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown, { capture: true });
|
||||
return () => window.removeEventListener('keydown', handleKeyDown, { capture: true });
|
||||
}, [toggle, setSettingsDialogOpen]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
isOpen,
|
||||
open,
|
||||
close,
|
||||
toggle,
|
||||
query,
|
||||
setQuery,
|
||||
results,
|
||||
groupedResults,
|
||||
recentItems,
|
||||
executeCommand,
|
||||
commandItems,
|
||||
}),
|
||||
[
|
||||
isOpen,
|
||||
open,
|
||||
close,
|
||||
toggle,
|
||||
query,
|
||||
setQuery,
|
||||
results,
|
||||
groupedResults,
|
||||
recentItems,
|
||||
executeCommand,
|
||||
commandItems,
|
||||
],
|
||||
);
|
||||
|
||||
return <CommandPaletteContext.Provider value={value}>{children}</CommandPaletteContext.Provider>;
|
||||
};
|
||||
|
||||
export default CommandPaletteProvider;
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
|
||||
interface HighlightCharsProps {
|
||||
str: string;
|
||||
indices: Set<number>;
|
||||
highlightClassName?: string;
|
||||
}
|
||||
|
||||
const HighlightChars: React.FC<HighlightCharsProps> = ({
|
||||
str,
|
||||
indices,
|
||||
highlightClassName = 'bg-primary/30 text-primary rounded-sm',
|
||||
}) => {
|
||||
if (indices.size === 0) {
|
||||
return <>{str}</>;
|
||||
}
|
||||
|
||||
const chars = str.normalize().split('');
|
||||
|
||||
return (
|
||||
<>
|
||||
{chars.map((char, i) =>
|
||||
indices.has(i) ? (
|
||||
<mark key={i} className={highlightClassName}>
|
||||
{char}
|
||||
</mark>
|
||||
) : (
|
||||
<span key={i}>{char}</span>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HighlightChars;
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as CommandPalette } from './CommandPalette';
|
||||
export { CommandPaletteProvider, useCommandPalette } from './CommandPaletteProvider';
|
||||
export { default as HighlightChars } from './HighlightChars';
|
||||
@@ -304,9 +304,16 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ThemeModeSelector themeMode={themeMode} onThemeModeChange={setThemeMode} />
|
||||
<ThemeModeSelector
|
||||
themeMode={themeMode}
|
||||
onThemeModeChange={setThemeMode}
|
||||
data-setting-id='settings.color.themeMode'
|
||||
/>
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<div
|
||||
data-setting-id='settings.color.invertImageInDarkMode'
|
||||
className='flex items-center justify-between'
|
||||
>
|
||||
<h2 className='font-medium'>{_('Invert Image In Dark Mode')}</h2>
|
||||
<input
|
||||
type='checkbox'
|
||||
@@ -317,7 +324,10 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center justify-between'>
|
||||
<div
|
||||
data-setting-id='settings.color.overrideBookColor'
|
||||
className='flex items-center justify-between'
|
||||
>
|
||||
<h2 className='font-medium'>{_('Override Book Color')}</h2>
|
||||
<input
|
||||
type='checkbox'
|
||||
@@ -334,6 +344,7 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
onThemeColorChange={setThemeColor}
|
||||
onEditTheme={handleEditTheme}
|
||||
onCreateTheme={() => setShowCustomThemeEditor(true)}
|
||||
data-setting-id='settings.color.themeColor'
|
||||
/>
|
||||
|
||||
<BackgroundTextureSelector
|
||||
@@ -347,6 +358,7 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
onSizeChange={setBackgroundSize}
|
||||
onImportImage={handleImportImage}
|
||||
onDeleteTexture={handleDeleteCustomTexture}
|
||||
data-setting-id='settings.color.backgroundTexture'
|
||||
/>
|
||||
|
||||
<HighlightColorsEditor
|
||||
@@ -354,6 +366,7 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
userHighlightColors={userHighlightColors}
|
||||
onChange={handleHighlightColorsChange}
|
||||
onUserColorsChange={handleUserHighlightColorsChange}
|
||||
data-setting-id='settings.color.highlightColors'
|
||||
/>
|
||||
|
||||
<TTSHighlightStyleEditor
|
||||
@@ -363,6 +376,7 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
onStyleChange={handleTTSStyleChange}
|
||||
onColorChange={handleTTSColorChange}
|
||||
onCustomColorsChange={handleCustomTtsColorsChange}
|
||||
data-setting-id='settings.color.ttsHighlightStyle'
|
||||
/>
|
||||
|
||||
<ReadingRulerSettings
|
||||
@@ -374,6 +388,7 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
onLinesChange={setReadingRulerLines}
|
||||
onOpacityChange={setReadingRulerOpacity}
|
||||
onColorChange={setReadingRulerColor}
|
||||
data-setting-id='settings.color.readingRuler'
|
||||
/>
|
||||
|
||||
<CodeHighlightingSettings
|
||||
@@ -381,6 +396,7 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
codeLanguage={codeLanguage}
|
||||
onToggle={setcodeHighlighting}
|
||||
onLanguageChange={setCodeLanguage}
|
||||
data-setting-id='settings.color.codeHighlighting'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -205,7 +205,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
|
||||
return (
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
<div className='w-full'>
|
||||
<div className='w-full' data-setting-id='settings.control.scrolledMode'>
|
||||
<h2 className='mb-2 font-medium'>{_('Scroll')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
@@ -219,7 +219,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
onChange={() => setScrolledMode(!isScrolledMode)}
|
||||
/>
|
||||
</div>
|
||||
<div className='config-item'>
|
||||
<div className='config-item' data-setting-id='settings.control.continuousScroll'>
|
||||
<span className=''>{_('Continuous Scroll')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
@@ -237,12 +237,13 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
min={0}
|
||||
max={200}
|
||||
step={10}
|
||||
data-setting-id='settings.control.overlapPixels'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<div className='w-full' data-setting-id='settings.control.clickToPaginate'>
|
||||
<h2 className='mb-2 font-medium'>{_('Pagination')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-base-200'>
|
||||
@@ -257,7 +258,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
onChange={() => setIsDisableClick(!isDisableClick)}
|
||||
/>
|
||||
</div>
|
||||
<div className='config-item'>
|
||||
<div className='config-item' data-setting-id='settings.control.clickBothSides'>
|
||||
<span className=''>
|
||||
{appService?.isMobileApp ? _('Tap Both Sides') : _('Click Both Sides')}
|
||||
</span>
|
||||
@@ -269,7 +270,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
onChange={() => setFullscreenClickArea(!fullscreenClickArea)}
|
||||
/>
|
||||
</div>
|
||||
<div className='config-item'>
|
||||
<div className='config-item' data-setting-id='settings.control.swapClickSides'>
|
||||
<span className=''>
|
||||
{appService?.isMobileApp ? _('Swap Tap Sides') : _('Swap Click Sides')}
|
||||
</span>
|
||||
@@ -281,7 +282,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
onChange={() => setSwapClickArea(!swapClickArea)}
|
||||
/>
|
||||
</div>
|
||||
<div className='config-item'>
|
||||
<div className='config-item' data-setting-id='settings.control.disableDoubleClick'>
|
||||
<span className=''>
|
||||
{appService?.isMobileApp ? _('Disable Double Tap') : _('Disable Double Click')}
|
||||
</span>
|
||||
@@ -307,7 +308,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<div className='w-full' data-setting-id='settings.control.enableQuickActions'>
|
||||
<h2 className='mb-2 font-medium'>{_('Annotation Tools')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
@@ -320,7 +321,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
onChange={() => setEnableAnnotationQuickActions(!enableAnnotationQuickActions)}
|
||||
/>
|
||||
</div>
|
||||
<div className='config-item'>
|
||||
<div className='config-item' data-setting-id='settings.control.quickAction'>
|
||||
<span className=''>{_('Quick Action')}</span>
|
||||
<Select
|
||||
value={annotationQuickAction || ''}
|
||||
@@ -329,7 +330,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
disabled={!enableAnnotationQuickActions}
|
||||
/>
|
||||
</div>
|
||||
<div className='config-item'>
|
||||
<div className='config-item' data-setting-id='settings.control.copyToNotebook'>
|
||||
<span className=''>{_('Copy to Notebook')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
@@ -342,7 +343,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<div className='w-full' data-setting-id='settings.control.pagingAnimation'>
|
||||
<h2 className='mb-2 font-medium'>{_('Animation')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
@@ -360,7 +361,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
</div>
|
||||
|
||||
{(appService?.isMobileApp || appService?.appPlatform === 'web') && (
|
||||
<div className='w-full'>
|
||||
<div className='w-full' data-setting-id='settings.control.einkMode'>
|
||||
<h2 className='mb-2 font-medium'>{_('Device')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
@@ -376,7 +377,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
</div>
|
||||
)}
|
||||
{(appService?.isAndroidApp || appService?.appPlatform === 'web') && (
|
||||
<div className='config-item'>
|
||||
<div className='config-item' data-setting-id='settings.control.colorEinkMode'>
|
||||
<span className=''>{_('Color E-Ink Mode')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
@@ -403,7 +404,7 @@ const ControlPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRes
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='w-full'>
|
||||
<div className='w-full' data-setting-id='settings.control.allowJavascript'>
|
||||
<h2 className='mb-2 font-medium'>{_('Security')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
|
||||
@@ -51,6 +51,7 @@ interface FontFaceProps {
|
||||
moreOptions?: string[];
|
||||
selected: string;
|
||||
onSelect: (option: string) => void;
|
||||
'data-setting-id'?: string;
|
||||
}
|
||||
|
||||
const handleFontFaceFont = (option: string, family: string) => {
|
||||
@@ -69,10 +70,11 @@ const FontFace = ({
|
||||
moreOptions,
|
||||
selected,
|
||||
onSelect,
|
||||
'data-setting-id': settingId,
|
||||
}: FontFaceProps) => {
|
||||
const _ = useTranslation();
|
||||
return (
|
||||
<div className={clsx('config-item', className)}>
|
||||
<div className={clsx('config-item', className)} data-setting-id={settingId}>
|
||||
<span className='line-clamp-2 min-w-10'>{label}</span>
|
||||
<FontDropdown
|
||||
family={family}
|
||||
@@ -284,7 +286,10 @@ const FontPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
|
||||
return (
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div
|
||||
data-setting-id='settings.font.overrideBookFont'
|
||||
className='flex items-center justify-between'
|
||||
>
|
||||
<h2 className='font-medium'>{_('Override Book Font')}</h2>
|
||||
<input
|
||||
type='checkbox'
|
||||
@@ -304,6 +309,7 @@ const FontPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
onChange={setDefaultFontSize}
|
||||
min={minFontSize}
|
||||
max={120}
|
||||
data-setting-id='settings.font.defaultFontSize'
|
||||
/>
|
||||
<NumberInput
|
||||
label={_('Minimum Font Size')}
|
||||
@@ -311,12 +317,13 @@ const FontPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
onChange={setMinFontSize}
|
||||
min={1}
|
||||
max={120}
|
||||
data-setting-id='settings.font.minimumFontSize'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<div className='w-full' data-setting-id='settings.font.fontWeight'>
|
||||
<h2 className='mb-2 font-medium'>{_('Font Weight')}</h2>
|
||||
<div className='card border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
@@ -336,7 +343,7 @@ const FontPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
<h2 className='mb-2 font-medium'>{_('Font Family')}</h2>
|
||||
<div className='card border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<div className='config-item'>
|
||||
<div className='config-item' data-setting-id='settings.font.defaultFont'>
|
||||
<span className=''>{_('Default Font')}</span>
|
||||
<FontDropdown
|
||||
options={fontFamilyOptions}
|
||||
@@ -354,6 +361,7 @@ const FontPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
options={CJKFonts}
|
||||
selected={defaultCJKFont}
|
||||
onSelect={setDefaultCJKFont}
|
||||
data-setting-id='settings.font.cjkFont'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -385,6 +393,7 @@ const FontPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
moreOptions={sysFonts}
|
||||
selected={serifFont}
|
||||
onSelect={setSerifFont}
|
||||
data-setting-id='settings.font.serifFont'
|
||||
/>
|
||||
<FontFace
|
||||
family='sans-serif'
|
||||
@@ -397,6 +406,7 @@ const FontPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
moreOptions={sysFonts}
|
||||
selected={sansSerifFont}
|
||||
onSelect={setSansSerifFont}
|
||||
data-setting-id='settings.font.sansSerifFont'
|
||||
/>
|
||||
<FontFace
|
||||
className='config-item-bottom'
|
||||
@@ -406,6 +416,7 @@ const FontPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
moreOptions={sysFonts}
|
||||
selected={monospaceFont}
|
||||
onSelect={setMonospaceFont}
|
||||
data-setting-id='settings.font.monospaceFont'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -242,7 +242,7 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
|
||||
return (
|
||||
<div className={clsx('my-4 w-full space-y-6')}>
|
||||
<div className='w-full'>
|
||||
<div className='w-full' data-setting-id='settings.language.interfaceLanguage'>
|
||||
<h2 className='mb-2 font-medium'>{_('Language')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
@@ -258,7 +258,7 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<div className='w-full' data-setting-id='settings.language.translationEnabled'>
|
||||
<h2 className='mb-2 font-medium'>{_('Translation')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-base-200'>
|
||||
@@ -283,7 +283,7 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='config-item'>
|
||||
<div className='config-item' data-setting-id='settings.language.ttsTextTranslation'>
|
||||
<span className=''>{_('TTS Text')}</span>
|
||||
<Select
|
||||
value={ttsReadAloudText}
|
||||
@@ -292,7 +292,7 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='config-item'>
|
||||
<div className='config-item' data-setting-id='settings.language.translationProvider'>
|
||||
<span className=''>{_('Translation Service')}</span>
|
||||
<Select
|
||||
value={getCurrentTranslationProviderOption().value}
|
||||
@@ -301,7 +301,7 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='config-item'>
|
||||
<div className='config-item' data-setting-id='settings.language.targetLanguage'>
|
||||
<span className=''>{_('Translate To')}</span>
|
||||
<Select
|
||||
value={getCurrentTargetLangOption().value}
|
||||
@@ -314,7 +314,7 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
</div>
|
||||
|
||||
{(isCJKEnv() || view?.language.isCJK) && (
|
||||
<div className='w-full'>
|
||||
<div className='w-full' data-setting-id='settings.language.quotationMarks'>
|
||||
<h2 className='mb-2 font-medium'>{_('Punctuation')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-base-200'>
|
||||
@@ -336,7 +336,7 @@ const LangPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
)}
|
||||
|
||||
{(isCJKEnv() || view?.language.isCJK) && (
|
||||
<div className='w-full'>
|
||||
<div className='w-full' data-setting-id='settings.language.chineseConversion'>
|
||||
<h2 className='mb-2 font-medium'>{_('Convert Simplified and Traditional Chinese')}</h2>
|
||||
<div className='card border-base-200 bg-base-100 border shadow'>
|
||||
<div className='divide-base-200'>
|
||||
|
||||
@@ -389,7 +389,10 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
|
||||
return (
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div
|
||||
data-setting-id='settings.layout.overrideBookLayout'
|
||||
className='flex items-center justify-between'
|
||||
>
|
||||
<h2 className='font-medium'>{_('Override Book Layout')}</h2>
|
||||
<input
|
||||
type='checkbox'
|
||||
@@ -399,7 +402,10 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
/>
|
||||
</div>
|
||||
{mightBeRTLBook && (
|
||||
<div className='flex items-center justify-between'>
|
||||
<div
|
||||
data-setting-id='settings.layout.writingMode'
|
||||
className='flex items-center justify-between'
|
||||
>
|
||||
<h2 className='font-medium'>{_('Writing Mode')}</h2>
|
||||
<div className='flex gap-4'>
|
||||
<button
|
||||
@@ -438,7 +444,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
)}
|
||||
|
||||
{viewSettings.vertical && (
|
||||
<div className='w-full'>
|
||||
<div className='w-full' data-setting-id='settings.layout.borderFrame'>
|
||||
<h2 className='mb-2 font-medium'>{_('Border Frame')}</h2>
|
||||
<div className='card bg-base-100 border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
@@ -482,6 +488,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
min={0}
|
||||
max={4}
|
||||
step={0.1}
|
||||
data-setting-id='settings.layout.paragraphMargin'
|
||||
/>
|
||||
<NumberInput
|
||||
label={_('Line Spacing')}
|
||||
@@ -490,6 +497,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
min={1.0}
|
||||
max={3.0}
|
||||
step={0.1}
|
||||
data-setting-id='settings.layout.lineSpacing'
|
||||
/>
|
||||
{langCode !== 'zh' && (
|
||||
<NumberInput
|
||||
@@ -499,6 +507,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
min={-4}
|
||||
max={8}
|
||||
step={0.5}
|
||||
data-setting-id='settings.layout.wordSpacing'
|
||||
/>
|
||||
)}
|
||||
<NumberInput
|
||||
@@ -508,6 +517,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
min={-2}
|
||||
max={4}
|
||||
step={0.5}
|
||||
data-setting-id='settings.layout.letterSpacing'
|
||||
/>
|
||||
<NumberInput
|
||||
label={_('Text Indent')}
|
||||
@@ -516,8 +526,9 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
min={-2}
|
||||
max={4}
|
||||
step={1}
|
||||
data-setting-id='settings.layout.paragraphIndent'
|
||||
/>
|
||||
<div className='config-item'>
|
||||
<div className='config-item' data-setting-id='settings.layout.fullJustification'>
|
||||
<span className=''>{_('Full Justification')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
@@ -526,7 +537,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
onChange={() => setFullJustification(!fullJustification)}
|
||||
/>
|
||||
</div>
|
||||
<div className='config-item'>
|
||||
<div className='config-item' data-setting-id='settings.layout.hyphenation'>
|
||||
<span className=''>{_('Hyphenation')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
@@ -539,7 +550,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<div className='w-full' data-setting-id='settings.layout.pageMargins'>
|
||||
<h2 className='mb-2 font-medium'>{_('Page')}</h2>
|
||||
<div className='card bg-base-100 border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
@@ -589,6 +600,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
onChange={setGapPercent}
|
||||
min={0}
|
||||
max={30}
|
||||
data-setting-id='settings.layout.pageGap'
|
||||
/>
|
||||
<NumberInput
|
||||
label={_('Maximum Number of Columns')}
|
||||
@@ -596,6 +608,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
onChange={setMaxColumnCount}
|
||||
min={1}
|
||||
max={4}
|
||||
data-setting-id='settings.layout.maxColumnCount'
|
||||
/>
|
||||
<NumberInput
|
||||
label={viewSettings.vertical ? _('Maximum Column Height') : _('Maximum Column Width')}
|
||||
@@ -605,6 +618,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
min={200}
|
||||
max={9999}
|
||||
step={50}
|
||||
data-setting-id='settings.layout.maxInlineSize'
|
||||
/>
|
||||
<NumberInput
|
||||
label={viewSettings.vertical ? _('Maximum Column Width') : _('Maximum Column Height')}
|
||||
@@ -614,6 +628,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
min={400}
|
||||
max={9999}
|
||||
step={50}
|
||||
data-setting-id='settings.layout.maxBlockSize'
|
||||
/>
|
||||
<div className='config-item'>
|
||||
<span className=''>{_('Apply also in Scrolled Mode')}</span>
|
||||
@@ -628,7 +643,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<div className='w-full' data-setting-id='settings.layout.showHeader'>
|
||||
<h2 className='mb-2 font-medium'>{_('Header & Footer')}</h2>
|
||||
<div className='card bg-base-100 border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
@@ -641,7 +656,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
onChange={() => setShowHeader(!showHeader)}
|
||||
/>
|
||||
</div>
|
||||
<div className='config-item'>
|
||||
<div className='config-item' data-setting-id='settings.layout.showFooter'>
|
||||
<span className=''>{_('Show Footer')}</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
@@ -694,7 +709,7 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
||||
onChange={() => setShowProgressInfo(!showProgressInfo)}
|
||||
/>
|
||||
</div>
|
||||
<div className='config-item'>
|
||||
<div className='config-item' data-setting-id='settings.layout.progressDisplay'>
|
||||
<span className=''>{_('Reading Progress Style')}</span>
|
||||
<Select
|
||||
value={progressStyle}
|
||||
|
||||
@@ -151,8 +151,9 @@ const MiscPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
error: string | null,
|
||||
saved: boolean,
|
||||
textareaRef: React.RefObject<HTMLTextAreaElement | null>,
|
||||
settingId?: string,
|
||||
) => (
|
||||
<div className='w-full'>
|
||||
<div className='w-full' data-setting-id={settingId}>
|
||||
<h2 className='mb-2 font-medium' aria-label={_(title)}>
|
||||
{_(title)}
|
||||
</h2>
|
||||
@@ -208,6 +209,7 @@ const MiscPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
contentError,
|
||||
draftContentStylesheetSaved,
|
||||
contentTextareaRef,
|
||||
'settings.custom.contentCss',
|
||||
)}
|
||||
|
||||
{renderCSSEditor(
|
||||
@@ -218,6 +220,7 @@ const MiscPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
|
||||
uiError,
|
||||
draftUIStylesheetSaved,
|
||||
uiTextareaRef,
|
||||
'settings.custom.readerUiCss',
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ interface NumberInputProps {
|
||||
step?: number;
|
||||
disabled?: boolean;
|
||||
onChange: (value: number) => void;
|
||||
'data-setting-id'?: string;
|
||||
}
|
||||
|
||||
const NumberInput: React.FC<NumberInputProps> = ({
|
||||
@@ -27,6 +28,7 @@ const NumberInput: React.FC<NumberInputProps> = ({
|
||||
max,
|
||||
step,
|
||||
disabled,
|
||||
'data-setting-id': settingId,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
@@ -72,7 +74,7 @@ const NumberInput: React.FC<NumberInputProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={clsx('config-item', className)}>
|
||||
<div className={clsx('config-item', className)} data-setting-id={settingId}>
|
||||
<span className='text-base-content line-clamp-2'>{label}</span>
|
||||
{iconSize && <span style={{ minWidth: `${iconSize}px` }} />}
|
||||
<div className='text-base-content flex items-center gap-2'>
|
||||
|
||||
@@ -11,7 +11,9 @@ import { PiDotsThreeVerticalBold, PiRobot } from 'react-icons/pi';
|
||||
import { LiaHandPointerSolid } from 'react-icons/lia';
|
||||
import { IoAccessibilityOutline } from 'react-icons/io5';
|
||||
import { MdArrowBackIosNew, MdArrowForwardIos, MdClose } from 'react-icons/md';
|
||||
import { FiSearch } from 'react-icons/fi';
|
||||
import { getDirFromUILanguage } from '@/utils/rtl';
|
||||
import { getCommandPaletteShortcut } from '@/services/environment';
|
||||
import FontPanel from './FontPanel';
|
||||
import LayoutPanel from './LayoutPanel';
|
||||
import ColorPanel from './ColorPanel';
|
||||
@@ -22,6 +24,7 @@ import ControlPanel from './ControlPanel';
|
||||
import LangPanel from './LangPanel';
|
||||
import MiscPanel from './MiscPanel';
|
||||
import AIPanel from './AIPanel';
|
||||
import { useCommandPalette } from '@/components/command-palette';
|
||||
|
||||
export type SettingsPanelType =
|
||||
| 'Font'
|
||||
@@ -49,8 +52,16 @@ const SettingsDialog: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const closeIconSize = useResponsiveSize(16);
|
||||
const [isRtl] = useState(() => getDirFromUILanguage() === 'rtl');
|
||||
const tabsRef = useRef<HTMLDivElement | null>(null);
|
||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||
const [showAllTabLabels, setShowAllTabLabels] = useState(false);
|
||||
const { setFontPanelView, setSettingsDialogOpen } = useSettingsStore();
|
||||
const { setFontPanelView, setSettingsDialogOpen, activeSettingsItemId, setActiveSettingsItemId } =
|
||||
useSettingsStore();
|
||||
const { open: openCommandPalette } = useCommandPalette();
|
||||
|
||||
const handleOpenCommandPalette = () => {
|
||||
openCommandPalette();
|
||||
setSettingsDialogOpen(false);
|
||||
};
|
||||
|
||||
const tabConfig = [
|
||||
{
|
||||
@@ -105,6 +116,16 @@ const SettingsDialog: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
localStorage.setItem('lastConfigPanel', tab);
|
||||
};
|
||||
|
||||
// sync localStorage and fontPanelView when activePanel changes
|
||||
const activePanelRef = useRef(activePanel);
|
||||
useEffect(() => {
|
||||
if (activePanelRef.current !== activePanel) {
|
||||
activePanelRef.current = activePanel;
|
||||
setFontPanelView('main-fonts');
|
||||
localStorage.setItem('lastConfigPanel', activePanel);
|
||||
}
|
||||
}, [activePanel, setFontPanelView]);
|
||||
|
||||
const [resetFunctions, setResetFunctions] = useState<
|
||||
Record<SettingsPanelType, (() => void) | null>
|
||||
>({
|
||||
@@ -132,6 +153,46 @@ const SettingsDialog: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setSettingsDialogOpen(false);
|
||||
};
|
||||
|
||||
// handle activeSettingsItemId: switch to correct panel and scroll to item
|
||||
useEffect(() => {
|
||||
if (!activeSettingsItemId) return;
|
||||
|
||||
// parse panel from item id (format: settings.panel.itemName)
|
||||
const parts = activeSettingsItemId.split('.');
|
||||
if (parts.length >= 2) {
|
||||
const panelMap: Record<string, SettingsPanelType> = {
|
||||
font: 'Font',
|
||||
layout: 'Layout',
|
||||
color: 'Color',
|
||||
control: 'Control',
|
||||
language: 'Language',
|
||||
ai: 'AI',
|
||||
custom: 'Custom',
|
||||
};
|
||||
const panelKey = parts[1]?.toLowerCase();
|
||||
const targetPanel = panelMap[panelKey || ''];
|
||||
if (targetPanel && targetPanel !== activePanel) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- panel switch based on external navigation is intended
|
||||
setActivePanel(targetPanel);
|
||||
}
|
||||
}
|
||||
|
||||
// scroll to item after panel renders
|
||||
const timeoutId = setTimeout(() => {
|
||||
const element = panelRef.current?.querySelector(
|
||||
`[data-setting-id="${activeSettingsItemId}"]`,
|
||||
);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
element.classList.add('settings-highlight');
|
||||
setTimeout(() => element.classList.remove('settings-highlight'), 2000);
|
||||
}
|
||||
setActiveSettingsItemId(null);
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [activeSettingsItemId, activePanel, setActiveSettingsItemId]);
|
||||
|
||||
useEffect(() => {
|
||||
setFontPanelView('main-fonts');
|
||||
|
||||
@@ -241,6 +302,14 @@ const SettingsDialog: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
))}
|
||||
</div>
|
||||
<div className='flex h-full items-center justify-end gap-x-2'>
|
||||
<button
|
||||
onClick={handleOpenCommandPalette}
|
||||
aria-label={_('Search Settings')}
|
||||
title={`${_('Search Settings')} (${getCommandPaletteShortcut()})`}
|
||||
className='btn btn-ghost hidden h-8 min-h-8 w-8 items-center justify-center p-0 sm:flex'
|
||||
>
|
||||
<FiSearch />
|
||||
</button>
|
||||
<Dropdown
|
||||
label={_('Settings Menu')}
|
||||
className='dropdown-bottom dropdown-end'
|
||||
@@ -271,7 +340,11 @@ const SettingsDialog: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div role='group' aria-label={`${_(currentPanel?.label || '')} - ${_('Settings')}`}>
|
||||
<div
|
||||
ref={panelRef}
|
||||
role='group'
|
||||
aria-label={`${_(currentPanel?.label || '')} - ${_('Settings')}`}
|
||||
>
|
||||
{activePanel === 'Font' && (
|
||||
<FontPanel
|
||||
bookKey={bookKey}
|
||||
|
||||
@@ -18,6 +18,7 @@ const DEFAULT_SHORTCUTS = {
|
||||
onReadAloudSelection: ['ctrl+r', 'cmd+r'],
|
||||
onProofreadSelection: ['ctrl+p', 'cmd+p'],
|
||||
onOpenFontLayoutSettings: ['shift+f', 'ctrl+,', 'cmd+,'],
|
||||
onOpenCommandPalette: ['ctrl+Shift+p', 'cmd+Shift+p'],
|
||||
onOpenBooks: ['ctrl+o'],
|
||||
onReloadPage: ['shift+r'],
|
||||
onToggleFullscreen: ['F11'],
|
||||
|
||||
@@ -0,0 +1,837 @@
|
||||
import { Fzf, FzfResultItem, byLengthAsc } from 'fzf';
|
||||
import { SettingsPanelType } from '@/components/settings/SettingsDialog';
|
||||
import { RiFontSize, RiDashboardLine, RiTranslate } from 'react-icons/ri';
|
||||
import { VscSymbolColor } from 'react-icons/vsc';
|
||||
import { LiaHandPointerSolid } from 'react-icons/lia';
|
||||
import { IoAccessibilityOutline } from 'react-icons/io5';
|
||||
import { PiRobot, PiSun, PiMoon } from 'react-icons/pi';
|
||||
import { TbSunMoon } from 'react-icons/tb';
|
||||
import { MdRefresh } from 'react-icons/md';
|
||||
import { IconType } from 'react-icons';
|
||||
import { stubTranslation as _ } from '@/utils/misc';
|
||||
|
||||
export type CommandCategory = 'settings' | 'actions' | 'navigation';
|
||||
|
||||
export interface CommandItem {
|
||||
id: string;
|
||||
labelKey: string;
|
||||
localizedLabel: string;
|
||||
keywords: string[];
|
||||
category: CommandCategory;
|
||||
panel?: SettingsPanelType;
|
||||
panelLabel?: string;
|
||||
section?: string;
|
||||
icon?: IconType;
|
||||
shortcut?: string[];
|
||||
action: () => void;
|
||||
isAvailable?: () => boolean;
|
||||
}
|
||||
|
||||
export interface CommandSearchResult {
|
||||
item: CommandItem;
|
||||
score: number;
|
||||
positions: Set<number>;
|
||||
highlightIndices: Set<number>;
|
||||
matchContext?: string;
|
||||
}
|
||||
|
||||
type TranslationFunc = (key: string) => string;
|
||||
|
||||
// selector for fzf - combines all searchable text
|
||||
const getSearchableText = (item: CommandItem): string => {
|
||||
return [
|
||||
item.localizedLabel,
|
||||
item.labelKey,
|
||||
item.panel ?? '',
|
||||
item.panelLabel ?? '',
|
||||
item.section ?? '',
|
||||
...item.keywords,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
// map fzf positions to display label positions
|
||||
const mapPositionsToLabel = (entry: FzfResultItem<CommandItem>): Set<number> => {
|
||||
const searchText = getSearchableText(entry.item);
|
||||
const label = entry.item.localizedLabel;
|
||||
const labelStart = searchText.indexOf(label);
|
||||
|
||||
if (labelStart === -1) return new Set();
|
||||
|
||||
const labelEnd = labelStart + label.length;
|
||||
const mapped = new Set<number>();
|
||||
|
||||
for (const pos of entry.positions) {
|
||||
if (pos >= labelStart && pos < labelEnd) {
|
||||
mapped.add(pos - labelStart);
|
||||
}
|
||||
}
|
||||
|
||||
return mapped;
|
||||
};
|
||||
|
||||
// find matched context from keywords/section/panel for secondary display
|
||||
const findMatchContext = (entry: FzfResultItem<CommandItem>): string | undefined => {
|
||||
const searchText = getSearchableText(entry.item);
|
||||
const label = entry.item.localizedLabel;
|
||||
const labelStart = searchText.indexOf(label);
|
||||
const labelEnd = labelStart + label.length;
|
||||
|
||||
// check if any match is outside the label
|
||||
for (const pos of entry.positions) {
|
||||
if (pos < labelStart || pos >= labelEnd) {
|
||||
// match is in keywords/section/panel area
|
||||
const parts = [
|
||||
entry.item.panelLabel ?? entry.item.panel,
|
||||
entry.item.section,
|
||||
...entry.item.keywords,
|
||||
].filter(Boolean);
|
||||
for (const part of parts) {
|
||||
if (part && searchText.includes(part)) {
|
||||
const partStart = searchText.indexOf(part, labelEnd);
|
||||
if (partStart !== -1) {
|
||||
for (const p of entry.positions) {
|
||||
if (p >= partStart && p < partStart + part.length) {
|
||||
return part;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const searchCommands = (query: string, items: CommandItem[]): CommandSearchResult[] => {
|
||||
if (!query.trim()) return [];
|
||||
|
||||
const availableItems = items.filter((item) => !item.isAvailable || item.isAvailable());
|
||||
|
||||
const fzf = new Fzf(availableItems, {
|
||||
selector: getSearchableText,
|
||||
tiebreakers: [byLengthAsc],
|
||||
casing: 'smart-case',
|
||||
normalize: true,
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
const results = fzf.find(query);
|
||||
|
||||
return results.map((entry) => ({
|
||||
item: entry.item,
|
||||
score: entry.score,
|
||||
positions: entry.positions,
|
||||
highlightIndices: mapPositionsToLabel(entry),
|
||||
matchContext: findMatchContext(entry),
|
||||
}));
|
||||
};
|
||||
|
||||
// group results by category
|
||||
export const groupResultsByCategory = (
|
||||
results: CommandSearchResult[],
|
||||
): Record<CommandCategory, CommandSearchResult[]> => {
|
||||
const grouped: Record<CommandCategory, CommandSearchResult[]> = {
|
||||
settings: [],
|
||||
actions: [],
|
||||
navigation: [],
|
||||
};
|
||||
|
||||
for (const result of results) {
|
||||
grouped[result.item.category].push(result);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
};
|
||||
|
||||
// settings panel icon map
|
||||
const panelIcons: Record<SettingsPanelType, IconType> = {
|
||||
Font: RiFontSize,
|
||||
Layout: RiDashboardLine,
|
||||
Color: VscSymbolColor,
|
||||
Control: LiaHandPointerSolid,
|
||||
Language: RiTranslate,
|
||||
AI: PiRobot,
|
||||
Custom: IoAccessibilityOutline,
|
||||
};
|
||||
|
||||
// font panel items
|
||||
const fontPanelItems = [
|
||||
{
|
||||
id: 'settings.font.overrideBookFont',
|
||||
labelKey: _('Override Book Font'),
|
||||
keywords: ['font', 'override', 'book', 'custom'],
|
||||
section: 'Font',
|
||||
},
|
||||
{
|
||||
id: 'settings.font.defaultFontSize',
|
||||
labelKey: _('Default Font Size'),
|
||||
keywords: ['font', 'size', 'default', 'px', 'pixels', 'text'],
|
||||
section: 'Font Size',
|
||||
},
|
||||
{
|
||||
id: 'settings.font.minimumFontSize',
|
||||
labelKey: _('Minimum Font Size'),
|
||||
keywords: ['font', 'size', 'minimum', 'min', 'small'],
|
||||
section: 'Font Size',
|
||||
},
|
||||
{
|
||||
id: 'settings.font.fontWeight',
|
||||
labelKey: _('Font Weight'),
|
||||
keywords: ['font', 'weight', 'bold', 'light', 'thickness'],
|
||||
section: 'Font Weight',
|
||||
},
|
||||
{
|
||||
id: 'settings.font.defaultFont',
|
||||
labelKey: _('Default Font'),
|
||||
keywords: ['font', 'family', 'serif', 'sans', 'default'],
|
||||
section: 'Font Family',
|
||||
},
|
||||
{
|
||||
id: 'settings.font.cjkFont',
|
||||
labelKey: _('CJK Font'),
|
||||
keywords: ['font', 'cjk', 'chinese', 'japanese', 'korean', 'asian'],
|
||||
section: 'Font Family',
|
||||
},
|
||||
{
|
||||
id: 'settings.font.serifFont',
|
||||
labelKey: _('Serif Font'),
|
||||
keywords: ['font', 'serif', 'family', 'typeface'],
|
||||
section: 'Font Face',
|
||||
},
|
||||
{
|
||||
id: 'settings.font.sansSerifFont',
|
||||
labelKey: _('Sans-Serif Font'),
|
||||
keywords: ['font', 'sans', 'serif', 'family', 'typeface'],
|
||||
section: 'Font Face',
|
||||
},
|
||||
{
|
||||
id: 'settings.font.monospaceFont',
|
||||
labelKey: _('Monospace Font'),
|
||||
keywords: ['font', 'monospace', 'mono', 'code', 'fixed', 'width'],
|
||||
section: 'Font Face',
|
||||
},
|
||||
];
|
||||
|
||||
// layout panel items
|
||||
const layoutPanelItems = [
|
||||
{
|
||||
id: 'settings.layout.overrideBookLayout',
|
||||
labelKey: _('Override Book Layout'),
|
||||
keywords: ['layout', 'override', 'book', 'custom'],
|
||||
section: 'Layout',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.writingMode',
|
||||
labelKey: _('Writing Mode'),
|
||||
keywords: ['writing', 'mode', 'vertical', 'horizontal', 'direction', 'rtl', 'ltr'],
|
||||
section: 'Layout',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.borderFrame',
|
||||
labelKey: _('Border Frame'),
|
||||
keywords: ['border', 'frame', 'vertical', 'mode'],
|
||||
section: 'Layout',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.paragraphMargin',
|
||||
labelKey: _('Paragraph Margin'),
|
||||
keywords: ['paragraph', 'margin', 'spacing', 'gap'],
|
||||
section: 'Paragraph',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.lineSpacing',
|
||||
labelKey: _('Line Spacing'),
|
||||
keywords: ['line', 'spacing', 'height', 'leading'],
|
||||
section: 'Paragraph',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.wordSpacing',
|
||||
labelKey: _('Word Spacing'),
|
||||
keywords: ['word', 'spacing', 'gap'],
|
||||
section: 'Paragraph',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.letterSpacing',
|
||||
labelKey: _('Letter Spacing'),
|
||||
keywords: ['letter', 'spacing', 'tracking', 'character'],
|
||||
section: 'Paragraph',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.paragraphIndent',
|
||||
labelKey: _('Text Indent'),
|
||||
keywords: ['paragraph', 'indent', 'first', 'line'],
|
||||
section: 'Paragraph',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.fullJustification',
|
||||
labelKey: _('Full Justification'),
|
||||
keywords: ['justify', 'justification', 'alignment', 'text', 'full'],
|
||||
section: 'Paragraph',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.hyphenation',
|
||||
labelKey: _('Hyphenation'),
|
||||
keywords: ['hyphen', 'hyphenation', 'break', 'word'],
|
||||
section: 'Paragraph',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.pageMargins',
|
||||
labelKey: _('Page Margins'),
|
||||
keywords: ['page', 'margin', 'edge', 'border'],
|
||||
section: 'Page',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.pageGap',
|
||||
labelKey: _('Column Gap (%)'),
|
||||
keywords: ['page', 'gap', 'spacing', 'gutter'],
|
||||
section: 'Page',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.maxColumnCount',
|
||||
labelKey: _('Maximum Number of Columns'),
|
||||
keywords: ['column', 'columns', 'max', 'count', 'multi'],
|
||||
section: 'Page',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.maxInlineSize',
|
||||
labelKey: _('Maximum Column Width'),
|
||||
keywords: ['width', 'max', 'inline', 'size', 'column'],
|
||||
section: 'Page',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.maxBlockSize',
|
||||
labelKey: _('Maximum Column Height'),
|
||||
keywords: ['height', 'max', 'block', 'size'],
|
||||
section: 'Page',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.showHeader',
|
||||
labelKey: _('Show Header'),
|
||||
keywords: ['header', 'show', 'top', 'bar', 'title'],
|
||||
section: 'Header & Footer',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.showFooter',
|
||||
labelKey: _('Show Footer'),
|
||||
keywords: ['footer', 'show', 'bottom', 'bar', 'page', 'number'],
|
||||
section: 'Header & Footer',
|
||||
},
|
||||
{
|
||||
id: 'settings.layout.progressDisplay',
|
||||
labelKey: _('Reading Progress Style'),
|
||||
keywords: ['progress', 'display', 'page', 'number', 'percentage'],
|
||||
section: 'Header & Footer',
|
||||
},
|
||||
];
|
||||
|
||||
// color panel items
|
||||
const colorPanelItems = [
|
||||
{
|
||||
id: 'settings.color.themeMode',
|
||||
labelKey: _('Theme Mode'),
|
||||
keywords: ['theme', 'mode', 'dark', 'light', 'auto', 'system'],
|
||||
section: 'Theme',
|
||||
},
|
||||
{
|
||||
id: 'settings.color.invertImageInDarkMode',
|
||||
labelKey: _('Invert Image In Dark Mode'),
|
||||
keywords: ['invert', 'image', 'dark', 'mode', 'photo'],
|
||||
section: 'Theme',
|
||||
},
|
||||
{
|
||||
id: 'settings.color.overrideBookColor',
|
||||
labelKey: _('Override Book Color'),
|
||||
keywords: ['override', 'book', 'color', 'custom'],
|
||||
section: 'Theme',
|
||||
},
|
||||
{
|
||||
id: 'settings.color.themeColor',
|
||||
labelKey: _('Theme Color'),
|
||||
keywords: ['theme', 'color', 'palette', 'accent'],
|
||||
section: 'Theme',
|
||||
},
|
||||
{
|
||||
id: 'settings.color.backgroundTexture',
|
||||
labelKey: _('Background Image'),
|
||||
keywords: ['background', 'texture', 'image', 'paper', 'pattern'],
|
||||
section: 'Theme',
|
||||
},
|
||||
{
|
||||
id: 'settings.color.highlightColors',
|
||||
labelKey: _('Highlight Colors'),
|
||||
keywords: ['highlight', 'color', 'annotation', 'marker'],
|
||||
section: 'Highlight',
|
||||
},
|
||||
{
|
||||
id: 'settings.color.ttsHighlightStyle',
|
||||
labelKey: _('TTS Highlighting'),
|
||||
keywords: ['tts', 'highlight', 'style', 'speech', 'read', 'aloud'],
|
||||
section: 'Highlight',
|
||||
},
|
||||
{
|
||||
id: 'settings.color.readingRuler',
|
||||
labelKey: _('Reading Ruler'),
|
||||
keywords: ['reading', 'ruler', 'line', 'guide', 'focus'],
|
||||
section: 'Reading',
|
||||
},
|
||||
{
|
||||
id: 'settings.color.codeHighlighting',
|
||||
labelKey: _('Code Highlighting'),
|
||||
keywords: ['code', 'highlighting', 'syntax', 'programming'],
|
||||
section: 'Code',
|
||||
},
|
||||
];
|
||||
|
||||
// control panel items
|
||||
const controlPanelItems = [
|
||||
{
|
||||
id: 'settings.control.scrolledMode',
|
||||
labelKey: _('Scrolled Mode'),
|
||||
keywords: ['scroll', 'scrolled', 'mode', 'paginate', 'continuous'],
|
||||
section: 'Scroll',
|
||||
},
|
||||
{
|
||||
id: 'settings.control.continuousScroll',
|
||||
labelKey: _('Continuous Scroll'),
|
||||
keywords: ['continuous', 'scroll', 'endless', 'infinite'],
|
||||
section: 'Scroll',
|
||||
},
|
||||
{
|
||||
id: 'settings.control.overlapPixels',
|
||||
labelKey: _('Overlap Pixels'),
|
||||
keywords: ['overlap', 'pixels', 'scroll', 'offset'],
|
||||
section: 'Scroll',
|
||||
},
|
||||
{
|
||||
id: 'settings.control.clickToPaginate',
|
||||
labelKey: _('Click to Paginate'),
|
||||
keywords: ['click', 'tap', 'paginate', 'page', 'turn'],
|
||||
section: 'Pagination',
|
||||
},
|
||||
{
|
||||
id: 'settings.control.clickBothSides',
|
||||
labelKey: _('Click Both Sides'),
|
||||
keywords: ['click', 'tap', 'both', 'sides', 'fullscreen'],
|
||||
section: 'Pagination',
|
||||
},
|
||||
{
|
||||
id: 'settings.control.swapClickSides',
|
||||
labelKey: _('Swap Click Sides'),
|
||||
keywords: ['swap', 'click', 'tap', 'sides', 'reverse'],
|
||||
section: 'Pagination',
|
||||
},
|
||||
{
|
||||
id: 'settings.control.disableDoubleClick',
|
||||
labelKey: _('Disable Double Click'),
|
||||
keywords: ['disable', 'double', 'click', 'tap'],
|
||||
section: 'Pagination',
|
||||
},
|
||||
{
|
||||
id: 'settings.control.enableQuickActions',
|
||||
labelKey: _('Enable Quick Actions'),
|
||||
keywords: ['quick', 'actions', 'annotation', 'enable'],
|
||||
section: 'Annotation Tools',
|
||||
},
|
||||
{
|
||||
id: 'settings.control.quickAction',
|
||||
labelKey: _('Quick Action'),
|
||||
keywords: ['quick', 'action', 'annotation', 'highlight', 'copy'],
|
||||
section: 'Annotation Tools',
|
||||
},
|
||||
{
|
||||
id: 'settings.control.copyToNotebook',
|
||||
labelKey: _('Copy to Notebook'),
|
||||
keywords: ['copy', 'notebook', 'annotation', 'excerpt'],
|
||||
section: 'Annotation Tools',
|
||||
},
|
||||
{
|
||||
id: 'settings.control.pagingAnimation',
|
||||
labelKey: _('Paging Animation'),
|
||||
keywords: ['paging', 'animation', 'transition', 'effect'],
|
||||
section: 'Animation',
|
||||
},
|
||||
{
|
||||
id: 'settings.control.einkMode',
|
||||
labelKey: _('E-Ink Mode'),
|
||||
keywords: ['eink', 'e-ink', 'kindle', 'e-reader', 'epaper'],
|
||||
section: 'Device',
|
||||
},
|
||||
{
|
||||
id: 'settings.control.colorEinkMode',
|
||||
labelKey: _('Color E-Ink Mode'),
|
||||
keywords: ['color', 'eink', 'e-ink', 'kaleido'],
|
||||
section: 'Device',
|
||||
},
|
||||
{
|
||||
id: 'settings.control.allowJavascript',
|
||||
labelKey: _('Allow JavaScript'),
|
||||
keywords: ['javascript', 'js', 'script', 'security', 'allow'],
|
||||
section: 'Security',
|
||||
},
|
||||
];
|
||||
|
||||
// language panel items
|
||||
const languagePanelItems = [
|
||||
{
|
||||
id: 'settings.language.interfaceLanguage',
|
||||
labelKey: _('Interface Language'),
|
||||
keywords: ['interface', 'language', 'locale', 'ui', 'translation'],
|
||||
section: 'Language',
|
||||
},
|
||||
{
|
||||
id: 'settings.language.translationEnabled',
|
||||
labelKey: _('Enable Translation'),
|
||||
keywords: ['translation', 'translate', 'enable', 'language'],
|
||||
section: 'Translation',
|
||||
},
|
||||
{
|
||||
id: 'settings.language.translationProvider',
|
||||
labelKey: _('Translation Service'),
|
||||
keywords: ['translation', 'provider', 'google', 'deepl', 'service'],
|
||||
section: 'Translation',
|
||||
},
|
||||
{
|
||||
id: 'settings.language.targetLanguage',
|
||||
labelKey: _('Translate To'),
|
||||
keywords: ['target', 'language', 'translation', 'destination'],
|
||||
section: 'Translation',
|
||||
},
|
||||
{
|
||||
id: 'settings.language.ttsTextTranslation',
|
||||
labelKey: _('TTS Text'),
|
||||
keywords: ['tts', 'text', 'translation', 'speech', 'read'],
|
||||
section: 'Translation',
|
||||
},
|
||||
{
|
||||
id: 'settings.language.quotationMarks',
|
||||
labelKey: _('Replace Quotation Marks'),
|
||||
keywords: ['quotation', 'marks', 'quotes', 'punctuation', 'cjk'],
|
||||
section: 'Punctuation',
|
||||
},
|
||||
{
|
||||
id: 'settings.language.chineseConversion',
|
||||
labelKey: _('Convert Simplified and Traditional Chinese'),
|
||||
keywords: ['chinese', 'conversion', 'simplified', 'traditional', 'cjk'],
|
||||
section: 'Chinese',
|
||||
},
|
||||
];
|
||||
|
||||
// ai panel items
|
||||
const aiPanelItems = [
|
||||
{
|
||||
id: 'settings.ai.enableAssistant',
|
||||
labelKey: _('Enable AI Assistant'),
|
||||
keywords: ['ai', 'assistant', 'enable', 'chatbot', 'llm'],
|
||||
section: 'AI',
|
||||
},
|
||||
{
|
||||
id: 'settings.ai.provider',
|
||||
labelKey: _('AI Provider'),
|
||||
keywords: ['ai', 'provider', 'ollama', 'gateway', 'service'],
|
||||
section: 'AI',
|
||||
},
|
||||
{
|
||||
id: 'settings.ai.ollamaUrl',
|
||||
labelKey: _('Ollama URL'),
|
||||
keywords: ['ollama', 'url', 'server', 'endpoint', 'api'],
|
||||
section: 'Ollama',
|
||||
},
|
||||
{
|
||||
id: 'settings.ai.ollamaModel',
|
||||
labelKey: _('Ollama Model'),
|
||||
keywords: ['ollama', 'model', 'llama', 'mistral', 'gemma'],
|
||||
section: 'Ollama',
|
||||
},
|
||||
{
|
||||
id: 'settings.ai.gatewayApiKey',
|
||||
labelKey: _('API Key'),
|
||||
keywords: ['api', 'key', 'gateway', 'token', 'secret'],
|
||||
section: 'AI Gateway',
|
||||
},
|
||||
{
|
||||
id: 'settings.ai.gatewayModel',
|
||||
labelKey: _('AI Gateway Model'),
|
||||
keywords: ['gateway', 'model', 'openai', 'gpt', 'claude'],
|
||||
section: 'AI Gateway',
|
||||
},
|
||||
];
|
||||
|
||||
// custom panel items
|
||||
const customPanelItems = [
|
||||
{
|
||||
id: 'settings.custom.contentCss',
|
||||
labelKey: _('Custom Content CSS'),
|
||||
keywords: ['custom', 'css', 'content', 'style', 'book'],
|
||||
section: 'Custom CSS',
|
||||
},
|
||||
{
|
||||
id: 'settings.custom.readerUiCss',
|
||||
labelKey: _('Custom Reader UI CSS'),
|
||||
keywords: ['custom', 'css', 'reader', 'ui', 'interface'],
|
||||
section: 'Custom CSS',
|
||||
},
|
||||
];
|
||||
|
||||
const actionItems = [
|
||||
{
|
||||
id: 'action.toggleTheme',
|
||||
labelKey: _('Theme Mode'),
|
||||
keywords: ['theme', 'dark', 'light', 'auto', 'mode', 'toggle'],
|
||||
},
|
||||
{
|
||||
id: 'action.fullscreen',
|
||||
labelKey: _('Fullscreen'),
|
||||
keywords: ['fullscreen', 'full', 'screen', 'maximize', 'window'],
|
||||
},
|
||||
{
|
||||
id: 'action.alwaysOnTop',
|
||||
labelKey: _('Always on Top'),
|
||||
keywords: ['always', 'top', 'pin', 'window', 'float'],
|
||||
},
|
||||
{
|
||||
id: 'action.screenWakeLock',
|
||||
labelKey: _('Keep Screen Awake'),
|
||||
keywords: ['screen', 'wake', 'lock', 'awake', 'sleep', 'display'],
|
||||
},
|
||||
{
|
||||
id: 'action.autoUpload',
|
||||
labelKey: _('Auto Upload Books to Cloud'),
|
||||
keywords: ['auto', 'upload', 'cloud', 'sync', 'backup'],
|
||||
},
|
||||
{
|
||||
id: 'action.reload',
|
||||
labelKey: _('Reload Page'),
|
||||
keywords: ['reload', 'refresh', 'page'],
|
||||
},
|
||||
{
|
||||
id: 'action.openLastBooks',
|
||||
labelKey: _('Open Last Book on Start'),
|
||||
keywords: ['open', 'last', 'book', 'start', 'resume'],
|
||||
},
|
||||
{
|
||||
id: 'action.about',
|
||||
labelKey: _('About Readest'),
|
||||
keywords: ['about', 'readest', 'version', 'info'],
|
||||
},
|
||||
{
|
||||
id: 'action.telemetry',
|
||||
labelKey: _('Help improve Readest'),
|
||||
keywords: ['telemetry', 'analytics', 'improve', 'statistics'],
|
||||
},
|
||||
];
|
||||
|
||||
export interface CommandRegistryOptions {
|
||||
_: TranslationFunc;
|
||||
openSettingsPanel: (panel: SettingsPanelType, itemId?: string) => void;
|
||||
toggleTheme: () => void;
|
||||
toggleFullscreen: () => void;
|
||||
toggleAlwaysOnTop: () => void;
|
||||
toggleScreenWakeLock: () => void;
|
||||
toggleAutoUpload: () => void;
|
||||
reloadPage: () => void;
|
||||
toggleOpenLastBooks: () => void;
|
||||
showAbout: () => void;
|
||||
toggleTelemetry: () => void;
|
||||
isDesktop: boolean;
|
||||
// TODO: add reader-specific actions when reader is open (tts, bookmark, etc.)
|
||||
}
|
||||
|
||||
export const buildCommandRegistry = (options: CommandRegistryOptions): CommandItem[] => {
|
||||
const { _, openSettingsPanel, isDesktop } = options;
|
||||
const items: CommandItem[] = [];
|
||||
|
||||
// helper to create settings item
|
||||
const createSettingsItem = (
|
||||
def: { id: string; labelKey: string; keywords: string[]; section?: string },
|
||||
panel: SettingsPanelType,
|
||||
panelLabel?: string,
|
||||
): CommandItem => ({
|
||||
id: def.id,
|
||||
labelKey: def.labelKey,
|
||||
localizedLabel: _(def.labelKey),
|
||||
keywords: def.keywords,
|
||||
category: 'settings',
|
||||
panel,
|
||||
panelLabel: _(panelLabel ?? panel),
|
||||
section: def.section,
|
||||
icon: panelIcons[panel],
|
||||
action: () => openSettingsPanel(panel, def.id),
|
||||
});
|
||||
|
||||
// add font panel items
|
||||
for (const def of fontPanelItems) {
|
||||
items.push(createSettingsItem(def, 'Font'));
|
||||
}
|
||||
|
||||
// add layout panel items
|
||||
for (const def of layoutPanelItems) {
|
||||
items.push(createSettingsItem(def, 'Layout'));
|
||||
}
|
||||
|
||||
// add color panel items
|
||||
for (const def of colorPanelItems) {
|
||||
items.push(createSettingsItem(def, 'Color'));
|
||||
}
|
||||
|
||||
// add control panel items
|
||||
for (const def of controlPanelItems) {
|
||||
items.push(createSettingsItem(def, 'Control', 'Behavior'));
|
||||
}
|
||||
|
||||
// add language panel items
|
||||
for (const def of languagePanelItems) {
|
||||
items.push(createSettingsItem(def, 'Language'));
|
||||
}
|
||||
|
||||
// add ai panel items (only in dev, as of now atleast)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
for (const def of aiPanelItems) {
|
||||
items.push(createSettingsItem(def, 'AI'));
|
||||
}
|
||||
}
|
||||
|
||||
// add custom panel items
|
||||
for (const def of customPanelItems) {
|
||||
items.push(createSettingsItem(def, 'Custom'));
|
||||
}
|
||||
|
||||
// add action items
|
||||
const getThemeIcon = (): IconType => {
|
||||
const themeMode =
|
||||
typeof localStorage !== 'undefined' ? localStorage.getItem('themeMode') : 'auto';
|
||||
return themeMode === 'dark' ? PiMoon : themeMode === 'light' ? PiSun : TbSunMoon;
|
||||
};
|
||||
|
||||
const createActionItem = (def: {
|
||||
id: string;
|
||||
action: () => void;
|
||||
icon?: IconType;
|
||||
isAvailable?: () => boolean;
|
||||
}): CommandItem => {
|
||||
const item = actionItems.find((item) => item.id === def.id);
|
||||
if (!item) throw new Error(`Action item definition not found for id: ${def.id}`);
|
||||
return {
|
||||
id: def.id,
|
||||
labelKey: item.labelKey,
|
||||
localizedLabel: _(item.labelKey),
|
||||
keywords: item.keywords,
|
||||
icon: def.icon ?? getThemeIcon(),
|
||||
category: 'actions',
|
||||
action: def.action,
|
||||
isAvailable: def.isAvailable,
|
||||
};
|
||||
};
|
||||
|
||||
items.push(
|
||||
createActionItem({
|
||||
id: 'action.toggleTheme',
|
||||
action: options.toggleTheme,
|
||||
}),
|
||||
);
|
||||
|
||||
items.push(
|
||||
createActionItem({
|
||||
id: 'action.fullscreen',
|
||||
action: options.toggleFullscreen,
|
||||
isAvailable: () => isDesktop,
|
||||
}),
|
||||
);
|
||||
|
||||
items.push(
|
||||
createActionItem({
|
||||
id: 'action.alwaysOnTop',
|
||||
action: options.toggleAlwaysOnTop,
|
||||
isAvailable: () => isDesktop,
|
||||
}),
|
||||
);
|
||||
|
||||
items.push(
|
||||
createActionItem({
|
||||
id: 'action.screenWakeLock',
|
||||
action: options.toggleScreenWakeLock,
|
||||
}),
|
||||
);
|
||||
|
||||
items.push(
|
||||
createActionItem({
|
||||
id: 'action.autoUpload',
|
||||
action: options.toggleAutoUpload,
|
||||
}),
|
||||
);
|
||||
|
||||
items.push(
|
||||
createActionItem({
|
||||
id: 'action.reload',
|
||||
icon: MdRefresh,
|
||||
action: options.reloadPage,
|
||||
}),
|
||||
);
|
||||
|
||||
items.push(
|
||||
createActionItem({
|
||||
id: 'action.openLastBooks',
|
||||
action: options.toggleOpenLastBooks,
|
||||
isAvailable: () => isDesktop,
|
||||
}),
|
||||
);
|
||||
|
||||
items.push(
|
||||
createActionItem({
|
||||
id: 'action.about',
|
||||
action: options.showAbout,
|
||||
}),
|
||||
);
|
||||
|
||||
items.push(
|
||||
createActionItem({
|
||||
id: 'action.telemetry',
|
||||
action: options.toggleTelemetry,
|
||||
}),
|
||||
);
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
// category labels for display
|
||||
export const getCategoryLabel = (_: TranslationFunc, category: CommandCategory): string => {
|
||||
switch (category) {
|
||||
case 'settings':
|
||||
return _('Settings');
|
||||
case 'actions':
|
||||
return _('Actions');
|
||||
case 'navigation':
|
||||
return _('Navigation');
|
||||
default:
|
||||
return category;
|
||||
}
|
||||
};
|
||||
|
||||
// get recent commands from localStorage
|
||||
export const getRecentCommands = (items: CommandItem[], limit = 5): CommandItem[] => {
|
||||
if (typeof localStorage === 'undefined') return [];
|
||||
|
||||
try {
|
||||
const recentIds = JSON.parse(localStorage.getItem('recentCommands') || '[]') as string[];
|
||||
return recentIds
|
||||
.slice(0, limit)
|
||||
.map((id) => items.find((item) => item.id === id))
|
||||
.filter((item): item is CommandItem => item !== undefined);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// track command usage for recent list
|
||||
export const trackCommandUsage = (commandId: string): void => {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
|
||||
try {
|
||||
const recentIds = JSON.parse(localStorage.getItem('recentCommands') || '[]') as string[];
|
||||
const updated = [commandId, ...recentIds.filter((id) => id !== commandId)].slice(0, 10);
|
||||
localStorage.setItem('recentCommands', JSON.stringify(updated));
|
||||
} catch {
|
||||
// ignore errors
|
||||
}
|
||||
};
|
||||
@@ -15,6 +15,11 @@ export const getBaseUrl = () => process.env['NEXT_PUBLIC_API_BASE_URL'] ?? READE
|
||||
export const getNodeBaseUrl = () =>
|
||||
process.env['NEXT_PUBLIC_NODE_BASE_URL'] ?? READEST_NODE_BASE_URL;
|
||||
|
||||
export const isMacPlatform = () =>
|
||||
typeof window !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
||||
|
||||
export const getCommandPaletteShortcut = () => (isMacPlatform() ? '⌘⇧P' : 'Ctrl+Shift+P');
|
||||
|
||||
const isWebDevMode = () => process.env['NODE_ENV'] === 'development' && isWebAppPlatform();
|
||||
|
||||
// Dev API only in development mode and web platform
|
||||
|
||||
@@ -12,12 +12,14 @@ interface SettingsState {
|
||||
isSettingsDialogOpen: boolean;
|
||||
isSettingsGlobal: boolean;
|
||||
fontPanelView: FontPanelView;
|
||||
activeSettingsItemId: string | null;
|
||||
setSettings: (settings: SystemSettings) => void;
|
||||
saveSettings: (envConfig: EnvConfigType, settings: SystemSettings) => void;
|
||||
setSettingsDialogBookKey: (bookKey: string) => void;
|
||||
setSettingsDialogOpen: (open: boolean) => void;
|
||||
setSettingsGlobal: (global: boolean) => void;
|
||||
setFontPanelView: (view: FontPanelView) => void;
|
||||
setActiveSettingsItemId: (id: string | null) => void;
|
||||
|
||||
applyUILanguage: (uiLanguage?: string) => void;
|
||||
}
|
||||
@@ -28,6 +30,7 @@ export const useSettingsStore = create<SettingsState>((set) => ({
|
||||
isSettingsDialogOpen: false,
|
||||
isSettingsGlobal: true,
|
||||
fontPanelView: 'main-fonts',
|
||||
activeSettingsItemId: null,
|
||||
setSettings: (settings) => set({ settings }),
|
||||
saveSettings: async (envConfig: EnvConfigType, settings: SystemSettings) => {
|
||||
const appService = await envConfig.getAppService();
|
||||
@@ -37,6 +40,7 @@ export const useSettingsStore = create<SettingsState>((set) => ({
|
||||
setSettingsDialogOpen: (open) => set({ isSettingsDialogOpen: open }),
|
||||
setSettingsGlobal: (global) => set({ isSettingsGlobal: global }),
|
||||
setFontPanelView: (view) => set({ fontPanelView: view }),
|
||||
setActiveSettingsItemId: (id) => set({ activeSettingsItemId: id }),
|
||||
|
||||
applyUILanguage: (uiLanguage?: string) => {
|
||||
const locale = uiLanguage ? uiLanguage : navigator.language;
|
||||
|
||||
@@ -622,3 +622,20 @@ foliate-fxl {
|
||||
.no-transitions * {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* settings highlight animation for command palette */
|
||||
@keyframes settings-highlight-pulse {
|
||||
0%,
|
||||
100% {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: theme('colors.primary/0.15');
|
||||
}
|
||||
}
|
||||
|
||||
.settings-highlight {
|
||||
animation: settings-highlight-pulse 0.5s ease-in-out 2;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
Generated
+8
-55
@@ -218,6 +218,9 @@ importers:
|
||||
franc-min:
|
||||
specifier: ^6.2.0
|
||||
version: 6.2.0
|
||||
fzf:
|
||||
specifier: ^0.5.2
|
||||
version: 0.5.2
|
||||
google-auth-library:
|
||||
specifier: ^10.5.0
|
||||
version: 10.5.0
|
||||
@@ -622,28 +625,24 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@ast-grep/napi-linux-arm64-musl@0.40.0':
|
||||
resolution: {integrity: sha512-MS9qalLRjUnF2PCzuTKTvCMVSORYHxxe3Qa0+SSaVULsXRBmuy5C/b1FeWwMFnwNnC0uie3VDet31Zujwi8q6A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@ast-grep/napi-linux-x64-gnu@0.40.0':
|
||||
resolution: {integrity: sha512-BeHZVMNXhM3WV3XE2yghO0fRxhMOt8BTN972p5piYEQUvKeSHmS8oeGcs6Ahgx5znBclqqqq37ZfioYANiTqJA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@ast-grep/napi-linux-x64-musl@0.40.0':
|
||||
resolution: {integrity: sha512-rG1YujF7O+lszX8fd5u6qkFTuv4FwHXjWvt1CCvCxXwQLSY96LaCW88oVKg7WoEYQh54y++Fk57F+Wh9Gv9nVQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@ast-grep/napi-win32-arm64-msvc@0.40.0':
|
||||
resolution: {integrity: sha512-9SqmnQqd4zTEUk6yx0TuW2ycZZs2+e569O/R0QnhSiQNpgwiJCYOe/yPS0BC9HkiaozQm6jjAcasWpFtz/dp+w==}
|
||||
@@ -1779,105 +1778,89 @@ packages:
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
@@ -1964,35 +1947,30 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.88':
|
||||
resolution: {integrity: sha512-kYyNrUsHLkoGHBc77u4Unh067GrfiCUMbGHC2+OTxbeWfZkPt2o32UOQkhnSswKd9Fko/wSqqGkY956bIUzruA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.88':
|
||||
resolution: {integrity: sha512-HVuH7QgzB0yavYdNZDRyAsn/ejoXB0hn8twwFnOqUbCCdkV+REna7RXjSR7+PdfW0qMQ2YYWsLvVBT5iL/mGpw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.88':
|
||||
resolution: {integrity: sha512-hvcvKIcPEQrvvJtJnwD35B3qk6umFJ8dFIr8bSymfrSMem0EQsfn1ztys8ETIFndTwdNWJKWluvxztA41ivsEw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.88':
|
||||
resolution: {integrity: sha512-eSMpGYY2xnZSQ6UxYJ6plDboxq4KeJ4zT5HaVkUnbObNN6DlbJe0Mclh3wifAmquXfrlgTZt6zhHsUgz++AK6g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.88':
|
||||
resolution: {integrity: sha512-qcIFfEgHrchyYqRrxsCeTQgpJZ/GqHiqPcU/Fvw/ARVlQeDX1VyFH+X+0gCR2tca6UJrq96vnW+5o7buCq+erA==}
|
||||
@@ -2039,28 +2017,24 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.0.10':
|
||||
resolution: {integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.0.10':
|
||||
resolution: {integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.0.10':
|
||||
resolution: {integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.0.10':
|
||||
resolution: {integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==}
|
||||
@@ -2750,79 +2724,66 @@ packages:
|
||||
resolution: {integrity: sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.56.0':
|
||||
resolution: {integrity: sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.56.0':
|
||||
resolution: {integrity: sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.56.0':
|
||||
resolution: {integrity: sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.56.0':
|
||||
resolution: {integrity: sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-loong64-musl@4.56.0':
|
||||
resolution: {integrity: sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.56.0':
|
||||
resolution: {integrity: sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-musl@4.56.0':
|
||||
resolution: {integrity: sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.56.0':
|
||||
resolution: {integrity: sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.56.0':
|
||||
resolution: {integrity: sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.56.0':
|
||||
resolution: {integrity: sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.56.0':
|
||||
resolution: {integrity: sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.56.0':
|
||||
resolution: {integrity: sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openbsd-x64@4.56.0':
|
||||
resolution: {integrity: sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==}
|
||||
@@ -3376,35 +3337,30 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-arm64-musl@2.9.6':
|
||||
resolution: {integrity: sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-linux-riscv64-gnu@2.9.6':
|
||||
resolution: {integrity: sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-gnu@2.9.6':
|
||||
resolution: {integrity: sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tauri-apps/cli-linux-x64-musl@2.9.6':
|
||||
resolution: {integrity: sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tauri-apps/cli-win32-arm64-msvc@2.9.6':
|
||||
resolution: {integrity: sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==}
|
||||
@@ -3825,49 +3781,41 @@ packages:
|
||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||
@@ -5288,6 +5236,9 @@ packages:
|
||||
functions-have-names@1.2.3:
|
||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||
|
||||
fzf@0.5.2:
|
||||
resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==}
|
||||
|
||||
gaxios@7.1.3:
|
||||
resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -13903,6 +13854,8 @@ snapshots:
|
||||
|
||||
functions-have-names@1.2.3: {}
|
||||
|
||||
fzf@0.5.2: {}
|
||||
|
||||
gaxios@7.1.3:
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
|
||||
Reference in New Issue
Block a user