This commit is contained in:
@@ -10,8 +10,11 @@ import { useThemeStore } from '@/store/themeStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useDeviceControlStore } from '@/store/deviceStore';
|
||||
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { setSystemUIVisibility } from '@/utils/bridge';
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { UpdaterWindow } from '@/components/UpdaterWindow';
|
||||
@@ -20,11 +23,13 @@ import ReaderContent from './ReaderContent';
|
||||
|
||||
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const { isDarkMode, showSystemUI, dismissSystemUI } = useThemeStore();
|
||||
const { hoveredBookKey } = useReaderStore();
|
||||
const { isSideBarVisible } = useSidebarStore();
|
||||
const { setLibrary } = useLibraryStore();
|
||||
const { hoveredBookKey } = useReaderStore();
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const { isSideBarVisible, setSideBarVisible } = useSidebarStore();
|
||||
const { isNotebookVisible, setNotebookVisible } = useNotebookStore();
|
||||
const { isDarkMode, showSystemUI, dismissSystemUI } = useThemeStore();
|
||||
const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore();
|
||||
const [libraryLoaded, setLibraryLoaded] = useState(false);
|
||||
const isInitiating = useRef(false);
|
||||
|
||||
@@ -33,7 +38,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
|
||||
useEffect(() => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (appService?.isMobile && !document.hidden) {
|
||||
if (appService?.isMobileApp && !document.hidden) {
|
||||
dismissSystemUI();
|
||||
setSystemUIVisibility({ visible: false, darkMode: isDarkMode });
|
||||
}
|
||||
@@ -45,6 +50,34 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appService, isDarkMode]);
|
||||
|
||||
const handleKeyDown = (event: CustomEvent) => {
|
||||
if (event.detail.keyName === 'Back') {
|
||||
setSideBarVisible(false);
|
||||
setNotebookVisible(false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!appService?.isAndroidApp) return;
|
||||
if (isSideBarVisible || isNotebookVisible) {
|
||||
acquireBackKeyInterception();
|
||||
eventDispatcher.onSync('native-key-down', handleKeyDown);
|
||||
}
|
||||
if (!isSideBarVisible && !isNotebookVisible) {
|
||||
releaseBackKeyInterception();
|
||||
eventDispatcher.offSync('native-key-down', handleKeyDown);
|
||||
}
|
||||
return () => {
|
||||
if (appService?.isAndroidApp) {
|
||||
releaseBackKeyInterception();
|
||||
eventDispatcher.offSync('native-key-down', handleKeyDown);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isSideBarVisible, isNotebookVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitiating.current) return;
|
||||
isInitiating.current = true;
|
||||
|
||||
@@ -10,10 +10,11 @@ import Link from './Link';
|
||||
|
||||
export const setAboutDialogVisible = (visible: boolean) => {
|
||||
const dialog = document.getElementById('about_window');
|
||||
if (visible) {
|
||||
(dialog as HTMLDialogElement)?.showModal();
|
||||
} else {
|
||||
(dialog as HTMLDialogElement)?.close();
|
||||
if (dialog) {
|
||||
const event = new CustomEvent('setDialogVisibility', {
|
||||
detail: { visible },
|
||||
});
|
||||
dialog.dispatchEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,16 +23,32 @@ export const AboutWindow = () => {
|
||||
const { appService } = useEnv();
|
||||
const [isUpdated, setIsUpdated] = useState(false);
|
||||
const [browserInfo, setBrowserInfo] = useState('');
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setBrowserInfo(parseWebViewVersion(appService));
|
||||
|
||||
const handleCustomEvent = (event: CustomEvent) => {
|
||||
setIsOpen(event.detail.visible);
|
||||
};
|
||||
|
||||
const el = document.getElementById('about_window');
|
||||
if (el) {
|
||||
el.addEventListener('setDialogVisibility', handleCustomEvent as EventListener);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (el) {
|
||||
el.removeEventListener('setDialogVisibility', handleCustomEvent as EventListener);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleCheckUpdate = async () => {
|
||||
const update = await checkForAppUpdates(_, false);
|
||||
if (update) {
|
||||
setAboutDialogVisible(false);
|
||||
setIsOpen(false);
|
||||
} else {
|
||||
setIsUpdated(true);
|
||||
}
|
||||
@@ -40,9 +57,9 @@ export const AboutWindow = () => {
|
||||
return (
|
||||
<Dialog
|
||||
id='about_window'
|
||||
isOpen={false}
|
||||
isOpen={isOpen}
|
||||
title={_('About Readest')}
|
||||
onClose={() => setAboutDialogVisible(false)}
|
||||
onClose={() => setIsOpen(false)}
|
||||
boxClassName='sm:!w-96 sm:h-auto'
|
||||
>
|
||||
<div className='about-content flex h-full flex-col items-center justify-center'>
|
||||
|
||||
@@ -3,9 +3,11 @@ import React, { ReactNode, useEffect, useState } from 'react';
|
||||
import { MdArrowBackIosNew, MdArrowForwardIos } from 'react-icons/md';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useDrag } from '@/hooks/useDrag';
|
||||
import { useDeviceControlStore } from '@/store/deviceStore';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { impactFeedback } from '@tauri-apps/plugin-haptics';
|
||||
import { getDirFromUILanguage } from '@/utils/rtl';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
|
||||
const VELOCITY_THRESHOLD = 0.5;
|
||||
const SNAP_THRESHOLD = 0.2;
|
||||
@@ -38,24 +40,40 @@ const Dialog: React.FC<DialogProps> = ({
|
||||
onClose,
|
||||
}) => {
|
||||
const { appService } = useEnv();
|
||||
const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore();
|
||||
const [isFullHeightInMobile, setIsFullHeightInMobile] = React.useState(!snapHeight);
|
||||
const [isRtl] = useState(() => getDirFromUILanguage() === 'rtl');
|
||||
const iconSize22 = useResponsiveSize(22);
|
||||
const isMobile = window.innerWidth < 640;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
const handleKeyDown = (event: KeyboardEvent | CustomEvent) => {
|
||||
if (event instanceof CustomEvent) {
|
||||
if (event.detail.keyName === 'Back') {
|
||||
onClose();
|
||||
return true;
|
||||
}
|
||||
} else if (event.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
if (appService?.isAndroidApp) {
|
||||
acquireBackKeyInterception();
|
||||
eventDispatcher.onSync('native-key-down', handleKeyDown);
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
if (appService?.isAndroidApp) {
|
||||
releaseBackKeyInterception();
|
||||
eventDispatcher.offSync('native-key-down', handleKeyDown);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [isOpen]);
|
||||
|
||||
const handleDragMove = (data: { clientY: number; deltaY: number }) => {
|
||||
if (!isMobile) return;
|
||||
|
||||
@@ -10,10 +10,10 @@ import { fetch } from '@tauri-apps/plugin-http';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { tauriDownload } from '@/utils/transfer';
|
||||
import { installPackage } from '@/utils/bridge';
|
||||
import { READEST_UPDATER_FILE, READEST_CHANGELOG_FILE } from '@/services/constants';
|
||||
import packageJson from '../../package.json';
|
||||
import Dialog from '@/components/Dialog';
|
||||
import { installPackage } from '@/utils/bridge';
|
||||
|
||||
interface ReleaseNotes {
|
||||
releases: Record<
|
||||
@@ -333,36 +333,47 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
|
||||
};
|
||||
|
||||
export const setUpdaterWindowVisible = (visible: boolean, newVersion?: string) => {
|
||||
if (newVersion) {
|
||||
localStorage.setItem('newVersion', newVersion);
|
||||
window.dispatchEvent(new CustomEvent('new-version-detected'));
|
||||
}
|
||||
const dialog = document.getElementById('updater_window');
|
||||
if (visible) {
|
||||
(dialog as HTMLDialogElement)?.showModal();
|
||||
} else {
|
||||
(dialog as HTMLDialogElement)?.close();
|
||||
if (dialog) {
|
||||
const event = new CustomEvent('setDialogVisibility', {
|
||||
detail: { visible, newVersion },
|
||||
});
|
||||
dialog.dispatchEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
export const UpdaterWindow = () => {
|
||||
const _ = useTranslation();
|
||||
const [newVersion, setNewVersion] = useState(localStorage.getItem('newVersion') || '');
|
||||
const [newVersion, setNewVersion] = useState('');
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
setNewVersion(localStorage.getItem('newVersion') || '');
|
||||
const handleCustomEvent = (event: CustomEvent) => {
|
||||
const { visible, newVersion } = event.detail;
|
||||
setIsOpen(visible);
|
||||
if (newVersion) {
|
||||
setNewVersion(newVersion);
|
||||
}
|
||||
};
|
||||
|
||||
const el = document.getElementById('updater_window');
|
||||
if (el) {
|
||||
el.addEventListener('setDialogVisibility', handleCustomEvent as EventListener);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (el) {
|
||||
el.removeEventListener('setDialogVisibility', handleCustomEvent as EventListener);
|
||||
}
|
||||
};
|
||||
window.addEventListener('new-version-detected', handler);
|
||||
return () => window.removeEventListener('new-version-detected', handler);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
id='updater_window'
|
||||
isOpen={false}
|
||||
isOpen={isOpen}
|
||||
title={_('Software Update')}
|
||||
onClose={() => setUpdaterWindowVisible(false)}
|
||||
onClose={() => setIsOpen(false)}
|
||||
boxClassName='sm:!w-[80%] sm:h-auto'
|
||||
>
|
||||
<UpdaterContent version={newVersion ?? undefined} />
|
||||
|
||||
@@ -13,7 +13,7 @@ const handleNativeKeyDown = (keyName: string) => {
|
||||
return eventDispatcher.dispatch('native-key-down', { keyName });
|
||||
}
|
||||
if (keyName === 'Back') {
|
||||
return eventDispatcher.dispatch('native-key-down', { keyName });
|
||||
return eventDispatcher.dispatchSync('native-key-down', { keyName });
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -21,35 +21,57 @@ const handleNativeKeyDown = (keyName: string) => {
|
||||
type DeviceControlState = {
|
||||
volumeKeysIntercepted: boolean;
|
||||
backKeyIntercepted: boolean;
|
||||
volumeKeysInterceptionCount: number;
|
||||
backKeyInterceptionCount: number;
|
||||
acquireVolumeKeyInterception: () => void;
|
||||
releaseVolumeKeyInterception: () => void;
|
||||
acquireBackKeyInterception: () => void;
|
||||
releaseBackKeyInterception: () => void;
|
||||
};
|
||||
|
||||
export const useDeviceControlStore = create<DeviceControlState>((set) => ({
|
||||
export const useDeviceControlStore = create<DeviceControlState>((set, get) => ({
|
||||
volumeKeysIntercepted: false,
|
||||
backKeyIntercepted: false,
|
||||
volumeKeysInterceptionCount: 0,
|
||||
backKeyInterceptionCount: 0,
|
||||
|
||||
acquireVolumeKeyInterception: () => {
|
||||
window.onNativeKeyDown = handleNativeKeyDown;
|
||||
interceptKeys({ volumeKeys: true });
|
||||
set({ volumeKeysIntercepted: true });
|
||||
const { volumeKeysInterceptionCount } = get();
|
||||
if (volumeKeysInterceptionCount == 0) {
|
||||
window.onNativeKeyDown = handleNativeKeyDown;
|
||||
interceptKeys({ volumeKeys: true });
|
||||
set({ volumeKeysIntercepted: true });
|
||||
}
|
||||
set({ volumeKeysInterceptionCount: volumeKeysInterceptionCount + 1 });
|
||||
},
|
||||
|
||||
releaseVolumeKeyInterception: () => {
|
||||
interceptKeys({ volumeKeys: false });
|
||||
set({ volumeKeysIntercepted: false });
|
||||
const { volumeKeysInterceptionCount } = get();
|
||||
if (volumeKeysInterceptionCount <= 1) {
|
||||
interceptKeys({ volumeKeys: false });
|
||||
set({ volumeKeysIntercepted: false, volumeKeysInterceptionCount: 0 });
|
||||
} else {
|
||||
set({ volumeKeysInterceptionCount: volumeKeysInterceptionCount - 1 });
|
||||
}
|
||||
},
|
||||
|
||||
acquireBackKeyInterception: () => {
|
||||
window.onNativeKeyDown = handleNativeKeyDown;
|
||||
interceptKeys({ backKey: true });
|
||||
set({ backKeyIntercepted: true });
|
||||
const { backKeyInterceptionCount } = get();
|
||||
if (backKeyInterceptionCount == 0) {
|
||||
window.onNativeKeyDown = handleNativeKeyDown;
|
||||
interceptKeys({ backKey: true });
|
||||
set({ backKeyIntercepted: true });
|
||||
}
|
||||
set({ backKeyInterceptionCount: backKeyInterceptionCount + 1 });
|
||||
},
|
||||
|
||||
releaseBackKeyInterception: () => {
|
||||
interceptKeys({ backKey: false });
|
||||
set({ backKeyIntercepted: false });
|
||||
const { backKeyInterceptionCount } = get();
|
||||
if (backKeyInterceptionCount <= 1) {
|
||||
interceptKeys({ backKey: false });
|
||||
set({ backKeyIntercepted: false, backKeyInterceptionCount: 0 });
|
||||
} else {
|
||||
set({ backKeyInterceptionCount: backKeyInterceptionCount - 1 });
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class EventDispatcher {
|
||||
private syncListeners: Map<string, Set<(event: CustomEvent) => boolean>>;
|
||||
private syncListeners: Map<string, Array<(event: CustomEvent) => boolean>>;
|
||||
private asyncListeners: Map<string, Set<(event: CustomEvent) => Promise<void> | void>>;
|
||||
|
||||
constructor() {
|
||||
@@ -30,20 +30,26 @@ class EventDispatcher {
|
||||
|
||||
onSync(event: string, callback: (event: CustomEvent) => boolean): void {
|
||||
if (!this.syncListeners.has(event)) {
|
||||
this.syncListeners.set(event, new Set());
|
||||
this.syncListeners.set(event, []);
|
||||
}
|
||||
this.syncListeners.get(event)!.add(callback);
|
||||
this.syncListeners.get(event)!.push(callback);
|
||||
}
|
||||
|
||||
offSync(event: string, callback: (event: CustomEvent) => boolean): void {
|
||||
this.syncListeners.get(event)?.delete(callback);
|
||||
const listeners = this.syncListeners.get(event);
|
||||
if (listeners) {
|
||||
this.syncListeners.set(
|
||||
event,
|
||||
listeners.filter((listener) => listener !== callback),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
dispatchSync(event: string, detail?: unknown): boolean {
|
||||
const listeners = this.syncListeners.get(event);
|
||||
if (listeners) {
|
||||
const customEvent = new CustomEvent(event, { detail });
|
||||
for (const listener of listeners) {
|
||||
for (const listener of [...listeners].reverse()) {
|
||||
const consumed = listener(customEvent);
|
||||
if (consumed) {
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user