This commit is contained in:
@@ -345,7 +345,7 @@ export default function AuthPage() {
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 inset-0 flex select-none flex-col items-center overflow-hidden',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
|
||||
'h-[100vh] supports-[height:100dvh]:h-[100dvh]',
|
||||
appService?.hasRoundedWindow && isRoundedWindow && 'window-border rounded-window',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ import posthog from 'posthog-js';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { parseWebViewVersion } from '@/utils/ua';
|
||||
import { parseWebViewInfo } from '@/utils/ua';
|
||||
import { handleGlobalError } from '@/utils/error';
|
||||
|
||||
interface ErrorPageProps {
|
||||
@@ -18,7 +18,7 @@ export default function Error({ error, reset }: ErrorPageProps) {
|
||||
const [browserInfo, setBrowserInfo] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setBrowserInfo(parseWebViewVersion(appService));
|
||||
setBrowserInfo(parseWebViewInfo(appService));
|
||||
posthog.captureException(error);
|
||||
handleGlobalError(error);
|
||||
}, [appService, error]);
|
||||
|
||||
@@ -353,16 +353,21 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
/>
|
||||
))}
|
||||
{viewMode === 'grid' && !navBooksGroup && allBookshelfItems.length > 0 && (
|
||||
<button
|
||||
aria-label={_('Import Books')}
|
||||
className={clsx(
|
||||
'border-1 bg-base-100 hover:bg-base-300/50 flex items-center justify-center',
|
||||
'mx-0 my-4 aspect-[28/41] sm:mx-4',
|
||||
)}
|
||||
onClick={handleImportBooks}
|
||||
>
|
||||
<PiPlus className='size-10' color='gray' />
|
||||
</button>
|
||||
<div className={clsx('mx-0 my-4 sm:mx-4')}>
|
||||
<button
|
||||
aria-label={_('Import Books')}
|
||||
className={clsx(
|
||||
'border-1 bg-base-100 hover:bg-base-300/50',
|
||||
'flex items-center justify-center',
|
||||
'aspect-[28/41] w-full',
|
||||
)}
|
||||
onClick={handleImportBooks}
|
||||
>
|
||||
<div className='flex items-center justify-center'>
|
||||
<PiPlus className='size-10' color='gray' />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{loading && (
|
||||
|
||||
@@ -342,7 +342,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
onKeyDown={handleKeyDown}
|
||||
{...handlers}
|
||||
>
|
||||
<div className='flex-grow'>
|
||||
<div className='flex h-full flex-col justify-end'>
|
||||
{'format' in item ? (
|
||||
<BookItem
|
||||
mode={mode}
|
||||
|
||||
@@ -96,12 +96,7 @@ const GroupItem: React.FC<GroupItemProps> = ({ mode, group, isSelectMode, groupS
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'group-item flex h-full flex-col justify-end',
|
||||
appService?.hasContextMenu ? 'cursor-pointer' : '',
|
||||
)}
|
||||
>
|
||||
<div className={clsx('group-item', appService?.hasContextMenu ? 'cursor-pointer' : '')}>
|
||||
<div
|
||||
className={clsx(
|
||||
'relative flex overflow-hidden',
|
||||
|
||||
@@ -631,7 +631,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
aria-label='Your Library'
|
||||
className={clsx(
|
||||
'library-page bg-base-200 text-base-content flex select-none flex-col overflow-hidden',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
|
||||
'h-[100vh] supports-[height:100dvh]:h-[100dvh]',
|
||||
appService?.hasRoundedWindow && isRoundedWindow && 'window-border rounded-window',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -142,7 +142,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
<div
|
||||
className={clsx(
|
||||
`reader-page bg-base-100 text-base-content select-none overflow-hidden`,
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
|
||||
'h-[100vh] supports-[height:100dvh]:h-[100dvh]',
|
||||
appService?.hasRoundedWindow && isRoundedWindow && 'window-border rounded-window',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
@@ -185,7 +184,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
setTimeout(() => setLoading(true), 200);
|
||||
return (
|
||||
loading && (
|
||||
<div className={clsx('hero hero-content', appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh')}>
|
||||
<div className='hero hero-content h-[100vh] supports-[height:100dvh]:h-[100dvh]'>
|
||||
<Spinner loading={true} />
|
||||
</div>
|
||||
)
|
||||
@@ -193,7 +192,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('reader-content flex', appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh')}>
|
||||
<div className='reader-content flex h-[100vh] supports-[height:100dvh]:h-[100dvh]'>
|
||||
<SideBar onGoToLibrary={handleCloseBooksToLibrary} />
|
||||
<BooksGrid bookKeys={bookKeys} onCloseBook={handleCloseBook} />
|
||||
<Notebook />
|
||||
|
||||
@@ -341,7 +341,7 @@ const ProfilePage = () => {
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 inset-0 select-none overflow-hidden',
|
||||
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
|
||||
'h-[100vh] supports-[height:100dvh]:h-[100dvh]',
|
||||
appService?.hasRoundedWindow && isRoundedWindow && 'window-border rounded-window',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@ import Image from 'next/image';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { checkForAppUpdates, checkAppReleaseNotes } from '@/helpers/updater';
|
||||
import { parseWebViewVersion } from '@/utils/ua';
|
||||
import { parseWebViewInfo } from '@/utils/ua';
|
||||
import { getAppVersion } from '@/utils/version';
|
||||
import SupportLinks from './SupportLinks';
|
||||
import LegalLinks from './LegalLinks';
|
||||
@@ -30,7 +30,7 @@ export const AboutWindow = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setBrowserInfo(parseWebViewVersion(appService));
|
||||
setBrowserInfo(parseWebViewInfo(appService));
|
||||
|
||||
const handleCustomEvent = (event: CustomEvent) => {
|
||||
setIsOpen(event.detail.visible);
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
declare module '*.css' {
|
||||
interface IClassNames {
|
||||
[className: string]: string;
|
||||
}
|
||||
const classNames: IClassNames;
|
||||
export default classNames;
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { applyCustomTheme, Palette } from '@/styles/themes';
|
||||
import { themes, applyCustomTheme, Palette } from '@/styles/themes';
|
||||
import { getStatusBarHeight, setSystemUIVisibility } from '@/utils/bridge';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { parseWebViewVersion } from '@/utils/ua';
|
||||
|
||||
type UseThemeProps = {
|
||||
systemUIVisible?: boolean;
|
||||
@@ -28,6 +29,8 @@ export const useTheme = ({
|
||||
setSystemUIAlwaysHidden,
|
||||
} = useThemeStore();
|
||||
|
||||
const useFallbackColors = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
updateAppTheme(appThemeColor);
|
||||
if (appService?.isAndroidApp) {
|
||||
@@ -88,10 +91,24 @@ export const useTheme = ({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [handleSystemUIVisibility]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!appService?.isAndroidApp) return;
|
||||
const webViewVersion = parseWebViewVersion(appService);
|
||||
// OKLCH color model is supported in Chromium 111+
|
||||
useFallbackColors.current = webViewVersion < 111;
|
||||
}, [appService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!themeColor || !themes.find((t) => t.name === themeColor)) return;
|
||||
if (useFallbackColors.current) {
|
||||
applyCustomTheme(undefined, themeColor, true);
|
||||
}
|
||||
}, [themeColor]);
|
||||
|
||||
useEffect(() => {
|
||||
const customThemes = settings.globalReadSettings?.customThemes ?? [];
|
||||
customThemes.forEach((customTheme) => {
|
||||
applyCustomTheme(customTheme);
|
||||
applyCustomTheme(customTheme, undefined, useFallbackColors.current);
|
||||
});
|
||||
localStorage.setItem('customThemes', JSON.stringify(customThemes));
|
||||
}, [settings.globalReadSettings?.customThemes]);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { stubTranslation as _ } from '../utils/misc';
|
||||
import { getContrastOklch, hexToOklch } from '../utils/color';
|
||||
import { getContrastHex, getContrastOklch, hexToOklch } from '../utils/color';
|
||||
|
||||
export type BaseColor = {
|
||||
bg: string;
|
||||
@@ -160,8 +160,8 @@ export const themes = [
|
||||
},
|
||||
] as Theme[];
|
||||
|
||||
const generateCustomThemeVariables = (palette: Palette): string => {
|
||||
return `
|
||||
const generateCustomThemeVariables = (palette: Palette, fallbackIncluded = false): string => {
|
||||
const colors = `
|
||||
--b1: ${hexToOklch(palette['base-100'])};
|
||||
--b2: ${hexToOklch(palette['base-200'])};
|
||||
--b3: ${hexToOklch(palette['base-300'])};
|
||||
@@ -188,22 +188,64 @@ const generateCustomThemeVariables = (palette: Palette): string => {
|
||||
--er: 70.9% 0.184 22;
|
||||
--erc: 100% 0 0;
|
||||
`;
|
||||
|
||||
const fallbackColors = `
|
||||
--fallback-b1: ${palette['base-100']};
|
||||
--fallback-b2: ${palette['base-200']};
|
||||
--fallback-b3: ${palette['base-300']};
|
||||
--fallback-bc: ${palette['base-content']};
|
||||
|
||||
--fallback-p: ${palette.primary};
|
||||
--fallback-pc: ${getContrastHex(palette.primary)};
|
||||
|
||||
--fallback-s: ${palette.secondary};
|
||||
--fallback-sc: ${getContrastHex(palette.secondary)};
|
||||
|
||||
--fallback-a: ${palette.accent};
|
||||
--fallback-ac: ${getContrastHex(palette.accent)};
|
||||
|
||||
--fallback-n: ${palette.neutral};
|
||||
--fallback-nc: ${palette['neutral-content']};
|
||||
|
||||
--fallback-in: #ff0000;
|
||||
--fallback-inc: #ffffff;
|
||||
--fallback-su: #00ff00;
|
||||
--fallback-suc: #000000;
|
||||
--fallback-wa: #ffff00;
|
||||
--fallback-wac: #000000;
|
||||
--fallback-er: #ff8000;
|
||||
--fallback-erc: #000000;
|
||||
`;
|
||||
|
||||
return colors + (fallbackIncluded ? fallbackColors : '');
|
||||
};
|
||||
|
||||
export const applyCustomTheme = (customTheme: CustomTheme) => {
|
||||
const lightPalette = generateLightPalette(customTheme.colors.light);
|
||||
const darkPalette = generateDarkPalette(customTheme.colors.dark);
|
||||
export const applyCustomTheme = (
|
||||
customTheme?: CustomTheme,
|
||||
themeName?: string,
|
||||
fallbackIncluded = false,
|
||||
) => {
|
||||
if (!customTheme && !themeName) return;
|
||||
|
||||
const lightThemeName = `${customTheme.name}-light`;
|
||||
const darkThemeName = `${customTheme.name}-dark`;
|
||||
const lightThemeName = customTheme ? `${customTheme.name}-light` : `${themeName}-light`;
|
||||
const darkThemeName = customTheme ? `${customTheme.name}-dark` : `${themeName}-dark`;
|
||||
|
||||
console.log('themeName', themeName);
|
||||
const lightPalette = customTheme
|
||||
? generateLightPalette(customTheme.colors.light)
|
||||
: (themes.find((t) => t.name === themeName) || themes[0]!).colors.light;
|
||||
|
||||
const darkPalette = customTheme
|
||||
? generateDarkPalette(customTheme.colors.dark)
|
||||
: (themes.find((t) => t.name === themeName) || themes[0]!).colors.dark;
|
||||
|
||||
const css = `
|
||||
[data-theme="${lightThemeName}"] {
|
||||
${generateCustomThemeVariables(lightPalette)}
|
||||
${generateCustomThemeVariables(lightPalette, fallbackIncluded)}
|
||||
}
|
||||
|
||||
[data-theme="${darkThemeName}"] {
|
||||
${generateCustomThemeVariables(darkPalette)}
|
||||
${generateCustomThemeVariables(darkPalette, fallbackIncluded)}
|
||||
}
|
||||
|
||||
:root {
|
||||
@@ -213,7 +255,7 @@ export const applyCustomTheme = (customTheme: CustomTheme) => {
|
||||
`;
|
||||
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.id = `theme-${lightThemeName}-styles`;
|
||||
styleElement.id = `theme-${customTheme ? customTheme.name : themeName}-styles`;
|
||||
styleElement.textContent = css;
|
||||
|
||||
const existingStyle = document.getElementById(styleElement.id);
|
||||
|
||||
@@ -38,3 +38,7 @@ export function hexToOklch(hexColor: string): string {
|
||||
export const getContrastOklch = (hexColor: string): string => {
|
||||
return tinycolor(hexColor).isDark() ? '100% 0 0' : '0% 0 0';
|
||||
};
|
||||
|
||||
export const getContrastHex = (hex: string): string => {
|
||||
return tinycolor(hex).isDark() ? '#FFFFFF' : '#000000';
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AppService } from '@/types/system';
|
||||
|
||||
export const parseWebViewVersion = (appService: AppService | null): string => {
|
||||
export const parseWebViewInfo = (appService: AppService | null): string => {
|
||||
const ua = navigator.userAgent;
|
||||
|
||||
if (appService?.isAndroidApp) {
|
||||
@@ -63,3 +63,9 @@ export const parseWebViewVersion = (appService: AppService | null): string => {
|
||||
return 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
export const parseWebViewVersion = (appService: AppService | null): number => {
|
||||
const webViewInfo = parseWebViewInfo(appService);
|
||||
const versionMatch = webViewInfo.match(/([0-9]+)\./);
|
||||
return versionMatch ? parseFloat(versionMatch[1]!) : 0;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user