PWA app theme color in the header and safe area avoiding home indicator (#220)

This commit is contained in:
Huang Xin
2025-01-23 00:22:09 +01:00
committed by GitHub
parent 4d5297fdb5
commit c678d8fa44
13 changed files with 87 additions and 17 deletions
+3 -2
View File
@@ -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 (
<div
className={clsx(
'library-page bg-base-200/50 text-base-content flex h-dvh select-none flex-col overflow-hidden',
'library-page bg-base-200 text-base-content flex h-dvh select-none flex-col overflow-hidden',
isTauriAppPlatform() && 'rounded-window',
)}
>
@@ -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<FooterBarProps> = ({ 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',
@@ -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 () => {
@@ -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);
@@ -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';
}}
@@ -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 && (
@@ -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);
@@ -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 (
<div className='bottom-tab border-base-300/50 relative flex w-full border-t'>
<div
className={clsx(
'bottom-tab border-base-300/50 relative flex w-full border-t',
isPWA() ? 'bottom-[calc(env(safe-area-inset-bottom)_/_2)]' : '',
)}
>
<div
className={clsx(
'bg-base-300 absolute bottom-1.5 left-1 -z-10 h-[calc(100%-12px)] w-[calc(33.3%-8px)] rounded-lg',
@@ -1,12 +1,15 @@
import clsx from 'clsx';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useBookDataStore } from '@/store/bookDataStore';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { TTSController } from '@/services/tts/TTSController';
import { getPopupPosition, Position } from '@/utils/sel';
import { eventDispatcher } from '@/utils/event';
import { parseSSMLLang } from '@/utils/ssml';
import { throttle } from '@/utils/ui';
import { isPWA } from '@/services/environment';
import Popup from '@/components/Popup';
import TTSPanel from './TTSPanel';
import TTSIcon from './TTSIcon';
@@ -28,6 +31,10 @@ const TTSControl = () => {
const [panelPosition, setPanelPosition] = useState<Position>();
const [trianglePosition, setTrianglePosition] = useState<Position>();
const popupWidth = useResponsiveSize(POPUP_WIDTH);
const popupHeight = useResponsiveSize(POPUP_HEIGHT);
const popupPadding = useResponsiveSize(POPUP_PADDING);
const iconRef = useRef<HTMLDivElement>(null);
const ttsControllerRef = useRef<TTSController | null>(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 && (
<div ref={iconRef} className='absolute bottom-12 right-6 h-12 w-12'>
<div
ref={iconRef}
className={clsx(
'absolute right-6 h-12 w-12',
isPWA() ? 'bottom-[calc(env(safe-area-inset-bottom)+48px)]' : 'bottom-12',
)}
>
<TTSIcon isPlaying={isPlaying} onClick={togglePopup} />
</div>
)}
{showPanel && panelPosition && trianglePosition && (
<Popup
width={POPUP_WIDTH}
height={POPUP_HEIGHT}
width={popupWidth}
height={popupHeight}
position={panelPosition}
trianglePosition={trianglePosition}
className='bg-base-200 absolute flex shadow-lg'
+10 -2
View File
@@ -2,7 +2,7 @@
import { useEffect, useState } from 'react';
import { ThemeCode } from '@/utils/style';
import { themes } from '@/styles/themes';
import { themes, Palette } from '@/styles/themes';
import { isWebAppPlatform } from '@/services/environment';
export type ThemeMode = 'auto' | 'light' | 'dark';
@@ -46,6 +46,7 @@ export const useTheme = () => {
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,
};
};
@@ -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`
+14 -2
View File
@@ -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 = [
+2
View File
@@ -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) => {