forked from akai/readest
PWA app theme color in the header and safe area avoiding home indicator (#220)
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user