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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user