diff --git a/apps/readest-app/src/app/library/page.tsx b/apps/readest-app/src/app/library/page.tsx index d8270b76..d0ce76c3 100644 --- a/apps/readest-app/src/app/library/page.tsx +++ b/apps/readest-app/src/app/library/page.tsx @@ -39,8 +39,8 @@ const LibraryPage = () => { checkOpenWithBooks, clearOpenWithBooks, } = useLibraryStore(); - useTheme(); const _ = useTranslation(); + const { updateAppTheme } = useTheme(); const { setSettings, saveSettings } = useSettingsStore(); const [loading, setLoading] = useState(false); const isInitiating = useRef(false); @@ -49,6 +49,7 @@ const LibraryPage = () => { const demoBooks = useDemoBooks(); useEffect(() => { + updateAppTheme('base-200'); const doAppUpdates = async () => { if (hasUpdater()) { await checkForAppUpdates(_); @@ -237,7 +238,7 @@ const LibraryPage = () => { return (
diff --git a/apps/readest-app/src/app/reader/components/FooterBar.tsx b/apps/readest-app/src/app/reader/components/FooterBar.tsx index 96997f4c..95adef94 100644 --- a/apps/readest-app/src/app/reader/components/FooterBar.tsx +++ b/apps/readest-app/src/app/reader/components/FooterBar.tsx @@ -8,7 +8,7 @@ import { useReaderStore } from '@/store/readerStore'; import { useSidebarStore } from '@/store/sidebarStore'; import { useTranslation } from '@/hooks/useTranslation'; import { eventDispatcher } from '@/utils/event'; -import { isTauriAppPlatform } from '@/services/environment'; +import { isPWA, isTauriAppPlatform } from '@/services/environment'; import Button from '@/components/Button'; interface FooterBarProps { @@ -62,6 +62,7 @@ const FooterBar: React.FC = ({ bookKey, pageinfo, isHoveredAnim className={clsx( 'footer-bar absolute bottom-0 z-10 flex h-12 w-full items-center gap-x-4 px-4', 'shadow-xs bg-base-100 transition-opacity duration-300', + isPWA() ? 'pb-[env(safe-area-inset-bottom)]' : '', isTauriAppPlatform() && 'rounded-window-bottom-right', !isSideBarVisible && isTauriAppPlatform() && 'rounded-window-bottom-left', isHoveredAnim && 'hover-bar-anim', diff --git a/apps/readest-app/src/app/reader/components/Reader.tsx b/apps/readest-app/src/app/reader/components/Reader.tsx index a9935666..e2ac88d4 100644 --- a/apps/readest-app/src/app/reader/components/Reader.tsx +++ b/apps/readest-app/src/app/reader/components/Reader.tsx @@ -5,6 +5,7 @@ import * as React from 'react'; import { useEffect, Suspense, useRef } from 'react'; import { useEnv } from '@/context/EnvContext'; +import { useTheme } from '@/hooks/useTheme'; import { useLibraryStore } from '@/store/libraryStore'; import { useSettingsStore } from '@/store/settingsStore'; import { isTauriAppPlatform } from '@/services/environment'; @@ -18,7 +19,10 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => { const { library, setLibrary } = useLibraryStore(); const isInitiating = useRef(false); + const { updateAppTheme } = useTheme(); + useEffect(() => { + updateAppTheme('base-100'); if (isInitiating.current) return; isInitiating.current = true; const initLibrary = async () => { diff --git a/apps/readest-app/src/app/reader/components/notebook/Notebook.tsx b/apps/readest-app/src/app/reader/components/notebook/Notebook.tsx index acbd1f2b..b333039e 100644 --- a/apps/readest-app/src/app/reader/components/notebook/Notebook.tsx +++ b/apps/readest-app/src/app/reader/components/notebook/Notebook.tsx @@ -7,6 +7,7 @@ import { useReaderStore } from '@/store/readerStore'; import { useSidebarStore } from '@/store/sidebarStore'; import { useNotebookStore } from '@/store/notebookStore'; import { useTranslation } from '@/hooks/useTranslation'; +import { useTheme } from '@/hooks/useTheme'; import { useEnv } from '@/context/EnvContext'; import { TextSelection } from '@/utils/sel'; import { BookNote } from '@/types/book'; @@ -23,6 +24,7 @@ const MAX_NOTEBOOK_WIDTH = 0.45; const Notebook: React.FC = ({}) => { const _ = useTranslation(); + const { updateAppTheme } = useTheme(); const { envConfig } = useEnv(); const { settings } = useSettingsStore(); const { sideBarBookKey } = useSidebarStore(); @@ -41,6 +43,15 @@ const Notebook: React.FC = ({}) => { } }; + useEffect(() => { + if (isNotebookVisible) { + updateAppTheme('base-200'); + } else { + updateAppTheme('base-100'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isNotebookVisible]); + useEffect(() => { setNotebookWidth(settings.globalReadSettings.notebookWidth); setNotebookPin(settings.globalReadSettings.isNotebookPinned); diff --git a/apps/readest-app/src/app/reader/components/sidebar/BookCard.tsx b/apps/readest-app/src/app/reader/components/sidebar/BookCard.tsx index 5899f6bb..1d14a1d8 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/BookCard.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/BookCard.tsx @@ -22,7 +22,7 @@ const BookCard = ({ book }: { book: Book }) => { alt={_('Book Cover')} width={56} height={80} - className='mr-4 aspect-auto max-h-20 w-[15%] max-w-14 rounded-sm object-cover shadow-md' + className='mr-4 aspect-auto max-h-16 w-[15%] max-w-12 rounded-sm object-cover shadow-md' onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} diff --git a/apps/readest-app/src/app/reader/components/sidebar/Content.tsx b/apps/readest-app/src/app/reader/components/sidebar/Content.tsx index 92fb8b8d..d2dd8d64 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/Content.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/Content.tsx @@ -53,7 +53,7 @@ const SidebarContent: React.FC<{ const handleTabChange = (tab: string) => { setFade(true); const timeout = setTimeout(() => { - setFade(false) + setFade(false); setTargetTab(tab); setConfig(sideBarBookKey!, config); clearTimeout(timeout); @@ -76,7 +76,7 @@ const SidebarContent: React.FC<{ ref={scrollContainerRef} className={clsx( 'scroll-container overflow-y-auto transition-opacity duration-300 ease-in-out', - { 'opacity-0': fade, 'opacity-100': !fade } + { 'opacity-0': fade, 'opacity-100': !fade }, )} > {targetTab === 'toc' && bookDoc.toc && ( diff --git a/apps/readest-app/src/app/reader/components/sidebar/SideBar.tsx b/apps/readest-app/src/app/reader/components/sidebar/SideBar.tsx index 4ddd2970..cabe2360 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/SideBar.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/SideBar.tsx @@ -8,6 +8,7 @@ import { useSidebarStore } from '@/store/sidebarStore'; import { BookSearchResult } from '@/types/book'; import { eventDispatcher } from '@/utils/event'; import { isTauriAppPlatform } from '@/services/environment'; +import { useTheme } from '@/hooks/useTheme'; import SidebarHeader from './Header'; import SidebarContent from './Content'; import BookCard from './BookCard'; @@ -23,6 +24,7 @@ const MAX_SIDEBAR_WIDTH = 0.45; const SideBar: React.FC<{ onGoToLibrary: () => void; }> = ({ onGoToLibrary }) => { + const { updateAppTheme } = useTheme(); const { settings } = useSettingsStore(); const { sideBarBookKey } = useSidebarStore(); const { getBookData } = useBookDataStore(); @@ -57,6 +59,15 @@ const SideBar: React.FC<{ } }; + useEffect(() => { + if (isSideBarVisible) { + updateAppTheme('base-200'); + } else { + updateAppTheme('base-100'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isSideBarVisible]); + useEffect(() => { eventDispatcher.on('search', onSearchEvent); eventDispatcher.on('navigate', onNavigateEvent); diff --git a/apps/readest-app/src/app/reader/components/sidebar/TabNavigation.tsx b/apps/readest-app/src/app/reader/components/sidebar/TabNavigation.tsx index 6433f254..0903fa6a 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/TabNavigation.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/TabNavigation.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { MdToc, MdEditNote, MdBookmarkBorder } from 'react-icons/md'; import { useTranslation } from '@/hooks/useTranslation'; +import { isPWA } from '@/services/environment'; const TabNavigation: React.FC<{ activeTab: string; @@ -13,7 +14,12 @@ const TabNavigation: React.FC<{ const tabs = ['toc', 'annotations', 'bookmarks']; return ( -
+
{ const [panelPosition, setPanelPosition] = useState(); const [trianglePosition, setTrianglePosition] = useState(); + const popupWidth = useResponsiveSize(POPUP_WIDTH); + const popupHeight = useResponsiveSize(POPUP_HEIGHT); + const popupPadding = useResponsiveSize(POPUP_PADDING); + const iconRef = useRef(null); const ttsControllerRef = useRef(null); @@ -212,9 +219,9 @@ const TTSControl = () => { const popupPos = getPopupPosition( trianglePos, windowRect, - POPUP_WIDTH, - POPUP_HEIGHT, - POPUP_PADDING, + popupWidth, + popupHeight, + popupPadding, ); setPanelPosition(popupPos); @@ -241,14 +248,20 @@ const TTSControl = () => { /> )} {showIndicator && ( -
+
)} {showPanel && panelPosition && trianglePosition && ( { bg: defaultPalette['base-100'], fg: defaultPalette['base-content'], primary: defaultPalette.primary, + palette: defaultPalette, }; }); @@ -83,12 +84,18 @@ export const useTheme = () => { if (isWebAppPlatform()) { document.querySelector('meta[name="theme-color"]')?.setAttribute('content', bg); } - setThemeCode({ bg, fg, primary }); + setThemeCode({ bg, fg, primary, palette }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [themeMode, themeColor, isDarkMode]); const updateThemeMode = (mode: ThemeMode) => setThemeMode(mode); const updateThemeColor = (color: string) => setThemeColor(color); + const updateAppTheme = (color: keyof Palette) => { + if (isWebAppPlatform()) { + const { palette } = themeCode; + document.querySelector('meta[name="theme-color"]')?.setAttribute('content', palette[color]); + } + }; return { themeMode, @@ -97,5 +104,6 @@ export const useTheme = () => { isDarkMode, updateThemeMode, updateThemeColor, + updateAppTheme, }; }; diff --git a/apps/readest-app/src/services/environment.ts b/apps/readest-app/src/services/environment.ts index 1ba2edf0..b03cca44 100644 --- a/apps/readest-app/src/services/environment.ts +++ b/apps/readest-app/src/services/environment.ts @@ -13,6 +13,7 @@ export const isWebAppPlatform = () => process.env['NEXT_PUBLIC_APP_PLATFORM'] == export const hasUpdater = () => window.__READEST_UPDATER_ACCESS === true && !process.env['NEXT_PUBLIC_DISABLE_UPDATER']; export const hasCli = () => window.__READEST_CLI_ACCESS === true; +export const isPWA = () => window.matchMedia('(display-mode: standalone)').matches; // Dev API only in development mode and web platform // with command `pnpm dev-web` diff --git a/apps/readest-app/src/styles/themes.ts b/apps/readest-app/src/styles/themes.ts index 854f3805..a57aebea 100644 --- a/apps/readest-app/src/styles/themes.ts +++ b/apps/readest-app/src/styles/themes.ts @@ -7,6 +7,18 @@ type BaseColor = { primary: string; }; +export type Palette = { + 'base-100': string; + 'base-200': string; + 'base-300': string; + 'base-content': string; + neutral: string; + 'neutral-content': string; + primary: string; + secondary: string; + accent: string; +}; + const generateLightPalette = ({ bg, fg, primary }: BaseColor) => { return { 'base-100': bg, // Main background @@ -18,7 +30,7 @@ const generateLightPalette = ({ bg, fg, primary }: BaseColor) => { primary: primary, secondary: tinycolor(primary).lighten(20).toHexString(), // Lighter secondary accent: tinycolor(primary).analogous()[1]!.toHexString(), // Analogous accent - }; + } as Palette; }; const generateDarkPalette = ({ bg, fg, primary }: BaseColor) => { @@ -32,7 +44,7 @@ const generateDarkPalette = ({ bg, fg, primary }: BaseColor) => { primary: primary, secondary: tinycolor(primary).darken(20).toHexString(), // Darker secondary accent: tinycolor(primary).triad()[1]!.toHexString(), // Triad accent - }; + } as Palette; }; export const themes = [ diff --git a/apps/readest-app/src/utils/style.ts b/apps/readest-app/src/utils/style.ts index 9dd061bc..d3e32a95 100644 --- a/apps/readest-app/src/utils/style.ts +++ b/apps/readest-app/src/utils/style.ts @@ -5,6 +5,7 @@ import { FALLBACK_FONTS, } from '@/services/constants'; import { ViewSettings } from '@/types/book'; +import { Palette } from '@/styles/themes'; import fontfacesCSS from '!!raw-loader!../styles/fonts.css'; @@ -193,6 +194,7 @@ export interface ThemeCode { bg: string; fg: string; primary: string; + palette: Palette; } export const getStyles = (viewSettings: ViewSettings, themeCode: ThemeCode) => {