diff --git a/apps/readest-app/src/app/library/components/ImportFromFolderDialog.tsx b/apps/readest-app/src/app/library/components/ImportFromFolderDialog.tsx index 20895883..703ca676 100644 --- a/apps/readest-app/src/app/library/components/ImportFromFolderDialog.tsx +++ b/apps/readest-app/src/app/library/components/ImportFromFolderDialog.tsx @@ -140,7 +140,12 @@ const ImportFromFolderDialog: React.FC = ({ const [folderMode, setFolderMode] = useState<'keep' | 'flatten'>(initialFolderMode); const [picking, setPicking] = useState(false); - // Enter to confirm. Escape is handled by via onClose. + // Enter to confirm, Escape / Android Back to cancel. We must wire + // `onCancel` even though also listens for Back, because + // `useKeyDownActions` registers its own `native-key-down` listener that + // returns `true` (consuming the event) on every Back keypress — if we + // leave `onCancel` undefined the handler swallows Back without doing + // anything, and the Dialog's own listener never gets a chance to run. useKeyDownActions({ onConfirm: () => { // Block the Enter shortcut while a folder pick is in flight so @@ -148,6 +153,10 @@ const ImportFromFolderDialog: React.FC = ({ if (picking) return; handleConfirm(); }, + onCancel: () => { + if (picking) return; + onCancel(); + }, }); const toggleGroup = (id: string) => { diff --git a/apps/readest-app/src/components/settings/FontPanel.tsx b/apps/readest-app/src/components/settings/FontPanel.tsx index 3b1393ec..2eb4a0a5 100644 --- a/apps/readest-app/src/components/settings/FontPanel.tsx +++ b/apps/readest-app/src/components/settings/FontPanel.tsx @@ -25,6 +25,7 @@ import { getSysFontsList } from '@/utils/bridge'; import { isCJKStr } from '@/utils/lang'; import { isTauriAppPlatform } from '@/services/environment'; import { useResetViewSettings } from '@/hooks/useResetSettings'; +import { useKeyDownActions } from '@/hooks/useKeyDownActions'; import { saveViewSettings } from '@/helpers/settings'; import { SettingsPanelPanelProp } from './SettingsDialog'; import { BoxedList, NavigationRow, SettingLabel, SettingsRow } from './primitives'; @@ -171,6 +172,19 @@ const FontPanel: React.FC = ({ bookKey, onRegisterReset setFontPanelView('main-fonts'); }; + // Android Back / Esc: when the Custom Fonts sub-page is open, intercept + // and step back to main-fonts instead of letting 's own listener + // close the entire Settings dialog. This works because + // `useKeyDownActions` registers its sync `native-key-down` listener + // *after* 's, and `dispatchSync` walks listeners LIFO — so when + // enabled this hook claims the Back press first and `return true` + // consumes it; when disabled (sub-page closed) Back falls through to + // and closes the dialog as before. + useKeyDownActions({ + enabled: fontPanelView === 'custom-fonts', + onCancel: handleBackToMain, + }); + useEffect(() => { onRegisterReset(handleReset); // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/apps/readest-app/src/components/settings/IntegrationsPanel.tsx b/apps/readest-app/src/components/settings/IntegrationsPanel.tsx index a7859f5c..686494af 100644 --- a/apps/readest-app/src/components/settings/IntegrationsPanel.tsx +++ b/apps/readest-app/src/components/settings/IntegrationsPanel.tsx @@ -14,6 +14,7 @@ import { import { useEnv } from '@/context/EnvContext'; import { useAuth } from '@/context/AuthContext'; import { useTranslation } from '@/hooks/useTranslation'; +import { useKeyDownActions } from '@/hooks/useKeyDownActions'; import { useSettingsStore } from '@/store/settingsStore'; import { useCustomOPDSStore } from '@/store/customOPDSStore'; import { useWebDAVSyncStore } from '@/store/webdavSyncStore'; @@ -57,6 +58,20 @@ const IntegrationsPanel: React.FC = () => { const [subPage, setSubPage] = useState(null); + // Android Back / Esc: when any integrations sub-page (KOSync, WebDAV, + // Readwise, Hardcover, OPDS, Send-to-Readest) is open, intercept and + // step back to the integrations list instead of letting 's + // listener close the whole Settings dialog. The hook registers its + // sync `native-key-down` listener *after* 's, and + // `dispatchSync` walks listeners LIFO — so this one claims Back first + // when enabled and `return true` consumes the event. When subPage is + // null the hook is disabled and Back falls through to close the dialog + // as before. + useKeyDownActions({ + enabled: subPage !== null, + onCancel: () => setSubPage(null), + }); + const toggleDiscordPresence = () => { const discordRichPresenceEnabled = !settings.discordRichPresenceEnabled; saveSysSettings(envConfig, 'discordRichPresenceEnabled', discordRichPresenceEnabled); diff --git a/apps/readest-app/src/components/settings/LangPanel.tsx b/apps/readest-app/src/components/settings/LangPanel.tsx index dd7f4d9e..c479c980 100644 --- a/apps/readest-app/src/components/settings/LangPanel.tsx +++ b/apps/readest-app/src/components/settings/LangPanel.tsx @@ -12,6 +12,7 @@ import { isTranslatorAvailable, } from '@/services/translators'; import { useResetViewSettings } from '@/hooks/useResetSettings'; +import { useKeyDownActions } from '@/hooks/useKeyDownActions'; import { TRANSLATED_LANGS, TRANSLATOR_LANGS } from '@/services/constants'; import { ConvertChineseVariant } from '@/types/book'; import { SettingsPanelPanelProp } from './SettingsDialog'; @@ -50,6 +51,15 @@ const LangPanel: React.FC = ({ bookKey, onRegisterReset ); const [showCustomDictionaries, setShowCustomDictionaries] = useState(false); + // Android Back / Esc: when the Manage Dictionaries sub-page is open, + // intercept and step back to the language list instead of letting + // 's listener close the whole Settings dialog. See the matching + // comment in FontPanel.tsx for the LIFO-dispatch reasoning. + useKeyDownActions({ + enabled: showCustomDictionaries, + onCancel: () => setShowCustomDictionaries(false), + }); + // Deep-link: callers (e.g. the dictionary popup's manage icon) can set // activeSettingsItemId to `'settings.language.dictionaries.manage'` to // jump straight into the Manage Dictionaries sub-page on open. Clear the