feat(opds): support downloading ebooks from OPDS catalogs (#2571)

This commit is contained in:
Huang Xin
2025-12-01 04:22:54 +08:00
committed by GitHub
parent 97f021da87
commit 2ca3561093
74 changed files with 4450 additions and 181 deletions
@@ -9,6 +9,7 @@ import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import { useTrafficLight } from '@/hooks/useTrafficLight';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import WindowButtons from '@/components/WindowButtons';
import Dropdown from '@/components/Dropdown';
@@ -39,14 +40,8 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
const _ = useTranslation();
const { appService } = useEnv();
const headerRef = useRef<HTMLDivElement>(null);
const {
isTrafficLightVisible,
trafficLightInFullscreen,
setTrafficLightVisibility,
initializeTrafficLightStore,
initializeTrafficLightListeners,
cleanupTrafficLightListeners,
} = useTrafficLightStore();
const { isTrafficLightVisible } = useTrafficLight();
const { trafficLightInFullscreen, setTrafficLightVisibility } = useTrafficLightStore();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const { bookKeys, hoveredBookKey, setHoveredBookKey } = useReaderStore();
const { systemUIVisible, statusBarHeight } = useThemeStore();
@@ -60,17 +55,6 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
if (!isOpen) setHoveredBookKey('');
};
useEffect(() => {
if (!appService?.hasTrafficLight) return;
initializeTrafficLightStore(appService);
initializeTrafficLightListeners();
return () => {
cleanupTrafficLightListeners();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appService]);
useEffect(() => {
if (!appService?.hasTrafficLight) return;
if (isSideBarVisible) return;
@@ -2,14 +2,14 @@
import clsx from 'clsx';
import * as React from 'react';
import { useEffect, Suspense, useRef, useState } from 'react';
import { useEffect, Suspense } from 'react';
import { useRouter } from 'next/navigation';
import { useEnv } from '@/context/EnvContext';
import { useTheme } from '@/hooks/useTheme';
import { useLibrary } from '@/hooks/useLibrary';
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';
@@ -51,18 +51,16 @@ Z-Index Layering Guide:
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
const router = useRouter();
const { envConfig, appService } = useEnv();
const { setLibrary } = useLibraryStore();
const { appService } = useEnv();
const { hoveredBookKey, getView } = useReaderStore();
const { settings, setSettings } = useSettingsStore();
const { settings } = useSettingsStore();
const { sideBarBookKey } = useSidebarStore();
const { isSideBarVisible, getIsSideBarVisible, setSideBarVisible } = useSidebarStore();
const { isNotebookVisible, getIsNotebookVisible, setNotebookVisible } = useNotebookStore();
const { isDarkMode, systemUIAlwaysHidden, isRoundedWindow } = useThemeStore();
const { showSystemUI, dismissSystemUI } = useThemeStore();
const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore();
const [libraryLoaded, setLibraryLoaded] = useState(false);
const isInitiating = useRef(false);
const { libraryLoaded } = useLibrary();
useTheme({ systemUIVisible: settings.alwaysShowStatusBar, appThemeColor: 'base-100' });
useScreenWakeLock(settings.screenWakeLock);
@@ -115,21 +113,6 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appService?.isAndroidApp, sideBarBookKey, isSideBarVisible, isNotebookVisible]);
useEffect(() => {
if (isInitiating.current) return;
isInitiating.current = true;
const initLibrary = async () => {
const appService = await envConfig.getAppService();
const settings = await appService.loadSettings();
setSettings(settings);
setLibrary(await appService.loadLibraryBooks());
setLibraryLoaded(true);
};
initLibrary();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!appService?.isMobileApp) return;
const systemUIVisible = !!hoveredBookKey || settings.alwaysShowStatusBar;
@@ -1,13 +1,12 @@
import clsx from 'clsx';
import React, { useEffect } from 'react';
import React from 'react';
import { GiBookshelf } from 'react-icons/gi';
import { FiSearch } from 'react-icons/fi';
import { MdOutlineMenu, MdOutlinePushPin, MdPushPin } from 'react-icons/md';
import { MdArrowBackIosNew } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { useTrafficLight } from '@/hooks/useTrafficLight';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import Dropdown from '@/components/Dropdown';
import BookMenu from './BookMenu';
@@ -20,30 +19,11 @@ const SidebarHeader: React.FC<{
onToggleSearchBar: () => void;
}> = ({ isPinned, isSearchBarVisible, onGoToLibrary, onClose, onTogglePin, onToggleSearchBar }) => {
const _ = useTranslation();
const { appService } = useEnv();
const {
isTrafficLightVisible,
initializeTrafficLightStore,
initializeTrafficLightListeners,
setTrafficLightVisibility,
cleanupTrafficLightListeners,
} = useTrafficLightStore();
const { isTrafficLightVisible } = useTrafficLight();
const iconSize14 = useResponsiveSize(14);
const iconSize18 = useResponsiveSize(18);
const iconSize22 = useResponsiveSize(22);
useEffect(() => {
if (!appService?.hasTrafficLight) return;
initializeTrafficLightStore(appService);
initializeTrafficLightListeners();
setTrafficLightVisibility(true);
return () => {
cleanupTrafficLightListeners();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appService?.hasTrafficLight]);
return (
<div
className={clsx(