diff --git a/apps/readest-app/src/components/settings/SettingsDialog.tsx b/apps/readest-app/src/components/settings/SettingsDialog.tsx
index 7c01b6dd..6fe49f93 100644
--- a/apps/readest-app/src/components/settings/SettingsDialog.tsx
+++ b/apps/readest-app/src/components/settings/SettingsDialog.tsx
@@ -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(null);
+ const panelRef = useRef(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 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 = {
+ 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 }) => {
))}
+
{activePanel === 'Font' && (
void;
+ isAvailable?: () => boolean;
+}
+
+export interface CommandSearchResult {
+ item: CommandItem;
+ score: number;
+ positions: Set;
+ highlightIndices: Set;
+ 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): Set => {
+ 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();
+
+ 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): 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 => {
+ const grouped: Record = {
+ settings: [],
+ actions: [],
+ navigation: [],
+ };
+
+ for (const result of results) {
+ grouped[result.item.category].push(result);
+ }
+
+ return grouped;
+};
+
+// settings panel icon map
+const panelIcons: Record = {
+ 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
+ }
+};
diff --git a/apps/readest-app/src/services/environment.ts b/apps/readest-app/src/services/environment.ts
index 7963bb8b..acbde2e5 100644
--- a/apps/readest-app/src/services/environment.ts
+++ b/apps/readest-app/src/services/environment.ts
@@ -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
diff --git a/apps/readest-app/src/store/settingsStore.ts b/apps/readest-app/src/store/settingsStore.ts
index 1bc130d3..e5ba274e 100644
--- a/apps/readest-app/src/store/settingsStore.ts
+++ b/apps/readest-app/src/store/settingsStore.ts
@@ -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((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((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;
diff --git a/apps/readest-app/src/styles/globals.css b/apps/readest-app/src/styles/globals.css
index 449c3a43..d2d18e8d 100644
--- a/apps/readest-app/src/styles/globals.css
+++ b/apps/readest-app/src/styles/globals.css
@@ -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;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b469b2dc..5d701e37 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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