mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Assign shortcuts by default to only the quick access items (#4622)
# Description of Changes
Change shortcuts to just be a limited set for Quick Access tools rather
than for everything to avoid breaking browser key commands by default.
Also fixes a bunch of types of variables that were representing
`ToolId`s (I stopped at `automate` because there's loads in there so
I've just introduced some `any` casts for now 😭)
This commit is contained in:
parent
d714a1617f
commit
3090a85726
@ -283,7 +283,8 @@
|
|||||||
"capturing": "Press keys… (Esc to cancel)",
|
"capturing": "Press keys… (Esc to cancel)",
|
||||||
"change": "Change shortcut",
|
"change": "Change shortcut",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"shortcut": "Shortcut"
|
"shortcut": "Shortcut",
|
||||||
|
"noShortcut": "No shortcut set"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"changeCreds": {
|
"changeCreds": {
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useToolWorkflow } from '../../../../contexts/ToolWorkflowContext';
|
import { useToolWorkflow } from '../../../../contexts/ToolWorkflowContext';
|
||||||
import { useHotkeys } from '../../../../contexts/HotkeyContext';
|
import { useHotkeys } from '../../../../contexts/HotkeyContext';
|
||||||
import HotkeyDisplay from '../../../hotkeys/HotkeyDisplay';
|
import HotkeyDisplay from '../../../hotkeys/HotkeyDisplay';
|
||||||
import { bindingEquals, eventToBinding } from '../../../../utils/hotkeys';
|
import { bindingEquals, eventToBinding, HotkeyBinding } from '../../../../utils/hotkeys';
|
||||||
|
import { ToolId } from 'src/types/toolId';
|
||||||
|
import { ToolRegistryEntry } from 'src/data/toolsTaxonomy';
|
||||||
|
|
||||||
const rowStyle: React.CSSProperties = {
|
const rowStyle: React.CSSProperties = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -24,10 +26,10 @@ const HotkeysSection: React.FC = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toolRegistry } = useToolWorkflow();
|
const { toolRegistry } = useToolWorkflow();
|
||||||
const { hotkeys, defaults, updateHotkey, resetHotkey, pauseHotkeys, resumeHotkeys, getDisplayParts, isMac } = useHotkeys();
|
const { hotkeys, defaults, updateHotkey, resetHotkey, pauseHotkeys, resumeHotkeys, getDisplayParts, isMac } = useHotkeys();
|
||||||
const [editingTool, setEditingTool] = useState<string | null>(null);
|
const [editingTool, setEditingTool] = useState<ToolId | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const tools = useMemo(() => Object.entries(toolRegistry), [toolRegistry]);
|
const tools = useMemo(() => Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][], [toolRegistry]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editingTool) {
|
if (!editingTool) {
|
||||||
@ -64,7 +66,7 @@ const HotkeysSection: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const conflictEntry = Object.entries(hotkeys).find(([toolId, existing]) => (
|
const conflictEntry = (Object.entries(hotkeys) as [ToolId, HotkeyBinding][]).find(([toolId, existing]) => (
|
||||||
toolId !== editingTool && bindingEquals(existing, binding)
|
toolId !== editingTool && bindingEquals(existing, binding)
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -85,7 +87,7 @@ const HotkeysSection: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, [editingTool, hotkeys, toolRegistry, updateHotkey, t]);
|
}, [editingTool, hotkeys, toolRegistry, updateHotkey, t]);
|
||||||
|
|
||||||
const handleStartCapture = (toolId: string) => {
|
const handleStartCapture = (toolId: ToolId) => {
|
||||||
setEditingTool(toolId);
|
setEditingTool(toolId);
|
||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,9 +7,10 @@ import { useToolSections } from '../../hooks/useToolSections';
|
|||||||
import SubcategoryHeader from './shared/SubcategoryHeader';
|
import SubcategoryHeader from './shared/SubcategoryHeader';
|
||||||
import NoToolsFound from './shared/NoToolsFound';
|
import NoToolsFound from './shared/NoToolsFound';
|
||||||
import "./toolPicker/ToolPicker.css";
|
import "./toolPicker/ToolPicker.css";
|
||||||
|
import { ToolId } from 'src/types/toolId';
|
||||||
|
|
||||||
interface SearchResultsProps {
|
interface SearchResultsProps {
|
||||||
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>;
|
filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
searchQuery?: string;
|
searchQuery?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,11 +6,12 @@ import "./toolPicker/ToolPicker.css";
|
|||||||
import { useToolSections } from "../../hooks/useToolSections";
|
import { useToolSections } from "../../hooks/useToolSections";
|
||||||
import NoToolsFound from "./shared/NoToolsFound";
|
import NoToolsFound from "./shared/NoToolsFound";
|
||||||
import { renderToolButtons } from "./shared/renderToolButtons";
|
import { renderToolButtons } from "./shared/renderToolButtons";
|
||||||
|
import { ToolId } from "src/types/toolId";
|
||||||
|
|
||||||
interface ToolPickerProps {
|
interface ToolPickerProps {
|
||||||
selectedToolKey: string | null;
|
selectedToolKey: string | null;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>;
|
filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>;
|
||||||
isSearching?: boolean;
|
isSearching?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,10 @@ import { Suspense } from "react";
|
|||||||
import { useToolWorkflow } from "../../contexts/ToolWorkflowContext";
|
import { useToolWorkflow } from "../../contexts/ToolWorkflowContext";
|
||||||
import { BaseToolProps } from "../../types/tool";
|
import { BaseToolProps } from "../../types/tool";
|
||||||
import ToolLoadingFallback from "./ToolLoadingFallback";
|
import ToolLoadingFallback from "./ToolLoadingFallback";
|
||||||
|
import { ToolId } from "src/types/toolId";
|
||||||
|
|
||||||
interface ToolRendererProps extends BaseToolProps {
|
interface ToolRendererProps extends BaseToolProps {
|
||||||
selectedToolKey: string;
|
selectedToolKey: ToolId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useToolSections } from '../../../hooks/useToolSections';
|
|||||||
import { renderToolButtons } from '../shared/renderToolButtons';
|
import { renderToolButtons } from '../shared/renderToolButtons';
|
||||||
import ToolSearch from '../toolPicker/ToolSearch';
|
import ToolSearch from '../toolPicker/ToolSearch';
|
||||||
import ToolButton from '../toolPicker/ToolButton';
|
import ToolButton from '../toolPicker/ToolButton';
|
||||||
|
import { ToolId } from 'src/types/toolId';
|
||||||
|
|
||||||
interface ToolSelectorProps {
|
interface ToolSelectorProps {
|
||||||
onSelect: (toolKey: string) => void;
|
onSelect: (toolKey: string) => void;
|
||||||
@ -30,7 +31,7 @@ export default function ToolSelector({
|
|||||||
|
|
||||||
// Filter out excluded tools (like 'automate' itself) and tools that don't support automation
|
// Filter out excluded tools (like 'automate' itself) and tools that don't support automation
|
||||||
const baseFilteredTools = useMemo(() => {
|
const baseFilteredTools = useMemo(() => {
|
||||||
return Object.entries(toolRegistry).filter(([key, tool]) =>
|
return (Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][]).filter(([key, tool]) =>
|
||||||
!excludeTools.includes(key) && getToolSupportsAutomate(tool)
|
!excludeTools.includes(key) && getToolSupportsAutomate(tool)
|
||||||
);
|
);
|
||||||
}, [toolRegistry, excludeTools]);
|
}, [toolRegistry, excludeTools]);
|
||||||
@ -66,7 +67,7 @@ export default function ToolSelector({
|
|||||||
}, [filteredTools]);
|
}, [filteredTools]);
|
||||||
|
|
||||||
// Use the same tool sections logic as the main ToolPicker
|
// Use the same tool sections logic as the main ToolPicker
|
||||||
const { sections, searchGroups } = useToolSections(transformedFilteredTools);
|
const { sections, searchGroups } = useToolSections(transformedFilteredTools as any /* FIX ME */);
|
||||||
|
|
||||||
// Determine what to display: search results or organized sections
|
// Determine what to display: search results or organized sections
|
||||||
const isSearching = searchTerm.trim().length > 0;
|
const isSearching = searchTerm.trim().length > 0;
|
||||||
@ -156,7 +157,7 @@ export default function ToolSelector({
|
|||||||
// Show selected tool in AutomationEntry style when tool is selected and dropdown closed
|
// Show selected tool in AutomationEntry style when tool is selected and dropdown closed
|
||||||
<div onClick={handleSearchFocus} style={{ cursor: 'pointer',
|
<div onClick={handleSearchFocus} style={{ cursor: 'pointer',
|
||||||
borderRadius: "var(--mantine-radius-lg)" }}>
|
borderRadius: "var(--mantine-radius-lg)" }}>
|
||||||
<ToolButton id='tool' tool={toolRegistry[selectedValue]} isSelected={false}
|
<ToolButton id={'tool' as any /* FIX ME */} tool={toolRegistry[selectedValue]} isSelected={false}
|
||||||
onSelect={()=>{}} rounded={true} disableNavigation={true}></ToolButton>
|
onSelect={()=>{}} rounded={true} disableNavigation={true}></ToolButton>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -5,13 +5,14 @@ import SubcategoryHeader from './SubcategoryHeader';
|
|||||||
import { getSubcategoryLabel } from "../../../data/toolsTaxonomy";
|
import { getSubcategoryLabel } from "../../../data/toolsTaxonomy";
|
||||||
import { TFunction } from 'i18next';
|
import { TFunction } from 'i18next';
|
||||||
import { SubcategoryGroup } from '../../../hooks/useToolSections';
|
import { SubcategoryGroup } from '../../../hooks/useToolSections';
|
||||||
|
import { ToolId } from 'src/types/toolId';
|
||||||
|
|
||||||
// Helper function to render tool buttons for a subcategory
|
// Helper function to render tool buttons for a subcategory
|
||||||
export const renderToolButtons = (
|
export const renderToolButtons = (
|
||||||
t: TFunction,
|
t: TFunction,
|
||||||
subcategory: SubcategoryGroup,
|
subcategory: SubcategoryGroup,
|
||||||
selectedToolKey: string | null,
|
selectedToolKey: string | null,
|
||||||
onSelect: (id: string) => void,
|
onSelect: (id: ToolId) => void,
|
||||||
showSubcategoryHeader: boolean = true,
|
showSubcategoryHeader: boolean = true,
|
||||||
disableNavigation: boolean = false,
|
disableNavigation: boolean = false,
|
||||||
searchResults?: Array<{ item: [string, any]; matchedText?: string }>
|
searchResults?: Array<{ item: [string, any]; matchedText?: string }>
|
||||||
|
|||||||
@ -9,12 +9,13 @@ import { handleUnlessSpecialClick } from "../../../utils/clickHandlers";
|
|||||||
import FitText from "../../shared/FitText";
|
import FitText from "../../shared/FitText";
|
||||||
import { useHotkeys } from "../../../contexts/HotkeyContext";
|
import { useHotkeys } from "../../../contexts/HotkeyContext";
|
||||||
import HotkeyDisplay from "../../hotkeys/HotkeyDisplay";
|
import HotkeyDisplay from "../../hotkeys/HotkeyDisplay";
|
||||||
|
import { ToolId } from "src/types/toolId";
|
||||||
|
|
||||||
interface ToolButtonProps {
|
interface ToolButtonProps {
|
||||||
id: string;
|
id: ToolId;
|
||||||
tool: ToolRegistryEntry;
|
tool: ToolRegistryEntry;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: ToolId) => void;
|
||||||
rounded?: boolean;
|
rounded?: boolean;
|
||||||
disableNavigation?: boolean;
|
disableNavigation?: boolean;
|
||||||
matchedSynonym?: string;
|
matchedSynonym?: string;
|
||||||
@ -28,7 +29,7 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
|||||||
const binding = hotkeys[id];
|
const binding = hotkeys[id];
|
||||||
const { getToolNavigation } = useToolNavigation();
|
const { getToolNavigation } = useToolNavigation();
|
||||||
|
|
||||||
const handleClick = (id: string) => {
|
const handleClick = (id: ToolId) => {
|
||||||
if (isUnavailable) return;
|
if (isUnavailable) return;
|
||||||
if (tool.link) {
|
if (tool.link) {
|
||||||
// Open external link in new tab
|
// Open external link in new tab
|
||||||
@ -47,12 +48,16 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
|||||||
: (
|
: (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
||||||
<span>{tool.description}</span>
|
<span>{tool.description}</span>
|
||||||
{binding && (
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.75rem' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', fontSize: '0.75rem' }}>
|
{binding ? (
|
||||||
|
<>
|
||||||
<span style={{ color: 'var(--mantine-color-dimmed)', fontWeight: 500 }}>{t('settings.hotkeys.shortcut', 'Shortcut')}</span>
|
<span style={{ color: 'var(--mantine-color-dimmed)', fontWeight: 500 }}>{t('settings.hotkeys.shortcut', 'Shortcut')}</span>
|
||||||
<HotkeyDisplay binding={binding} />
|
<HotkeyDisplay binding={binding} />
|
||||||
</div>
|
</>
|
||||||
|
) : (
|
||||||
|
<span style={{ color: 'var(--mantine-color-dimmed)', fontWeight: 500, fontStyle: 'italic' }}>{t('settings.hotkeys.noShortcut', 'No shortcut set')}</span>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -2,14 +2,17 @@ import React, { createContext, useCallback, useContext, useEffect, useMemo, useS
|
|||||||
import { HotkeyBinding, bindingEquals, bindingMatchesEvent, deserializeBindings, getDisplayParts, isMacLike, normalizeBinding, serializeBindings } from '../utils/hotkeys';
|
import { HotkeyBinding, bindingEquals, bindingMatchesEvent, deserializeBindings, getDisplayParts, isMacLike, normalizeBinding, serializeBindings } from '../utils/hotkeys';
|
||||||
import { useToolWorkflow } from './ToolWorkflowContext';
|
import { useToolWorkflow } from './ToolWorkflowContext';
|
||||||
import { ToolId } from '../types/toolId';
|
import { ToolId } from '../types/toolId';
|
||||||
|
import { ToolCategoryId, ToolRegistryEntry } from '../data/toolsTaxonomy';
|
||||||
|
|
||||||
|
type Bindings = Partial<Record<ToolId, HotkeyBinding>>;
|
||||||
|
|
||||||
interface HotkeyContextValue {
|
interface HotkeyContextValue {
|
||||||
hotkeys: Record<string, HotkeyBinding>;
|
hotkeys: Bindings;
|
||||||
defaults: Record<string, HotkeyBinding>;
|
defaults: Bindings;
|
||||||
isMac: boolean;
|
isMac: boolean;
|
||||||
updateHotkey: (toolId: string, binding: HotkeyBinding) => void;
|
updateHotkey: (toolId: ToolId, binding: HotkeyBinding) => void;
|
||||||
resetHotkey: (toolId: string) => void;
|
resetHotkey: (toolId: ToolId) => void;
|
||||||
isBindingAvailable: (binding: HotkeyBinding, excludeToolId?: string) => boolean;
|
isBindingAvailable: (binding: HotkeyBinding, excludeToolId?: ToolId) => boolean;
|
||||||
pauseHotkeys: () => void;
|
pauseHotkeys: () => void;
|
||||||
resumeHotkeys: () => void;
|
resumeHotkeys: () => void;
|
||||||
areHotkeysPaused: boolean;
|
areHotkeysPaused: boolean;
|
||||||
@ -20,46 +23,29 @@ const HotkeyContext = createContext<HotkeyContextValue | undefined>(undefined);
|
|||||||
|
|
||||||
const STORAGE_KEY = 'stirlingpdf.hotkeys';
|
const STORAGE_KEY = 'stirlingpdf.hotkeys';
|
||||||
|
|
||||||
const KEY_ORDER: string[] = [
|
const generateDefaultHotkeys = (toolEntries: [ToolId, ToolRegistryEntry][], macLike: boolean): Bindings => {
|
||||||
'Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5', 'Digit6', 'Digit7', 'Digit8', 'Digit9', 'Digit0',
|
const defaults: Bindings = {};
|
||||||
'KeyQ', 'KeyW', 'KeyE', 'KeyR', 'KeyT', 'KeyY', 'KeyU', 'KeyI', 'KeyO', 'KeyP',
|
|
||||||
'KeyA', 'KeyS', 'KeyD', 'KeyF', 'KeyG', 'KeyH', 'KeyJ', 'KeyK', 'KeyL',
|
|
||||||
'KeyZ', 'KeyX', 'KeyC', 'KeyV', 'KeyB', 'KeyN', 'KeyM',
|
|
||||||
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
|
|
||||||
];
|
|
||||||
|
|
||||||
const generateDefaultHotkeys = (toolIds: string[], macLike: boolean): Record<string, HotkeyBinding> => {
|
// Get Quick Access tools (RECOMMENDED_TOOLS category) from registry
|
||||||
const defaults: Record<string, HotkeyBinding> = {};
|
const quickAccessTools = toolEntries
|
||||||
let index = 0;
|
.filter(([_, tool]) => tool.categoryId === ToolCategoryId.RECOMMENDED_TOOLS)
|
||||||
let useShift = false;
|
.map(([toolId, _]) => toolId);
|
||||||
|
|
||||||
const nextBinding = (): HotkeyBinding => {
|
// Assign Cmd+Option+Number (Mac) or Ctrl+Alt+Number (Windows) to Quick Access tools
|
||||||
if (index >= KEY_ORDER.length) {
|
quickAccessTools.forEach((toolId, index) => {
|
||||||
index = 0;
|
if (index < 9) { // Limit to Digit1-9
|
||||||
if (!useShift) {
|
const digitNumber = index + 1;
|
||||||
useShift = true;
|
defaults[toolId] = {
|
||||||
} else {
|
code: `Digit${digitNumber}`,
|
||||||
// If we somehow run out of combinations, wrap back around (unlikely given tool count)
|
alt: true,
|
||||||
useShift = false;
|
shift: false,
|
||||||
}
|
meta: macLike,
|
||||||
|
ctrl: !macLike,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const code = KEY_ORDER[index];
|
|
||||||
index += 1;
|
|
||||||
|
|
||||||
return {
|
|
||||||
code,
|
|
||||||
alt: true,
|
|
||||||
shift: useShift,
|
|
||||||
meta: macLike,
|
|
||||||
ctrl: !macLike,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
toolIds.forEach(toolId => {
|
|
||||||
defaults[toolId] = nextBinding();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// All other tools have no default (will be undefined in the record)
|
||||||
return defaults;
|
return defaults;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -74,7 +60,7 @@ const shouldIgnoreTarget = (target: EventTarget | null): boolean => {
|
|||||||
export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const { toolRegistry, handleToolSelect } = useToolWorkflow();
|
const { toolRegistry, handleToolSelect } = useToolWorkflow();
|
||||||
const isMac = useMemo(() => isMacLike(), []);
|
const isMac = useMemo(() => isMacLike(), []);
|
||||||
const [customBindings, setCustomBindings] = useState<Record<string, HotkeyBinding>>(() => {
|
const [customBindings, setCustomBindings] = useState<Bindings>(() => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@ -82,16 +68,16 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
});
|
});
|
||||||
const [areHotkeysPaused, setHotkeysPaused] = useState(false);
|
const [areHotkeysPaused, setHotkeysPaused] = useState(false);
|
||||||
|
|
||||||
const toolIds = useMemo(() => Object.keys(toolRegistry), [toolRegistry]);
|
const toolEntries = useMemo(() => Object.entries(toolRegistry), [toolRegistry]) as [ToolId, ToolRegistryEntry][];
|
||||||
|
|
||||||
const defaults = useMemo(() => generateDefaultHotkeys(toolIds, isMac), [toolIds, isMac]);
|
const defaults = useMemo(() => generateDefaultHotkeys(toolEntries, isMac), [toolRegistry, isMac]);
|
||||||
|
|
||||||
// Remove bindings for tools that are no longer present
|
// Remove bindings for tools that are no longer present
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCustomBindings(prev => {
|
setCustomBindings(prev => {
|
||||||
const next: Record<string, HotkeyBinding> = {};
|
const next: Bindings = {};
|
||||||
let changed = false;
|
let changed = false;
|
||||||
Object.entries(prev).forEach(([toolId, binding]) => {
|
(Object.entries(prev) as [ToolId, HotkeyBinding][]).forEach(([toolId, binding]) => {
|
||||||
if (toolRegistry[toolId]) {
|
if (toolRegistry[toolId]) {
|
||||||
next[toolId] = binding;
|
next[toolId] = binding;
|
||||||
} else {
|
} else {
|
||||||
@ -103,13 +89,21 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
}, [toolRegistry]);
|
}, [toolRegistry]);
|
||||||
|
|
||||||
const resolved = useMemo(() => {
|
const resolved = useMemo(() => {
|
||||||
const merged: Record<string, HotkeyBinding> = {};
|
const merged: Bindings = {};
|
||||||
toolIds.forEach(toolId => {
|
toolEntries.forEach(([toolId, _]) => {
|
||||||
const custom = customBindings[toolId];
|
const custom = customBindings[toolId];
|
||||||
merged[toolId] = custom ? normalizeBinding(custom) : defaults[toolId];
|
const defaultBinding = defaults[toolId];
|
||||||
|
|
||||||
|
// Only add to resolved if there's a custom binding or a default binding
|
||||||
|
if (custom) {
|
||||||
|
merged[toolId] = normalizeBinding(custom);
|
||||||
|
} else if (defaultBinding) {
|
||||||
|
merged[toolId] = defaultBinding;
|
||||||
|
}
|
||||||
|
// If neither exists, don't add to merged (tool has no hotkey)
|
||||||
});
|
});
|
||||||
return merged;
|
return merged;
|
||||||
}, [customBindings, defaults, toolIds]);
|
}, [customBindings, defaults, toolEntries]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
@ -118,7 +112,7 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
window.localStorage.setItem(STORAGE_KEY, serializeBindings(customBindings));
|
window.localStorage.setItem(STORAGE_KEY, serializeBindings(customBindings));
|
||||||
}, [customBindings]);
|
}, [customBindings]);
|
||||||
|
|
||||||
const isBindingAvailable = useCallback((binding: HotkeyBinding, excludeToolId?: string) => {
|
const isBindingAvailable = useCallback((binding: HotkeyBinding, excludeToolId?: ToolId) => {
|
||||||
const normalized = normalizeBinding(binding);
|
const normalized = normalizeBinding(binding);
|
||||||
return Object.entries(resolved).every(([toolId, existing]) => {
|
return Object.entries(resolved).every(([toolId, existing]) => {
|
||||||
if (toolId === excludeToolId) {
|
if (toolId === excludeToolId) {
|
||||||
@ -128,7 +122,7 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
});
|
});
|
||||||
}, [resolved]);
|
}, [resolved]);
|
||||||
|
|
||||||
const updateHotkey = useCallback((toolId: string, binding: HotkeyBinding) => {
|
const updateHotkey = useCallback((toolId: ToolId, binding: HotkeyBinding) => {
|
||||||
setCustomBindings(prev => {
|
setCustomBindings(prev => {
|
||||||
const normalized = normalizeBinding(binding);
|
const normalized = normalizeBinding(binding);
|
||||||
const defaultsForTool = defaults[toolId];
|
const defaultsForTool = defaults[toolId];
|
||||||
@ -142,7 +136,7 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
});
|
});
|
||||||
}, [defaults]);
|
}, [defaults]);
|
||||||
|
|
||||||
const resetHotkey = useCallback((toolId: string) => {
|
const resetHotkey = useCallback((toolId: ToolId) => {
|
||||||
setCustomBindings(prev => {
|
setCustomBindings(prev => {
|
||||||
if (!(toolId in prev)) {
|
if (!(toolId in prev)) {
|
||||||
return prev;
|
return prev;
|
||||||
@ -165,12 +159,12 @@ export const HotkeyProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
|||||||
if (event.repeat) return;
|
if (event.repeat) return;
|
||||||
if (shouldIgnoreTarget(event.target)) return;
|
if (shouldIgnoreTarget(event.target)) return;
|
||||||
|
|
||||||
const entries = Object.entries(resolved) as Array<[string, HotkeyBinding]>;
|
const entries = Object.entries(resolved) as [ToolId, HotkeyBinding][];
|
||||||
for (const [toolId, binding] of entries) {
|
for (const [toolId, binding] of entries) {
|
||||||
if (bindingMatchesEvent(binding, event)) {
|
if (bindingMatchesEvent(binding, event)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handleToolSelect(toolId as ToolId);
|
handleToolSelect(toolId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,10 +73,10 @@ function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowActio
|
|||||||
// Context value interface
|
// Context value interface
|
||||||
interface ToolWorkflowContextValue extends ToolWorkflowState {
|
interface ToolWorkflowContextValue extends ToolWorkflowState {
|
||||||
// Tool management (from hook)
|
// Tool management (from hook)
|
||||||
selectedToolKey: string | null;
|
selectedToolKey: ToolId | null;
|
||||||
selectedTool: ToolRegistryEntry | null;
|
selectedTool: ToolRegistryEntry | null;
|
||||||
toolRegistry: Record<string, ToolRegistryEntry>;
|
toolRegistry: Partial<ToolRegistry>;
|
||||||
getSelectedTool: (toolId: string | null) => ToolRegistryEntry | null;
|
getSelectedTool: (toolId: ToolId | null) => ToolRegistryEntry | null;
|
||||||
|
|
||||||
// UI Actions
|
// UI Actions
|
||||||
setSidebarsVisible: (visible: boolean) => void;
|
setSidebarsVisible: (visible: boolean) => void;
|
||||||
@ -101,7 +101,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
|
|||||||
handleReaderToggle: () => void;
|
handleReaderToggle: () => void;
|
||||||
|
|
||||||
// Computed values
|
// Computed values
|
||||||
filteredTools: Array<{ item: [string, ToolRegistryEntry]; matchedText?: string }>; // Filtered by search
|
filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>; // Filtered by search
|
||||||
isPanelVisible: boolean;
|
isPanelVisible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -824,7 +824,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
name: t("home.compare.title", "Compare"),
|
name: t("home.compare.title", "Compare"),
|
||||||
component: null,
|
component: null,
|
||||||
description: t("home.compare.desc", "Compare two PDF documents and highlight differences"),
|
description: t("home.compare.desc", "Compare two PDF documents and highlight differences"),
|
||||||
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS /* TODO: Change to RECOMMENDED_TOOLS when component is implemented */,
|
||||||
subcategoryId: SubcategoryId.GENERAL,
|
subcategoryId: SubcategoryId.GENERAL,
|
||||||
synonyms: getSynonyms(t, "compare"),
|
synonyms: getSynonyms(t, "compare"),
|
||||||
supportsAutomate: false,
|
supportsAutomate: false,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
|||||||
import { useToolNavigation } from './useToolNavigation';
|
import { useToolNavigation } from './useToolNavigation';
|
||||||
import { useToolWorkflow } from '../contexts/ToolWorkflowContext';
|
import { useToolWorkflow } from '../contexts/ToolWorkflowContext';
|
||||||
import { handleUnlessSpecialClick } from '../utils/clickHandlers';
|
import { handleUnlessSpecialClick } from '../utils/clickHandlers';
|
||||||
|
import { ToolId } from 'src/types/toolId';
|
||||||
|
|
||||||
export interface SidebarNavigationProps {
|
export interface SidebarNavigationProps {
|
||||||
/** Full URL for the navigation (for href attribute) */
|
/** Full URL for the navigation (for href attribute) */
|
||||||
@ -16,7 +17,7 @@ export interface SidebarNavigationProps {
|
|||||||
*/
|
*/
|
||||||
export function useSidebarNavigation(): {
|
export function useSidebarNavigation(): {
|
||||||
getHomeNavigation: () => SidebarNavigationProps;
|
getHomeNavigation: () => SidebarNavigationProps;
|
||||||
getToolNavigation: (toolId: string) => SidebarNavigationProps | null;
|
getToolNavigation: (toolId: ToolId) => SidebarNavigationProps | null;
|
||||||
} {
|
} {
|
||||||
const { getToolNavigation: getToolNavProps } = useToolNavigation();
|
const { getToolNavigation: getToolNavProps } = useToolNavigation();
|
||||||
const { getSelectedTool } = useToolWorkflow();
|
const { getSelectedTool } = useToolWorkflow();
|
||||||
@ -32,7 +33,7 @@ export function useSidebarNavigation(): {
|
|||||||
return { href, onClick: defaultNavClick };
|
return { href, onClick: defaultNavClick };
|
||||||
}, [defaultNavClick]);
|
}, [defaultNavClick]);
|
||||||
|
|
||||||
const getToolNavigation = useCallback((toolId: string): SidebarNavigationProps | null => {
|
const getToolNavigation = useCallback((toolId: ToolId): SidebarNavigationProps | null => {
|
||||||
// Handle special nav sections that aren't tools
|
// Handle special nav sections that aren't tools
|
||||||
if (toolId === 'read') return { href: '/read', onClick: defaultNavClick };
|
if (toolId === 'read') return { href: '/read', onClick: defaultNavClick };
|
||||||
if (toolId === 'automate') return { href: '/automate', onClick: defaultNavClick };
|
if (toolId === 'automate') return { href: '/automate', onClick: defaultNavClick };
|
||||||
|
|||||||
@ -1,16 +1,17 @@
|
|||||||
import { useState, useCallback, useMemo } from 'react';
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
|
import { useFlatToolRegistry } from "../data/useTranslatedToolRegistry";
|
||||||
import { getAllEndpoints, type ToolRegistryEntry } from "../data/toolsTaxonomy";
|
import { getAllEndpoints, type ToolRegistryEntry, type ToolRegistry } from "../data/toolsTaxonomy";
|
||||||
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
|
||||||
import { FileId } from '../types/file';
|
import { FileId } from '../types/file';
|
||||||
|
import { ToolId } from 'src/types/toolId';
|
||||||
|
|
||||||
interface ToolManagementResult {
|
interface ToolManagementResult {
|
||||||
selectedTool: ToolRegistryEntry | null;
|
selectedTool: ToolRegistryEntry | null;
|
||||||
toolSelectedFileIds: FileId[];
|
toolSelectedFileIds: FileId[];
|
||||||
toolRegistry: Record<string, ToolRegistryEntry>;
|
toolRegistry: Partial<ToolRegistry>;
|
||||||
setToolSelectedFileIds: (fileIds: FileId[]) => void;
|
setToolSelectedFileIds: (fileIds: FileId[]) => void;
|
||||||
getSelectedTool: (toolKey: string | null) => ToolRegistryEntry | null;
|
getSelectedTool: (toolKey: ToolId | null) => ToolRegistryEntry | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useToolManagement = (): ToolManagementResult => {
|
export const useToolManagement = (): ToolManagementResult => {
|
||||||
@ -30,9 +31,9 @@ export const useToolManagement = (): ToolManagementResult => {
|
|||||||
return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
|
return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
|
||||||
}, [endpointsLoading, endpointStatus, baseRegistry]);
|
}, [endpointsLoading, endpointStatus, baseRegistry]);
|
||||||
|
|
||||||
const toolRegistry: Record<string, ToolRegistryEntry> = useMemo(() => {
|
const toolRegistry: Partial<ToolRegistry> = useMemo(() => {
|
||||||
const availableToolRegistry: Record<string, ToolRegistryEntry> = {};
|
const availableToolRegistry: Partial<ToolRegistry> = {};
|
||||||
Object.keys(baseRegistry).forEach(toolKey => {
|
(Object.keys(baseRegistry) as ToolId[]).forEach(toolKey => {
|
||||||
if (isToolAvailable(toolKey)) {
|
if (isToolAvailable(toolKey)) {
|
||||||
const baseTool = baseRegistry[toolKey as keyof typeof baseRegistry];
|
const baseTool = baseRegistry[toolKey as keyof typeof baseRegistry];
|
||||||
availableToolRegistry[toolKey] = {
|
availableToolRegistry[toolKey] = {
|
||||||
@ -45,7 +46,7 @@ export const useToolManagement = (): ToolManagementResult => {
|
|||||||
return availableToolRegistry;
|
return availableToolRegistry;
|
||||||
}, [isToolAvailable, t, baseRegistry]);
|
}, [isToolAvailable, t, baseRegistry]);
|
||||||
|
|
||||||
const getSelectedTool = useCallback((toolKey: string | null): ToolRegistryEntry | null => {
|
const getSelectedTool = useCallback((toolKey: ToolId | null): ToolRegistryEntry | null => {
|
||||||
return toolKey ? toolRegistry[toolKey] || null : null;
|
return toolKey ? toolRegistry[toolKey] || null : null;
|
||||||
}, [toolRegistry]);
|
}, [toolRegistry]);
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,10 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
import { SUBCATEGORY_ORDER, SubcategoryId, ToolCategoryId, ToolRegistryEntry } from '../data/toolsTaxonomy';
|
import { SUBCATEGORY_ORDER, SubcategoryId, ToolCategoryId, ToolRegistryEntry } from '../data/toolsTaxonomy';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ToolId } from 'src/types/toolId';
|
||||||
|
|
||||||
type SubcategoryIdMap = {
|
type SubcategoryIdMap = {
|
||||||
[subcategoryId in SubcategoryId]: Array<{ id: string /* FIX ME: Should be ToolId */; tool: ToolRegistryEntry }>;
|
[subcategoryId in SubcategoryId]: Array<{ id: ToolId; tool: ToolRegistryEntry }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type GroupedTools = {
|
type GroupedTools = {
|
||||||
@ -14,7 +15,7 @@ type GroupedTools = {
|
|||||||
export interface SubcategoryGroup {
|
export interface SubcategoryGroup {
|
||||||
subcategoryId: SubcategoryId;
|
subcategoryId: SubcategoryId;
|
||||||
tools: {
|
tools: {
|
||||||
id: string /* FIX ME: Should be ToolId */;
|
id: ToolId;
|
||||||
tool: ToolRegistryEntry;
|
tool: ToolRegistryEntry;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
@ -28,7 +29,7 @@ export interface ToolSection {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useToolSections(
|
export function useToolSections(
|
||||||
filteredTools: Array<{ item: [string /* FIX ME: Should be ToolId */, ToolRegistryEntry]; matchedText?: string }>,
|
filteredTools: Array<{ item: [ToolId, ToolRegistryEntry]; matchedText?: string }>,
|
||||||
searchQuery?: string
|
searchQuery?: string
|
||||||
) {
|
) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -107,9 +108,9 @@ export function useToolSections(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const subMap = {} as SubcategoryIdMap;
|
const subMap = {} as SubcategoryIdMap;
|
||||||
const seen = new Set<string /* FIX ME: Should be ToolId */>();
|
const seen = new Set<ToolId>();
|
||||||
filteredTools.forEach(({ item: [id, tool] }) => {
|
filteredTools.forEach(({ item: [id, tool] }) => {
|
||||||
const toolId = id as string /* FIX ME: Should be ToolId */;
|
const toolId = id as ToolId;
|
||||||
if (seen.has(toolId)) return;
|
if (seen.has(toolId)) return;
|
||||||
seen.add(toolId);
|
seen.add(toolId);
|
||||||
const sub = tool.subcategoryId;
|
const sub = tool.subcategoryId;
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
|
import { ToolId } from "src/types/toolId";
|
||||||
import { ToolRegistryEntry } from "../data/toolsTaxonomy";
|
import { ToolRegistryEntry } from "../data/toolsTaxonomy";
|
||||||
import { scoreMatch, minScoreForQuery, normalizeForSearch } from "./fuzzySearch";
|
import { scoreMatch, minScoreForQuery, normalizeForSearch } from "./fuzzySearch";
|
||||||
|
|
||||||
export interface RankedToolItem {
|
export interface RankedToolItem {
|
||||||
item: [string, ToolRegistryEntry];
|
item: [ToolId, ToolRegistryEntry];
|
||||||
matchedText?: string;
|
matchedText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10,18 +11,18 @@ export function filterToolRegistryByQuery(
|
|||||||
toolRegistry: Record<string, ToolRegistryEntry>,
|
toolRegistry: Record<string, ToolRegistryEntry>,
|
||||||
query: string
|
query: string
|
||||||
): RankedToolItem[] {
|
): RankedToolItem[] {
|
||||||
const entries = Object.entries(toolRegistry);
|
const entries = Object.entries(toolRegistry) as [ToolId, ToolRegistryEntry][];
|
||||||
if (!query.trim()) {
|
if (!query.trim()) {
|
||||||
return entries.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] }));
|
return entries.map(([id, tool]) => ({ item: [id, tool] as [ToolId, ToolRegistryEntry] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const nq = normalizeForSearch(query);
|
const nq = normalizeForSearch(query);
|
||||||
const threshold = minScoreForQuery(query);
|
const threshold = minScoreForQuery(query);
|
||||||
|
|
||||||
const exactName: Array<{ id: string; tool: ToolRegistryEntry; pos: number }> = [];
|
const exactName: Array<{ id: ToolId; tool: ToolRegistryEntry; pos: number }> = [];
|
||||||
const exactSyn: Array<{ id: string; tool: ToolRegistryEntry; text: string; pos: number }> = [];
|
const exactSyn: Array<{ id: ToolId; tool: ToolRegistryEntry; text: string; pos: number }> = [];
|
||||||
const fuzzyName: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = [];
|
const fuzzyName: Array<{ id: ToolId; tool: ToolRegistryEntry; score: number; text: string }> = [];
|
||||||
const fuzzySyn: Array<{ id: string; tool: ToolRegistryEntry; score: number; text: string }> = [];
|
const fuzzySyn: Array<{ id: ToolId; tool: ToolRegistryEntry; score: number; text: string }> = [];
|
||||||
|
|
||||||
for (const [id, tool] of entries) {
|
for (const [id, tool] of entries) {
|
||||||
const nameNorm = normalizeForSearch(tool.name || '');
|
const nameNorm = normalizeForSearch(tool.name || '');
|
||||||
@ -78,7 +79,7 @@ export function filterToolRegistryByQuery(
|
|||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const ordered: RankedToolItem[] = [];
|
const ordered: RankedToolItem[] = [];
|
||||||
|
|
||||||
const push = (id: string, tool: ToolRegistryEntry, matchedText?: string) => {
|
const push = (id: ToolId, tool: ToolRegistryEntry, matchedText?: string) => {
|
||||||
if (seen.has(id)) return;
|
if (seen.has(id)) return;
|
||||||
seen.add(id);
|
seen.add(id);
|
||||||
ordered.push({ item: [id, tool], matchedText });
|
ordered.push({ item: [id, tool], matchedText });
|
||||||
@ -92,7 +93,7 @@ export function filterToolRegistryByQuery(
|
|||||||
if (ordered.length > 0) return ordered;
|
if (ordered.length > 0) return ordered;
|
||||||
|
|
||||||
// Fallback: return everything unchanged
|
// Fallback: return everything unchanged
|
||||||
return entries.map(([id, tool]) => ({ item: [id, tool] as [string, ToolRegistryEntry] }));
|
return entries.map(([id, tool]) => ({ item: [id, tool] as [ToolId, ToolRegistryEntry] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user