fix: respect Android Back / Esc inside Settings sub-pages and Import-from-Folder dialog (#4286)

* fix(library): cancel Import-from-Folder dialog on Android Back / Esc

The dialog was previously relying on <Dialog>'s built-in
`native-key-down` listener to handle Back / Escape, but
`useKeyDownActions` (used here for the Enter-to-confirm shortcut)
registers its own sync listener that returns `true` on every Back
keypress, consuming the event before <Dialog> ever sees it. As a
result Android Back and Escape were silently swallowed inside this
dialog.

Wire `onCancel` so the hook actually performs the cancel itself, and
guard it (like Enter) while a folder pick is in flight to avoid
canceling mid-pick.

* fix(settings): step back to parent panel on Android Back / Esc inside sub-pages

Several settings panels render an in-place sub-view based on local
state (FontPanel -> Custom Fonts, LangPanel -> Custom Dictionaries,
IntegrationsPanel -> KOSync / WebDAV / Readwise / Hardcover / OPDS /
Send-to-Readest). Pressing Android Back (or Escape) while one of
these sub-pages was open used to close the entire Settings dialog
because only <Dialog>'s own `native-key-down` listener handled the
event.

Mount a `useKeyDownActions` hook at each parent panel, gated on the
sub-page being open, that calls the existing "go back" handler and
consumes the event. Because `dispatchSync` walks listeners LIFO, the
panel-level hook (registered after <Dialog>'s) claims Back first
while a sub-page is open; once the sub-page is closed the hook is
disabled and Back falls through to <Dialog> as before, closing the
whole Settings dialog.

This keeps all logic in the three parent panels — no changes needed
to the seven sub-page components — and a single hook in
IntegrationsPanel covers all six integrations sub-pages.
This commit is contained in:
loveheaven
2026-05-25 22:00:33 +08:00
committed by GitHub
parent ca17131f2a
commit c5384b2a6b
4 changed files with 49 additions and 1 deletions
@@ -140,7 +140,12 @@ const ImportFromFolderDialog: React.FC<ImportFromFolderDialogProps> = ({
const [folderMode, setFolderMode] = useState<'keep' | 'flatten'>(initialFolderMode);
const [picking, setPicking] = useState(false);
// Enter to confirm. Escape is handled by <Dialog> via onClose.
// Enter to confirm, Escape / Android Back to cancel. We must wire
// `onCancel` even though <Dialog> 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<ImportFromFolderDialogProps> = ({
if (picking) return;
handleConfirm();
},
onCancel: () => {
if (picking) return;
onCancel();
},
});
const toggleGroup = (id: string) => {
@@ -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<SettingsPanelPanelProp> = ({ 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 <Dialog>'s own listener
// close the entire Settings dialog. This works because
// `useKeyDownActions` registers its sync `native-key-down` listener
// *after* <Dialog>'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
// <Dialog> and closes the dialog as before.
useKeyDownActions({
enabled: fontPanelView === 'custom-fonts',
onCancel: handleBackToMain,
});
useEffect(() => {
onRegisterReset(handleReset);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -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<SubPage>(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 <Dialog>'s
// listener close the whole Settings dialog. The hook registers its
// sync `native-key-down` listener *after* <Dialog>'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);
@@ -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<SettingsPanelPanelProp> = ({ 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
// <Dialog>'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