Add theme and color config
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"zustand": "5.0.0-rc.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -45,6 +46,7 @@
|
||||
"@types/node": "^20",
|
||||
"@types/react": "18.3.12",
|
||||
"@types/react-dom": "18.3.1",
|
||||
"@types/tinycolor2": "^1.4.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cpx": "^1.5.0",
|
||||
"cross-var": "^1.1.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "Readest",
|
||||
"version": "0.6.2",
|
||||
"version": "0.6.5",
|
||||
"identifier": "com.bilingify.digest",
|
||||
"build": {
|
||||
"frontendDist": "../out",
|
||||
|
||||
@@ -5,10 +5,12 @@ import { usePathname } from 'next/navigation';
|
||||
import { AuthProvider } from '@/context/AuthContext';
|
||||
import { EnvProvider } from '@/context/EnvContext';
|
||||
import { CSPostHogProvider } from '@/context/PHContext';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import '../styles/globals.css';
|
||||
import '../styles/fonts.css';
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
useTheme();
|
||||
const pathname = usePathname();
|
||||
React.useEffect(() => {
|
||||
document.documentElement.setAttribute('data-page', pathname.replace('/', '') || 'default');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
import { MdDelete, MdOpenInNew } from 'react-icons/md';
|
||||
import { MdCheckCircle, MdCheckCircleOutline } from 'react-icons/md';
|
||||
|
||||
@@ -97,7 +97,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
<div className='bookshelf'>
|
||||
<div className='grid grid-cols-3 gap-0 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8'>
|
||||
{bookshelfItems.map((item, index) => (
|
||||
<div key={`library-item-${index}`} className='flex h-full flex-col p-4 hover:bg-gray-200'>
|
||||
<div key={`library-item-${index}`} className='hover:bg-base-200 flex h-full flex-col p-4'>
|
||||
<div className='flex-grow'>
|
||||
{'format' in item ? (
|
||||
<div
|
||||
@@ -173,16 +173,16 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
|
||||
|
||||
{bookshelfItems.length > 0 && (
|
||||
<div
|
||||
className='border-1 m-4 flex aspect-[28/41] items-center justify-center bg-white hover:bg-gray-200'
|
||||
className='border-1 bg-base-100 hover:bg-base-200 m-4 flex aspect-[28/41] items-center justify-center'
|
||||
role='button'
|
||||
onClick={onImportBooks}
|
||||
>
|
||||
<FaPlus className='size-8' color='gray' />
|
||||
<PiPlus className='size-10' color='gray' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{selectedBooks.length > 0 && (
|
||||
<div className='fixed bottom-4 left-1/2 flex -translate-x-1/2 transform space-x-4 rounded-lg bg-gray-800 p-4 text-white shadow-lg'>
|
||||
<div className='text-base-content bg-base-300 fixed bottom-4 left-1/2 flex -translate-x-1/2 transform space-x-4 rounded-lg p-4 shadow-lg'>
|
||||
<button onClick={openSelectedBooks} className='flex items-center space-x-2'>
|
||||
<MdOpenInNew />
|
||||
<span>Open</span>
|
||||
|
||||
@@ -52,7 +52,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
<div
|
||||
ref={headerRef}
|
||||
className={clsx(
|
||||
'titlebar z-10 h-11 w-full bg-gray-100 py-2 pr-6',
|
||||
'titlebar z-10 h-11 w-full py-2 pr-6',
|
||||
isTrafficLightVisible ? 'pl-16' : 'pl-2',
|
||||
)}
|
||||
>
|
||||
@@ -66,23 +66,25 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
placeholder='Search books...'
|
||||
spellCheck='false'
|
||||
className={clsx(
|
||||
'input rounded-badge h-7 w-full bg-gray-200 pl-10 pr-10',
|
||||
'input rounded-badge bg-base-200 h-7 w-full pl-10 pr-10',
|
||||
'font-sans text-sm font-light',
|
||||
'border-none focus:outline-none focus:ring-0',
|
||||
)}
|
||||
/>
|
||||
<div className='absolute right-4 flex items-center space-x-4 text-gray-500'>
|
||||
<span className='mx-2 h-5 w-[1px] bg-gray-300'></span>
|
||||
<div className='dropdown dropdown-bottom flex h-5 cursor-pointer justify-center text-gray-500'>
|
||||
<span className='mx-2 h-5 w-[1px] bg-gray-400'></span>
|
||||
<div className='dropdown dropdown-bottom flex h-5 cursor-pointer justify-center'>
|
||||
<div className='lg:tooltip lg:tooltip-bottom' data-tip='Add books'>
|
||||
<PiPlus tabIndex={-1} className='h-5 w-5' />
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={-1}
|
||||
className='dropdown-content dropdown-center menu bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow'
|
||||
className='dropdown-content dropdown-center menu rounded-box z-[1] mt-3 w-52 p-2 shadow'
|
||||
>
|
||||
<li>
|
||||
<button onClick={onImportBooks}>From Local File</button>
|
||||
<button className='text-base-content' onClick={onImportBooks}>
|
||||
From Local File
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -77,7 +77,7 @@ const LibraryPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='library-page rounded-window flex h-full min-h-screen select-none flex-col overflow-hidden bg-gray-100'>
|
||||
<div className='library-page rounded-window bg-base-200/50 flex h-full min-h-screen select-none flex-col overflow-hidden'>
|
||||
<div className='fixed top-0 z-40 w-full'>
|
||||
<LibraryHeader
|
||||
isSelectMode={isSelectMode}
|
||||
@@ -86,7 +86,7 @@ const LibraryPage = () => {
|
||||
/>
|
||||
</div>
|
||||
{loading && (
|
||||
<div className='fixed inset-0 z-50 flex items-center justify-center bg-white/50'>
|
||||
<div className='fixed inset-0 z-50 flex items-center justify-center'>
|
||||
<Spinner loading />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -76,7 +76,11 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
|
||||
|
||||
return (
|
||||
<button onClick={toggleBookmark} className='p-2'>
|
||||
{isBookmarked ? <MdOutlineBookmark size={20} /> : <MdOutlineBookmarkAdd size={20} />}
|
||||
{isBookmarked ? (
|
||||
<MdOutlineBookmark size={20} className='text-base-content' />
|
||||
) : (
|
||||
<MdOutlineBookmarkAdd size={20} className='text-base-content' />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { BookDoc } from '@/libs/document';
|
||||
import { BookConfig, BookNote, BookSearchConfig, BookSearchResult } from '@/types/book';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
|
||||
import {
|
||||
handleKeydown,
|
||||
@@ -61,6 +62,7 @@ const FoliateViewer: React.FC<{
|
||||
const [viewInited, setViewInited] = useState(false);
|
||||
const isViewCreated = useRef(false);
|
||||
const { setView: setFoliateView, setProgress, getViewSettings } = useReaderStore();
|
||||
const { themeCode } = useTheme();
|
||||
|
||||
const progressRelocateHandler = (event: Event) => {
|
||||
const detail = (event as CustomEvent).detail;
|
||||
@@ -118,7 +120,6 @@ const FoliateViewer: React.FC<{
|
||||
|
||||
await view.open(bookDoc);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
view.renderer.setStyles?.(getStyles(viewSettings));
|
||||
const isScrolled = viewSettings.scrolled!;
|
||||
const marginPx = viewSettings.marginPx!;
|
||||
const gapPercent = viewSettings.gapPercent!;
|
||||
@@ -146,6 +147,7 @@ const FoliateViewer: React.FC<{
|
||||
await view.goToFraction(0);
|
||||
}
|
||||
setViewInited(true);
|
||||
view.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
|
||||
window.addEventListener('message', handleClickTurnPage);
|
||||
};
|
||||
|
||||
@@ -42,12 +42,12 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim
|
||||
<button className='btn btn-ghost mx-2 h-8 min-h-8 w-8 p-0' onClick={handleGoPrev}>
|
||||
<RiArrowLeftWideLine size={20} />
|
||||
</button>
|
||||
<span className='mx-2 text-center text-sm text-black'>
|
||||
<span className='mx-2 text-center text-sm'>
|
||||
{pageinfoValid ? `${Math.round(progressFraction * 100)}%` : ''}
|
||||
</span>
|
||||
<input
|
||||
type='range'
|
||||
className='mx-2 w-full'
|
||||
className='text-base-content mx-2 w-full'
|
||||
min={0}
|
||||
max={100}
|
||||
value={pageinfoValid ? progressFraction * 100 : 0}
|
||||
|
||||
@@ -20,9 +20,9 @@ const NotebookToggler: React.FC<NotebookTogglerProps> = ({ bookKey }) => {
|
||||
return (
|
||||
<button onClick={handleToggleSidebar} className='p-2'>
|
||||
{sideBarBookKey == bookKey && isNotebookVisible ? (
|
||||
<TbLayoutSidebarRightFilled size={20} />
|
||||
<TbLayoutSidebarRightFilled size={20} className='text-base-content' />
|
||||
) : (
|
||||
<TbLayoutSidebarRight size={20} />
|
||||
<TbLayoutSidebarRight size={20} className='text-base-content' />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,9 @@ const PageInfoView: React.FC<PageInfoProps> = ({ bookFormat, section, pageinfo,
|
||||
className='pageinfo absolute bottom-0 left-0 right-0 flex h-12 items-center justify-end'
|
||||
style={{ paddingRight: gapRight }}
|
||||
>
|
||||
<h2 className='text-right font-sans text-xs font-extralight text-slate-500'>{pageInfo}</h2>
|
||||
<h2 className='text-neutral-content text-right font-sans text-xs font-extralight'>
|
||||
{pageInfo}
|
||||
</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -90,7 +90,7 @@ const ReaderContent: React.FC<{ settings: SystemSettings }> = ({ settings }) =>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex h-screen'>
|
||||
<div className='text-base-content flex h-screen'>
|
||||
<SideBar onGoToLibrary={handleCloseBooks} />
|
||||
<BooksGrid bookKeys={bookKeys} onCloseBook={handleCloseBook} />
|
||||
<Notebook />
|
||||
|
||||
@@ -20,7 +20,9 @@ const SectionInfo: React.FC<SectionInfoProps> = ({ section, gapLeft }) => {
|
||||
)}
|
||||
style={{ left: gapLeft }}
|
||||
>
|
||||
<h2 className='text-center font-sans text-xs font-light text-slate-500'>{section || ''}</h2>
|
||||
<h2 className='text-neutral-content text-center font-sans text-xs font-light'>
|
||||
{section || ''}
|
||||
</h2>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,9 +20,9 @@ const SidebarToggler: React.FC<SidebarTogglerProps> = ({ bookKey }) => {
|
||||
return (
|
||||
<button onClick={handleToggleSidebar} className='p-2'>
|
||||
{sideBarBookKey == bookKey && isSideBarVisible ? (
|
||||
<TbLayoutSidebarFilled size={20} />
|
||||
<TbLayoutSidebarFilled size={20} className='text-base-content' />
|
||||
) : (
|
||||
<TbLayoutSidebar size={20} />
|
||||
<TbLayoutSidebar size={20} className='text-base-content' />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -2,9 +2,12 @@ import clsx from 'clsx';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { BiMoon, BiSun } from 'react-icons/bi';
|
||||
import { TbSunMoon } from 'react-icons/tb';
|
||||
import { MdZoomOut, MdZoomIn, MdCheck } from 'react-icons/md';
|
||||
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useTheme, ThemeMode } from '@/hooks/useTheme';
|
||||
import { getStyles } from '@/utils/style';
|
||||
|
||||
interface ViewMenuProps {
|
||||
@@ -21,7 +24,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
const { getView, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
|
||||
const [isDarkMode, setDarkMode] = useState(viewSettings!.theme === 'dark');
|
||||
const { themeMode, isDarkMode, themeCode, updateThemeMode } = useTheme();
|
||||
const [isScrolledMode, setScrolledMode] = useState(viewSettings!.scrolled);
|
||||
const [isInvertedColors, setInvertedColors] = useState(viewSettings!.invert);
|
||||
const [zoomLevel, setZoomLevel] = useState(viewSettings!.zoomLevel!);
|
||||
@@ -30,7 +33,6 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
const zoomOut = () => setZoomLevel((prev) => Math.max(prev - 10, 50));
|
||||
const resetZoom = () => setZoomLevel(100);
|
||||
const toggleScrolledMode = () => setScrolledMode(!isScrolledMode);
|
||||
const toggleDarkMode = () => setDarkMode(!isDarkMode);
|
||||
const toggleInvertedColors = () => setInvertedColors(!isInvertedColors);
|
||||
|
||||
const openFontLayoutMenu = () => {
|
||||
@@ -38,6 +40,16 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
onSetSettingsDialogOpen(true);
|
||||
};
|
||||
|
||||
const cycleThemeMode = () => {
|
||||
const nextMode: ThemeMode =
|
||||
themeMode === 'auto' ? 'light' : themeMode === 'light' ? 'dark' : 'auto';
|
||||
updateThemeMode(nextMode);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
|
||||
}, [themeCode]);
|
||||
|
||||
useEffect(() => {
|
||||
getView(bookKey)?.renderer.setAttribute('flow', isScrolledMode ? 'scrolled' : 'paginated');
|
||||
viewSettings!.scrolled = isScrolledMode;
|
||||
@@ -46,6 +58,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.toggle('invert', isInvertedColors);
|
||||
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
|
||||
}, [isInvertedColors]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -53,7 +66,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
if (!view) return;
|
||||
// FIXME: zoom level is not working in paginated mode
|
||||
if (viewSettings?.scrolled) {
|
||||
view.renderer.setStyles?.(getStyles(viewSettings!));
|
||||
view.renderer.setStyles?.(getStyles(viewSettings!, themeCode));
|
||||
}
|
||||
viewSettings!.zoomLevel = zoomLevel;
|
||||
setViewSettings(bookKey, viewSettings!);
|
||||
@@ -63,7 +76,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
<div
|
||||
id='exclude-title-bar-mousedown'
|
||||
tabIndex={0}
|
||||
className='view-menu dropdown-content dropdown-right no-triangle z-20 mt-1 w-72 border bg-white shadow-2xl'
|
||||
className='view-menu dropdown-content dropdown-right no-triangle border-base-100 z-20 mt-1 w-72 border shadow-2xl'
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -73,14 +86,17 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
>
|
||||
<button
|
||||
onClick={zoomOut}
|
||||
className={clsx('rounded-full p-2 hover:bg-gray-100', !isScrolledMode && 'btn-disabled')}
|
||||
className={clsx(
|
||||
'hover:bg-base-200 text-base-content rounded-full p-2',
|
||||
!isScrolledMode && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
>
|
||||
<MdZoomOut size={20} />
|
||||
</button>
|
||||
<button
|
||||
className={clsx(
|
||||
'h-8 min-h-8 w-[50%] rounded-md p-1 text-center hover:bg-gray-100',
|
||||
!isScrolledMode && 'btn-disabled',
|
||||
'hover:bg-base-200 text-base-content h-8 min-h-8 w-[50%] rounded-md p-1 text-center',
|
||||
!isScrolledMode && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
onClick={resetZoom}
|
||||
>
|
||||
@@ -88,65 +104,49 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
</button>
|
||||
<button
|
||||
onClick={zoomIn}
|
||||
className={clsx('rounded-full p-2 hover:bg-gray-100', !isScrolledMode && 'btn-disabled')}
|
||||
className={clsx(
|
||||
'hover:bg-base-200 text-base-content rounded-full p-2',
|
||||
!isScrolledMode && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
>
|
||||
<MdZoomIn size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr className='my-1' />
|
||||
<hr className='border-base-200 my-1' />
|
||||
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={openFontLayoutMenu}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}></span>
|
||||
<span className='ml-2'>Font & Layout</span>
|
||||
</div>
|
||||
<span className='text-sm text-gray-400'>Shift+F</span>
|
||||
</button>
|
||||
<MenuItem label='Font & Layout' shortcut='Shift+F' onClick={openFontLayoutMenu} />
|
||||
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
<MenuItem
|
||||
label='Scrolled Mode'
|
||||
shortcut='Shift+J'
|
||||
icon={isScrolledMode ? <MdCheck size={20} /> : undefined}
|
||||
onClick={toggleScrolledMode}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{isScrolledMode && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>Scrolled Mode</span>
|
||||
</div>
|
||||
<span className='text-sm text-gray-400'>Shift+J</span>
|
||||
</button>
|
||||
/>
|
||||
|
||||
<hr className='my-1' />
|
||||
<hr className='border-base-200 my-1' />
|
||||
|
||||
<button
|
||||
className='flex w-full items-center rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={toggleDarkMode}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{isDarkMode ? <BiMoon size={20} /> : <BiSun size={20} />}
|
||||
</span>
|
||||
</div>
|
||||
<span className='ml-2'>{isDarkMode ? 'Dark Mode' : 'Light Mode'}</span>
|
||||
</button>
|
||||
<button
|
||||
className={clsx(
|
||||
'flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100',
|
||||
!isDarkMode && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
<MenuItem
|
||||
label={
|
||||
themeMode === 'dark' ? 'Dark Mode' : themeMode === 'light' ? 'Light Mode' : 'Auto Mode'
|
||||
}
|
||||
icon={
|
||||
themeMode === 'dark' ? (
|
||||
<BiMoon size={20} />
|
||||
) : themeMode === 'light' ? (
|
||||
<BiSun size={20} />
|
||||
) : (
|
||||
<TbSunMoon size={20} />
|
||||
)
|
||||
}
|
||||
onClick={cycleThemeMode}
|
||||
/>
|
||||
<MenuItem
|
||||
label='Invert Colors in Dark Mode'
|
||||
icon={isInvertedColors ? <MdCheck size={20} className='text-base-content' /> : undefined}
|
||||
onClick={toggleInvertedColors}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span style={{ minWidth: '20px' }}>
|
||||
{isInvertedColors && <MdCheck size={20} className='text-base-content' />}
|
||||
</span>
|
||||
<span className='ml-2'>Invert Colors in Dark Mode</span>
|
||||
</div>
|
||||
</button>
|
||||
disabled={!isDarkMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ const NotebookHeader: React.FC<{
|
||||
<div className='z-10 flex items-center space-x-3'>
|
||||
<button
|
||||
onClick={handleTogglePin}
|
||||
className={`${isPinned ? 'bg-gray-300' : 'bg-base-300'} btn btn-ghost btn-circle h-6 min-h-6 w-6`}
|
||||
className={`${isPinned ? 'bg-base-300' : 'bg-base-300/65'} btn btn-ghost btn-circle h-6 min-h-6 w-6`}
|
||||
>
|
||||
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
|
||||
</button>
|
||||
|
||||
@@ -48,7 +48,7 @@ const NoteEditor: React.FC<NoteEditorProps> = ({ onSave, onEdit }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='note-editor-container mt-2 rounded-md bg-white p-2'>
|
||||
<div className='note-editor-container bg-base-100 mt-2 rounded-md p-2'>
|
||||
<div className='flex w-full justify-between space-x-2'>
|
||||
<div className='settings-content relative w-full'>
|
||||
<textarea
|
||||
|
||||
@@ -145,7 +145,7 @@ const Notebook: React.FC = ({}) => {
|
||||
<li key={`${index}-${item.id}`} className='my-2'>
|
||||
<div
|
||||
tabIndex={0}
|
||||
className='collapse-arrow border-base-300 collapse border bg-white'
|
||||
className='collapse-arrow border-base-300 bg-base-100 collapse border'
|
||||
>
|
||||
<div
|
||||
className='collapse-title h-9 min-h-9 p-2 pr-8 text-sm font-medium'
|
||||
|
||||
@@ -1,32 +1,87 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { MdOutlineLightMode, MdOutlineDarkMode } from 'react-icons/md';
|
||||
import { MdRadioButtonUnchecked, MdRadioButtonChecked } from 'react-icons/md';
|
||||
import { TbSunMoon } from 'react-icons/tb';
|
||||
|
||||
const ColorPanel: React.FC<{ bookKey: string }> = () => {
|
||||
const [selectedTheme, setSelectedTheme] = useState('Default');
|
||||
const themes = [
|
||||
'Default',
|
||||
'Gray',
|
||||
'Sepia',
|
||||
'Grass',
|
||||
'Cherry',
|
||||
'Sky',
|
||||
'Nord',
|
||||
'Solarized',
|
||||
'Gruvbox',
|
||||
];
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { themes } from '@/styles/themes';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { getStyles } from '@/utils/style';
|
||||
|
||||
const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { themeMode, themeColor, themeCode, isDarkMode, updateThemeMode, updateThemeColor } =
|
||||
useTheme();
|
||||
const { getView, getViewSettings } = useReaderStore();
|
||||
const view = getView(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
|
||||
useEffect(() => {
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
}, [themeCode]);
|
||||
|
||||
return (
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>Color Settings</h2>
|
||||
<div className='mt-4 grid grid-cols-3 gap-2'>
|
||||
{themes.map((theme) => (
|
||||
<div className='flex items-center justify-between'>
|
||||
<h2 className='font-medium'>Theme Mode</h2>
|
||||
<div className='flex gap-2'>
|
||||
<div className='tooltip tooltip-bottom' data-tip='Auto Mode'>
|
||||
<button
|
||||
key={theme}
|
||||
className={`btn ${selectedTheme === theme ? 'btn-active' : ''}`}
|
||||
onClick={() => setSelectedTheme(theme)}
|
||||
className={`btn btn-ghost btn-circle ${themeMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => updateThemeMode('auto')}
|
||||
>
|
||||
{theme}
|
||||
<TbSunMoon size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='tooltip tooltip-bottom' data-tip='Light Mode'>
|
||||
<button
|
||||
className={`btn btn-ghost btn-circle ${themeMode === 'light' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => updateThemeMode('light')}
|
||||
>
|
||||
<MdOutlineLightMode size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='tooltip tooltip-bottom' data-tip='Dark Mode'>
|
||||
<button
|
||||
className={`btn btn-ghost btn-circle ${themeMode === 'dark' ? 'btn-active bg-base-300' : ''}`}
|
||||
onClick={() => updateThemeMode('dark')}
|
||||
>
|
||||
<MdOutlineDarkMode size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className='mb-2 font-medium'>Theme Color</h2>
|
||||
<div className='grid grid-cols-3 gap-4'>
|
||||
{themes.map(({ name, label, colors }) => (
|
||||
<label
|
||||
key={name}
|
||||
className={`relative flex cursor-pointer flex-col items-center justify-center rounded-lg p-4 shadow-md ${
|
||||
themeColor === name ? 'ring-2 ring-indigo-500 ring-offset-2' : ''
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: isDarkMode ? colors.dark['base-100'] : colors.light['base-100'],
|
||||
color: isDarkMode ? colors.dark['base-content'] : colors.light['base-content'],
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type='radio'
|
||||
name='theme'
|
||||
value={name}
|
||||
checked={themeColor === name}
|
||||
onChange={() => updateThemeColor(name)}
|
||||
className='hidden'
|
||||
/>
|
||||
{themeColor === name ? (
|
||||
<MdRadioButtonChecked size={24} />
|
||||
) : (
|
||||
<MdRadioButtonUnchecked size={24} />
|
||||
)}
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,10 +17,10 @@ const DialogMenu: React.FC<DialogMenuProps> = ({ toggleDropdown }) => {
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className='dropdown-content dropdown-right no-triangle z-20 mt-1 w-44 border bg-white shadow-2xl'
|
||||
className='dropdown-content dropdown-right no-triangle border-base-200 z-20 mt-1 w-44 border shadow-2xl'
|
||||
>
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
className='hover:bg-base-200 text-base-content flex w-full items-center justify-between rounded-md p-2'
|
||||
onClick={handleToggleGlobal}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
|
||||
@@ -6,6 +6,7 @@ import FontDropdown from './FontDropDown';
|
||||
import { MONOSPACE_FONTS, SANS_SERIF_FONTS, SERIF_FONTS } from '@/services/constants';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
|
||||
interface FontFaceProps {
|
||||
className?: string;
|
||||
@@ -24,7 +25,7 @@ const handleFontFaceFont = (option: string, family: string) => {
|
||||
|
||||
const FontFace = ({ className, family, label, options, selected, onSelect }: FontFaceProps) => (
|
||||
<div className={clsx('config-item', className)}>
|
||||
<span className='text-gray-700'>{label}</span>
|
||||
<span className=''>{label}</span>
|
||||
<FontDropdown
|
||||
family={family}
|
||||
options={options}
|
||||
@@ -40,6 +41,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { setSettings, getView, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const view = getView(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const { themeCode } = useTheme();
|
||||
|
||||
const [defaultFontSize, setDefaultFontSize] = useState(viewSettings.defaultFontSize!);
|
||||
const [minFontSize, setMinFontSize] = useState(viewSettings.minimumFontSize!);
|
||||
@@ -56,7 +58,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
settings.globalViewSettings.defaultFont = defaultFont;
|
||||
setSettings(settings);
|
||||
}
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings));
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
}, [defaultFont]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -66,7 +68,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
settings.globalViewSettings.defaultFontSize = defaultFontSize;
|
||||
setSettings(settings);
|
||||
}
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings));
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
}, [defaultFontSize]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -76,7 +78,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
settings.globalViewSettings.minimumFontSize = minFontSize;
|
||||
setSettings(settings);
|
||||
}
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings));
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
}, [minFontSize]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -86,7 +88,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
settings.globalViewSettings.serifFont = serifFont;
|
||||
setSettings(settings);
|
||||
}
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings));
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
}, [serifFont]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -96,7 +98,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
settings.globalViewSettings.sansSerifFont = sansSerifFont;
|
||||
setSettings(settings);
|
||||
}
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings));
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
}, [sansSerifFont]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -106,7 +108,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
settings.globalViewSettings.monospaceFont = monospaceFont;
|
||||
setSettings(settings);
|
||||
}
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings));
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
}, [monospaceFont]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -116,7 +118,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
settings.globalViewSettings.overrideFont = overrideFont;
|
||||
setSettings(settings);
|
||||
}
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings));
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
}, [overrideFont]);
|
||||
|
||||
const handleFontFamilyFont = (option: string) => {
|
||||
@@ -136,8 +138,8 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>Font Size</h2>
|
||||
<div className='card bg-base-100 border shadow'>
|
||||
<div className='divide-y'>
|
||||
<div className='card border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label='Default Font Size'
|
||||
@@ -160,10 +162,10 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>Font Family</h2>
|
||||
<div className='card bg-base-100 border shadow'>
|
||||
<div className='divide-y'>
|
||||
<div className='card border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<div className='config-item config-item-top'>
|
||||
<span className='text-gray-700'>Default Font</span>
|
||||
<span className=''>Default Font</span>
|
||||
<FontDropdown
|
||||
options={fontFamilyOptions}
|
||||
selected={defaultFont}
|
||||
@@ -173,7 +175,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
</div>
|
||||
|
||||
<div className='config-item config-item-bottom'>
|
||||
<span className='text-gray-700'>Override Publisher Font</span>
|
||||
<span className=''>Override Publisher Font</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
@@ -187,8 +189,8 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>Font Face</h2>
|
||||
<div className='card bg-base-100 border shadow'>
|
||||
<div className='divide-y'>
|
||||
<div className='card border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<FontFace
|
||||
className='config-item-top'
|
||||
family='serif'
|
||||
|
||||
@@ -3,12 +3,14 @@ import { useReaderStore } from '@/store/readerStore';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
|
||||
import NumberInput from './NumberInput';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
|
||||
const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { settings, isFontLayoutSettingsGlobal } = useReaderStore();
|
||||
const { setSettings, getView, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const view = getView(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const { themeCode } = useTheme();
|
||||
|
||||
const [lineHeight, setLineHeight] = useState(viewSettings.lineHeight!);
|
||||
const [fullJustification, setFullJustification] = useState(viewSettings.fullJustification!);
|
||||
@@ -26,7 +28,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
settings.globalViewSettings.lineHeight = lineHeight;
|
||||
setSettings(settings);
|
||||
}
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings));
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
}, [lineHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -36,7 +38,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
settings.globalViewSettings.fullJustification = fullJustification;
|
||||
setSettings(settings);
|
||||
}
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings));
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
}, [fullJustification]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -46,7 +48,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
settings.globalViewSettings.hyphenation = hyphenation;
|
||||
setSettings(settings);
|
||||
}
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings));
|
||||
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
}, [hyphenation]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -100,8 +102,8 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
<div className='my-4 w-full space-y-6'>
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>Paragraph</h2>
|
||||
<div className='card bg-base-100 border shadow'>
|
||||
<div className='divide-y'>
|
||||
<div className='card bg-base-100 border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label='Line Height'
|
||||
@@ -112,7 +114,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
step={0.1}
|
||||
/>
|
||||
<div className='config-item config-item-bottom'>
|
||||
<span className='text-gray-700'>Full Justification</span>
|
||||
<span className=''>Full Justification</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
@@ -121,7 +123,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
/>
|
||||
</div>
|
||||
<div className='config-item config-item-bottom'>
|
||||
<span className='text-gray-700'>Hyphenation</span>
|
||||
<span className=''>Hyphenation</span>
|
||||
<input
|
||||
type='checkbox'
|
||||
className='toggle'
|
||||
@@ -135,8 +137,8 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
<div className='w-full'>
|
||||
<h2 className='mb-2 font-medium'>Page</h2>
|
||||
<div className='card bg-base-100 border shadow'>
|
||||
<div className='divide-y'>
|
||||
<div className='card bg-base-100 border-base-200 border shadow'>
|
||||
<div className='divide-base-200 divide-y'>
|
||||
<NumberInput
|
||||
className='config-item-top'
|
||||
label='Margins (px)'
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import cssbeautify from 'cssbeautify';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
|
||||
const cssRegex =
|
||||
/((?:\s*)([\w#.@*,:\-.:>+~\[\]\"=(),*\s]+)\s*{(?:[\s]*)((?:[A-Za-z\- \s]+[:]\s*['"0-9\w .,\/\()\-!#%]+;?)*)*\s*}(?:\s*))/gim;
|
||||
@@ -10,6 +11,7 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { settings, isFontLayoutSettingsGlobal } = useReaderStore();
|
||||
const { setSettings, getView, getViewSettings, setViewSettings } = useReaderStore();
|
||||
const viewSettings = getViewSettings(bookKey)!;
|
||||
const { themeCode } = useTheme();
|
||||
|
||||
const [animated, setAnimated] = useState(viewSettings.animated!);
|
||||
const [userStylesheet, setUserStylesheet] = useState(viewSettings.userStylesheet!);
|
||||
@@ -43,7 +45,7 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
settings.globalViewSettings.userStylesheet = formattedCSS;
|
||||
setSettings(settings);
|
||||
}
|
||||
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings));
|
||||
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
|
||||
} catch (err) {
|
||||
setError('Invalid CSS: Please check your input.');
|
||||
console.log('CSS Error:', err);
|
||||
|
||||
@@ -55,14 +55,14 @@ const NumberInput: React.FC<NumberInputProps> = ({
|
||||
|
||||
return (
|
||||
<div className={clsx('config-item', className)}>
|
||||
<span className='text-gray-700'>{label}</span>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='text-base-content'>{label}</span>
|
||||
<div className='text-base-content flex items-center gap-2'>
|
||||
<input
|
||||
type='text'
|
||||
value={localValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleOnBlur}
|
||||
className='input input-ghost settings-content w-20 max-w-xs rounded border-0 bg-transparent px-3 py-1 text-right !outline-none'
|
||||
className='input input-ghost settings-content text-base-content w-20 max-w-xs rounded border-0 bg-transparent px-3 py-1 text-right !outline-none'
|
||||
onFocus={(e) => e.target.select()}
|
||||
/>
|
||||
<button
|
||||
|
||||
@@ -25,28 +25,28 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
|
||||
<div className='dialog-header bg-base-100 sticky top-0 z-10 flex items-center justify-center px-4 pt-2'>
|
||||
<div className='dialog-tabs flex h-10 max-w-[80%] flex-grow items-center justify-around'>
|
||||
<button
|
||||
className={`btn btn-ghost h-8 min-h-8 px-4 ${activePanel === 'Font' ? 'btn-active' : ''}`}
|
||||
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Font' ? 'btn-active' : ''}`}
|
||||
onClick={() => setActivePanel('Font')}
|
||||
>
|
||||
<RiFontSize size={20} className='mr-0' />
|
||||
Font
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-ghost h-8 min-h-8 px-4 ${activePanel === 'Layout' ? 'btn-active' : ''}`}
|
||||
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Layout' ? 'btn-active' : ''}`}
|
||||
onClick={() => setActivePanel('Layout')}
|
||||
>
|
||||
<RiDashboardLine size={20} className='mr-0' />
|
||||
Layout
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-ghost h-8 min-h-8 px-4 ${activePanel === 'Color' ? 'btn-active' : ''}`}
|
||||
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Color' ? 'btn-active' : ''}`}
|
||||
onClick={() => setActivePanel('Color')}
|
||||
>
|
||||
<VscSymbolColor size={20} className='mr-0' />
|
||||
Color
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-ghost h-8 min-h-8 px-4 ${activePanel === 'Misc' ? 'btn-active' : ''}`}
|
||||
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Misc' ? 'btn-active' : ''}`}
|
||||
onClick={() => setActivePanel('Misc')}
|
||||
>
|
||||
<IoAccessibilityOutline size={20} className='mr-0' />
|
||||
@@ -70,7 +70,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-2 flex-grow overflow-y-auto px-[10%]'>
|
||||
<div className='text-base-content mt-2 flex-grow overflow-y-auto px-[10%]'>
|
||||
{activePanel === 'Font' && <FontPanel bookKey={bookKey} />}
|
||||
{activePanel === 'Layout' && <LayoutPanel bookKey={bookKey} />}
|
||||
{activePanel === 'Color' && <ColorPanel bookKey={bookKey} />}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import { MdInfoOutline } from 'react-icons/md';
|
||||
import { formatAuthors } from '@/utils/book';
|
||||
|
||||
@@ -11,20 +12,22 @@ interface BookCardProps {
|
||||
const BookCard: React.FC<BookCardProps> = ({ cover, title, author }) => {
|
||||
return (
|
||||
<div className='flex h-20 w-full items-center'>
|
||||
<img
|
||||
<Image
|
||||
src={cover}
|
||||
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'
|
||||
/>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<h4 className='line-clamp-2 w-[90%] text-sm font-semibold'>{title}</h4>
|
||||
<p className='truncate text-sm text-gray-600'>{formatAuthors(author)}</p>
|
||||
<p className='text-neutral-content truncate text-sm'>{formatAuthors(author)}</p>
|
||||
</div>
|
||||
<button
|
||||
className='btn btn-ghost h-6 min-h-6 w-6 rounded-full p-0 transition-colors hover:bg-gray-300'
|
||||
className='btn btn-ghost hover:bg-base-300 h-6 min-h-6 w-6 rounded-full p-0 transition-colors'
|
||||
aria-label='More info'
|
||||
>
|
||||
<MdInfoOutline size={18} className='text-gray-600' />
|
||||
<MdInfoOutline size={18} className='fill-base-content' />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
import useBooksManager from '../../hooks/useBooksManager';
|
||||
|
||||
interface BookMenuProps {
|
||||
@@ -22,29 +24,12 @@ const BookMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className='book-menu dropdown-content dropdown-center z-20 mt-3 w-56 border bg-white shadow-2xl'
|
||||
className='book-menu dropdown-content dropdown-center border-base-100 z-20 mt-3 w-56 shadow-2xl'
|
||||
>
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={handleOpenSplitView}
|
||||
>
|
||||
<span className='ml-2'>Parallel Read</span>
|
||||
<span className='text-sm text-gray-400'>Shift+P</span>
|
||||
</button>
|
||||
<button
|
||||
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={handleReloadPage}
|
||||
>
|
||||
<span className='ml-2'>Reload Page</span>
|
||||
<span className='text-sm text-gray-400'>Shift+R</span>
|
||||
</button>
|
||||
<hr className='my-1' />
|
||||
<button
|
||||
className='flex w-full items-center rounded-md p-2 hover:bg-gray-100'
|
||||
onClick={showAboutReadest}
|
||||
>
|
||||
<span className='ml-2'>About Readest</span>
|
||||
</button>
|
||||
<MenuItem label='Parallel Read' shortcut='Shift+P' onClick={handleOpenSplitView} />
|
||||
<MenuItem label='Reload Page' shortcut='Shift+R' onClick={handleReloadPage} />
|
||||
<hr className='border-base-200 my-1' />
|
||||
<MenuItem label='About Readest' onClick={showAboutReadest} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item, editable = f
|
||||
className={clsx(
|
||||
'border-base-300 my-2 cursor-pointer rounded-lg p-2 text-sm',
|
||||
editable && 'collapse-arrow collapse',
|
||||
isCurrent ? 'bg-base-300 hover:bg-gray-300/70' : 'hover:bg-base-300 bg-white',
|
||||
isCurrent ? 'bg-base-300/85 hover:bg-base-300' : 'hover:bg-base-200 bg-base-100',
|
||||
)}
|
||||
tabIndex={0}
|
||||
onClick={handleClickItem}
|
||||
|
||||
@@ -33,22 +33,22 @@ const SidebarHeader: React.FC<{
|
||||
onClick={onToggleSearchBar}
|
||||
className={clsx(
|
||||
'btn btn-ghost left-0 h-8 min-h-8 w-8 p-0',
|
||||
isSearchBarVisible ? 'bg-gray-300 hover:bg-gray-300' : 'bg-base-200 hover:bg-base-200',
|
||||
isSearchBarVisible ? 'bg-base-300' : '',
|
||||
)}
|
||||
>
|
||||
<FiSearch size={18} />
|
||||
<FiSearch size={18} className='text-base-content' />
|
||||
</button>
|
||||
<Dropdown
|
||||
className='dropdown-bottom flex justify-center'
|
||||
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
|
||||
toggleButton={<MdOutlineMenu size={20} />}
|
||||
toggleButton={<MdOutlineMenu size={20} className='fill-base-content' />}
|
||||
>
|
||||
<BookMenu />
|
||||
</Dropdown>
|
||||
<div className='right-0 flex h-8 w-8 items-center justify-center'>
|
||||
<button
|
||||
onClick={onTogglePin}
|
||||
className={`${isPinned ? 'bg-gray-300' : 'bg-base-300'} btn btn-ghost btn-circle h-6 min-h-6 w-6`}
|
||||
className={`${isPinned ? 'bg-base-300' : 'bg-base-300/65'} btn btn-ghost btn-circle h-6 min-h-6 w-6`}
|
||||
>
|
||||
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
|
||||
</button>
|
||||
|
||||
@@ -38,7 +38,7 @@ const SideBar: React.FC<{
|
||||
settings.globalReadSettings.isSideBarPinned,
|
||||
);
|
||||
|
||||
const onSearchEvent = (event: CustomEvent) => {
|
||||
const onSearchEvent = async (event: CustomEvent) => {
|
||||
const { term } = event.detail;
|
||||
setSideBarVisible(true);
|
||||
setIsSearchBarVisible(true);
|
||||
@@ -123,7 +123,7 @@ const SideBar: React.FC<{
|
||||
onSearchResultChange={setSearchResults}
|
||||
/>
|
||||
</div>
|
||||
<div className='border-b px-3'>
|
||||
<div className='border-base-300/50 border-b px-3'>
|
||||
<BookCard cover={book.coverImageUrl!} title={book.title} author={book.author} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TOCItem } from '@/libs/document';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { findParentPath } from '@/utils/toc';
|
||||
import { getContentMd5 } from '@/utils/misc';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const createExpanderIcon = (isExpanded: boolean) => {
|
||||
return (
|
||||
@@ -11,8 +12,12 @@ const createExpanderIcon = (isExpanded: boolean) => {
|
||||
viewBox='0 0 8 10'
|
||||
width='8'
|
||||
height='10'
|
||||
className={`transform transition-transform ${isExpanded ? 'rotate-90' : 'rotate-0'}`}
|
||||
className={clsx(
|
||||
'text-base-content transform transition-transform',
|
||||
isExpanded ? 'rotate-90' : 'rotate-0',
|
||||
)}
|
||||
style={{ transformOrigin: 'center' }}
|
||||
fill='currentColor'
|
||||
>
|
||||
<polygon points='0 0, 8 5, 0 10' />
|
||||
</svg>
|
||||
@@ -59,7 +64,7 @@ const TOCItemView: React.FC<{
|
||||
aria-selected={isActive ? 'true' : 'false'}
|
||||
data-href={item.href ? getContentMd5(item.href) : undefined}
|
||||
className={`flex w-full cursor-pointer items-center rounded-md py-2 ${
|
||||
isActive ? 'bg-gray-300 hover:bg-gray-400' : 'hover:bg-gray-300'
|
||||
isActive ? 'bg-base-300/85 hover:bg-base-300' : 'hover:bg-base-300/85'
|
||||
}`}
|
||||
>
|
||||
{item.subitems && (
|
||||
|
||||
@@ -5,7 +5,7 @@ const TabNavigation: React.FC<{
|
||||
activeTab: string;
|
||||
onTabChange: (tab: string) => void;
|
||||
}> = ({ activeTab, onTabChange }) => (
|
||||
<div className='bottom-tab flex w-full border-t'>
|
||||
<div className='bottom-tab border-base-300/50 flex w-full border-t'>
|
||||
{['toc', 'annotations', 'bookmarks'].map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
|
||||
@@ -42,11 +42,7 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
<div
|
||||
tabIndex={-1}
|
||||
onClick={toggleDropdown}
|
||||
className={clsx(
|
||||
'dropdown-toggle hover:bg-gray-300',
|
||||
buttonClassName,
|
||||
isOpen && 'bg-base-300',
|
||||
)}
|
||||
className={clsx('dropdown-toggle', buttonClassName, isOpen && 'bg-base-200')}
|
||||
>
|
||||
{toggleButton}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
interface MenuItemProps {
|
||||
label: string;
|
||||
shortcut?: string;
|
||||
icon?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const MenuItem: React.FC<MenuItemProps> = ({ label, shortcut, icon, onClick, disabled }) => {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
'hover:bg-neutral text-base-content flex w-full items-center justify-between rounded-md p-2',
|
||||
disabled && 'btn-disabled text-gray-400',
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
{<span style={{ minWidth: '20px' }}>{icon}</span>}
|
||||
<span className='ml-2'>{label}</span>
|
||||
</div>
|
||||
{shortcut && <span className='text-neutral-content text-sm'>{shortcut}</span>}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuItem;
|
||||
@@ -84,7 +84,7 @@ const WindowButtons: React.FC<WindowButtonsProps> = ({
|
||||
{showMinimize && (
|
||||
<button
|
||||
onClick={handleMinimize}
|
||||
className='window-button'
|
||||
className='window-button text-base-content'
|
||||
aria-label='Minimize'
|
||||
id='titlebar-minimize'
|
||||
>
|
||||
@@ -97,7 +97,7 @@ const WindowButtons: React.FC<WindowButtonsProps> = ({
|
||||
{showMaximize && (
|
||||
<button
|
||||
onClick={handleMaximize}
|
||||
className='window-button'
|
||||
className='window-button text-base-content'
|
||||
aria-label='Maximize/Restore'
|
||||
id='titlebar-maximize'
|
||||
>
|
||||
@@ -110,7 +110,7 @@ const WindowButtons: React.FC<WindowButtonsProps> = ({
|
||||
{showClose && (
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className='window-button'
|
||||
className='window-button text-base-content'
|
||||
aria-label='Close'
|
||||
id='titlebar-close'
|
||||
>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ThemeCode } from '@/utils/style';
|
||||
import { themes } from '@/styles/themes';
|
||||
|
||||
export type ThemeMode = 'auto' | 'light' | 'dark';
|
||||
|
||||
export const useTheme = () => {
|
||||
const [themeMode, setThemeMode] = useState<ThemeMode>(() => {
|
||||
if (typeof window !== 'undefined' && localStorage) {
|
||||
return (localStorage.getItem('themeMode') as ThemeMode) || 'auto';
|
||||
}
|
||||
return 'auto';
|
||||
});
|
||||
|
||||
const [themeColor, setThemeColor] = useState(() => {
|
||||
if (typeof window !== 'undefined' && localStorage) {
|
||||
return localStorage.getItem('themeColor') || 'default';
|
||||
}
|
||||
return 'default';
|
||||
});
|
||||
|
||||
const [systemIsDarkMode, setSystemIsDarkMode] = useState(() => {
|
||||
return (
|
||||
typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
);
|
||||
});
|
||||
|
||||
const isDarkMode = themeMode === 'dark' || (themeMode === 'auto' && systemIsDarkMode);
|
||||
|
||||
const [themeCode, setThemeCode] = useState<ThemeCode>(() => {
|
||||
let themeMode = 'auto';
|
||||
let themeColor = 'default';
|
||||
let systemIsDarkMode = false;
|
||||
if (typeof window !== 'undefined') {
|
||||
themeColor = localStorage.getItem('themeColor') || 'default';
|
||||
themeMode = localStorage.getItem('themeMode') as ThemeMode;
|
||||
systemIsDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
const isDarkMode = themeMode === 'dark' || (themeMode === 'auto' && systemIsDarkMode);
|
||||
const defaultTheme = themes.find((theme) => theme.name === themeColor);
|
||||
const defaultPalette = isDarkMode ? defaultTheme!.colors.dark : defaultTheme!.colors.light;
|
||||
return {
|
||||
bg: defaultPalette['base-100'],
|
||||
fg: defaultPalette['base-content'],
|
||||
primary: defaultPalette.primary,
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const handleSystemThemeChange = () => {
|
||||
setSystemIsDarkMode(mediaQuery.matches);
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleSystemThemeChange);
|
||||
return () => mediaQuery.removeEventListener('change', handleSystemThemeChange);
|
||||
}
|
||||
return undefined;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute(
|
||||
'data-theme',
|
||||
`${themeColor}-${isDarkMode ? 'dark' : 'light'}`,
|
||||
);
|
||||
}, [themeColor, isDarkMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined' && localStorage) {
|
||||
localStorage.setItem('themeMode', themeMode);
|
||||
localStorage.setItem('themeColor', themeColor);
|
||||
}
|
||||
const theme = themes.find((t) => t.name === themeColor);
|
||||
const palette = isDarkMode ? theme!.colors.dark : theme!.colors.light;
|
||||
setThemeCode({
|
||||
bg: palette['base-100'],
|
||||
fg: palette['base-content'],
|
||||
primary: palette.primary,
|
||||
});
|
||||
}, [themeMode, themeColor]);
|
||||
|
||||
const updateThemeMode = (mode: ThemeMode) => setThemeMode(mode);
|
||||
const updateThemeColor = (color: string) => setThemeColor(color);
|
||||
|
||||
return {
|
||||
themeMode,
|
||||
themeColor,
|
||||
themeCode,
|
||||
isDarkMode,
|
||||
updateThemeMode,
|
||||
updateThemeColor,
|
||||
};
|
||||
};
|
||||
@@ -7,19 +7,18 @@
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
background: var(--background);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
html[data-page="default"] {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #222222;
|
||||
--foreground: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
/* html[data-page="default"] {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
@@ -29,7 +28,7 @@ html[data-page="library"] {
|
||||
|
||||
html[data-page="reader"] {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
} */
|
||||
|
||||
body {
|
||||
color: var(--foreground);
|
||||
@@ -45,14 +44,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
library-page {
|
||||
--background: #f3f4f6;
|
||||
}
|
||||
|
||||
reader-page {
|
||||
--background: #ffffff;
|
||||
}
|
||||
|
||||
foliate-view {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -79,7 +70,7 @@ foliate-view {
|
||||
.window-button {
|
||||
@apply inline-flex h-6 w-6 items-center justify-center rounded-full;
|
||||
@apply transform transition duration-200 ease-in-out hover:scale-105;
|
||||
@apply bg-base-300 hover:bg-gray-300;
|
||||
@apply bg-base-200/85 hover:bg-base-200;
|
||||
}
|
||||
|
||||
.rounded-window {
|
||||
@@ -100,7 +91,7 @@ foliate-view {
|
||||
|
||||
.dropdown-content {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
@apply bg-base-100;
|
||||
padding: 10px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 14px;
|
||||
@@ -118,7 +109,7 @@ foliate-view {
|
||||
}
|
||||
|
||||
.dropdown-content::before {
|
||||
border-bottom: 12px solid white;
|
||||
border-bottom: 12px solid theme('colors.base-100');
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -166,12 +157,11 @@ foliate-view {
|
||||
sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
@apply text-gray-700;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
@apply flex h-14 items-center justify-between p-4;
|
||||
@apply hover:bg-gray-50;
|
||||
@apply hover:bg-base-100/50;
|
||||
}
|
||||
|
||||
.config-item-top {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
type BaseColor = {
|
||||
bg: string;
|
||||
fg: string;
|
||||
primary: string;
|
||||
};
|
||||
|
||||
const generateLightPalette = ({ bg, fg, primary }: BaseColor) => {
|
||||
return {
|
||||
'base-100': bg, // Main background
|
||||
'base-200': tinycolor(bg).darken(5).toHexString(), // Slightly darker
|
||||
'base-300': tinycolor(bg).darken(15).toHexString(), // More darker
|
||||
'base-content': fg, // Main text color
|
||||
neutral: tinycolor(bg).darken(15).desaturate(20).toHexString(), // Muted neutral
|
||||
'neutral-content': tinycolor(fg).lighten(20).desaturate(20).toHexString(), // Slightly lighter text
|
||||
primary: primary,
|
||||
secondary: tinycolor(primary).lighten(20).toHexString(), // Lighter secondary
|
||||
accent: tinycolor(primary).analogous()[1]!.toHexString(), // Analogous accent
|
||||
};
|
||||
};
|
||||
|
||||
const generateDarkPalette = ({ bg, fg, primary }: BaseColor) => {
|
||||
return {
|
||||
'base-100': bg, // Main background
|
||||
'base-200': tinycolor(bg).lighten(5).toHexString(), // Slightly lighter
|
||||
'base-300': tinycolor(bg).lighten(15).toHexString(), // More lighter
|
||||
'base-content': fg, // Main text color
|
||||
neutral: tinycolor(bg).lighten(15).desaturate(20).toHexString(), // Muted neutral
|
||||
'neutral-content': tinycolor(fg).darken(20).desaturate(20).toHexString(), // Darkened text
|
||||
primary: primary,
|
||||
secondary: tinycolor(primary).darken(20).toHexString(), // Darker secondary
|
||||
accent: tinycolor(primary).triad()[1]!.toHexString(), // Triad accent
|
||||
};
|
||||
};
|
||||
|
||||
export const themes = [
|
||||
{
|
||||
name: 'default',
|
||||
label: 'Default',
|
||||
colors: {
|
||||
light: generateLightPalette({ fg: '#171717', bg: '#ffffff', primary: '#0066cc' }),
|
||||
dark: generateDarkPalette({ fg: '#e0e0e0', bg: '#222222', primary: '#77bbee' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gray',
|
||||
label: 'Gray',
|
||||
colors: {
|
||||
light: generateLightPalette({ fg: '#222222', bg: '#e0e0e0', primary: '#4488cc' }),
|
||||
dark: generateDarkPalette({ fg: '#c6c6c6', bg: '#444444', primary: '#88ccee' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'sepia',
|
||||
label: 'Sepia',
|
||||
colors: {
|
||||
light: generateLightPalette({ fg: '#5b4636', bg: '#f1e8d0', primary: '#008b8b' }),
|
||||
dark: generateDarkPalette({ fg: '#ffd595', bg: '#342e25', primary: '#48d1cc' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'grass',
|
||||
label: 'Grass',
|
||||
colors: {
|
||||
light: generateLightPalette({ fg: '#232c16', bg: '#d7dbbd', primary: '#177b4d' }),
|
||||
dark: generateDarkPalette({ fg: '#d8deba', bg: '#333627', primary: '#a6d608' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'cherry',
|
||||
label: 'Cherry',
|
||||
colors: {
|
||||
light: generateLightPalette({ fg: '#4e1609', bg: '#f0d1d5', primary: '#de3838' }),
|
||||
dark: generateDarkPalette({ fg: '#e5c4c8', bg: '#462f32', primary: '#ff646e' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'sky',
|
||||
label: 'Sky',
|
||||
colors: {
|
||||
light: generateLightPalette({ fg: '#262d48', bg: '#cedef5', primary: '#2d53e5' }),
|
||||
dark: generateDarkPalette({ fg: '#babee1', bg: '#282e47', primary: '#ff646e' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'solarized',
|
||||
label: 'Solarized',
|
||||
colors: {
|
||||
light: generateLightPalette({ fg: '#586e75', bg: '#fdf6e3', primary: '#268bd2' }),
|
||||
dark: generateDarkPalette({ fg: '#93a1a1', bg: '#002b36', primary: '#268bd2' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'gruvbox',
|
||||
label: 'Gruvbox',
|
||||
colors: {
|
||||
light: generateLightPalette({ fg: '#3c3836', bg: '#fbf1c7', primary: '#076678' }),
|
||||
dark: generateDarkPalette({ fg: '#ebdbb2', bg: '#282828', primary: '#83a598' }),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'nord',
|
||||
label: 'Nord',
|
||||
colors: {
|
||||
light: generateLightPalette({ fg: '#2e3440', bg: '#eceff4', primary: '#5e81ac' }),
|
||||
dark: generateDarkPalette({ fg: '#d8dee9', bg: '#2e3440', primary: '#88c0d0' }),
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,35 +1,31 @@
|
||||
class EventDispatcher {
|
||||
private syncListeners: Map<string, Set<(event: CustomEvent) => boolean>>;
|
||||
private asyncListeners: Map<string, Set<(event: CustomEvent) => Promise<boolean>>>;
|
||||
private asyncListeners: Map<string, Set<(event: CustomEvent) => Promise<void>>>;
|
||||
|
||||
constructor() {
|
||||
this.syncListeners = new Map();
|
||||
this.asyncListeners = new Map();
|
||||
}
|
||||
|
||||
on(event: string, callback: (event: CustomEvent) => Promise<boolean>): void {
|
||||
on(event: string, callback: (event: CustomEvent) => Promise<void>): void {
|
||||
if (!this.asyncListeners.has(event)) {
|
||||
this.asyncListeners.set(event, new Set());
|
||||
}
|
||||
this.asyncListeners.get(event)!.add(callback);
|
||||
}
|
||||
|
||||
off(event: string, callback: (event: CustomEvent) => Promise<boolean>): void {
|
||||
off(event: string, callback: (event: CustomEvent) => Promise<void>): void {
|
||||
this.asyncListeners.get(event)?.delete(callback);
|
||||
}
|
||||
|
||||
async dispatch(event: string, detail?: unknown): Promise<boolean> {
|
||||
async dispatch(event: string, detail?: unknown): Promise<void> {
|
||||
const listeners = this.asyncListeners.get(event);
|
||||
if (listeners) {
|
||||
const customEvent = new CustomEvent(event, { detail });
|
||||
for (const listener of listeners) {
|
||||
const consumed = await listener(customEvent);
|
||||
if (consumed) {
|
||||
return true;
|
||||
}
|
||||
listener(customEvent);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
onSync(event: string, callback: (event: CustomEvent) => boolean): void {
|
||||
|
||||
@@ -34,10 +34,20 @@ const getLayoutStyles = (
|
||||
justify: boolean,
|
||||
hyphenate: boolean,
|
||||
zoomLevel: number,
|
||||
bg: string,
|
||||
fg: string,
|
||||
primary: string,
|
||||
) => `
|
||||
@namespace epub "http://www.idpf.org/2007/ops";
|
||||
html {
|
||||
color-scheme: light dark;
|
||||
color: ${fg};
|
||||
}
|
||||
a:any-link {
|
||||
color: ${primary};
|
||||
}
|
||||
aside[epub|type~="footnote"] {
|
||||
display: none;
|
||||
}
|
||||
/* https://github.com/whatwg/html/issues/5426 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@@ -45,11 +55,34 @@ const getLayoutStyles = (
|
||||
color: lightblue;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
line-height: ${spacing};
|
||||
hanging-punctuation: allow-end last;
|
||||
orphans: 2;
|
||||
widows: 2;
|
||||
}
|
||||
[align="left"] { text-align: left; }
|
||||
[align="right"] { text-align: right; }
|
||||
[align="center"] { text-align: center; }
|
||||
[align="justify"] { text-align: justify; }
|
||||
:is(hgroup, header) p {
|
||||
text-align: unset;
|
||||
hyphens: unset;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap !important;
|
||||
tab-size: 2;
|
||||
}
|
||||
body {
|
||||
zoom: ${zoomLevel}%;
|
||||
background-color: ${bg} !important;
|
||||
}
|
||||
svg, img {
|
||||
background-color: transparent !important;
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
p, li, blockquote, dd {
|
||||
line-height: ${spacing};
|
||||
text-align: ${justify ? 'justify' : 'start'};
|
||||
-webkit-hyphens: ${hyphenate ? 'auto' : 'manual'};
|
||||
hyphens: ${hyphenate ? 'auto' : 'manual'};
|
||||
@@ -76,13 +109,22 @@ const getLayoutStyles = (
|
||||
}
|
||||
`;
|
||||
|
||||
export const getStyles = (viewSettings: ViewSettings) => {
|
||||
export interface ThemeCode {
|
||||
bg: string;
|
||||
fg: string;
|
||||
primary: string;
|
||||
}
|
||||
|
||||
export const getStyles = (viewSettings: ViewSettings, themeCode: ThemeCode) => {
|
||||
const layoutStyles = getLayoutStyles(
|
||||
viewSettings.lineHeight!,
|
||||
viewSettings.fullJustification!,
|
||||
viewSettings.hyphenation!,
|
||||
// FIXME: zoom level is not working in paginated mode
|
||||
viewSettings.scrolled ? viewSettings.zoomLevel! : 100,
|
||||
themeCode.bg,
|
||||
themeCode.fg,
|
||||
themeCode.primary,
|
||||
);
|
||||
const fontStyles = getFontStyles(
|
||||
viewSettings.serifFont!,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
import { themes } from './src/styles/themes';
|
||||
import daisyui from 'daisyui';
|
||||
|
||||
const config: Config = {
|
||||
@@ -22,5 +23,19 @@ const config: Config = {
|
||||
},
|
||||
},
|
||||
plugins: [daisyui],
|
||||
daisyui: {
|
||||
themes: themes.reduce(
|
||||
(acc, { name, colors }) => {
|
||||
acc.push({
|
||||
[`${name}-light`]: colors.light,
|
||||
});
|
||||
acc.push({
|
||||
[`${name}-dark`]: colors.dark,
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
['light', 'dark'] as (Record<string, unknown> | string)[],
|
||||
),
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
|
||||
Generated
+17
-1
@@ -86,6 +86,9 @@ importers:
|
||||
react-icons:
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0(react@18.3.1)
|
||||
tinycolor2:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0
|
||||
zustand:
|
||||
specifier: 5.0.0-rc.2
|
||||
version: 5.0.0-rc.2(@types/react@18.3.12)(react@18.3.1)
|
||||
@@ -105,6 +108,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: 18.3.1
|
||||
version: 18.3.1
|
||||
'@types/tinycolor2':
|
||||
specifier: ^1.4.6
|
||||
version: 1.4.6
|
||||
autoprefixer:
|
||||
specifier: ^10.4.20
|
||||
version: 10.4.20(postcss@8.4.47)
|
||||
@@ -749,6 +755,9 @@ packages:
|
||||
'@types/resolve@1.20.2':
|
||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||
|
||||
'@types/tinycolor2@1.4.6':
|
||||
resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.11.0':
|
||||
resolution: {integrity: sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -3420,6 +3429,9 @@ packages:
|
||||
thenify@3.3.1:
|
||||
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
|
||||
|
||||
tinycolor2@1.6.0:
|
||||
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
|
||||
|
||||
to-fast-properties@1.0.3:
|
||||
resolution: {integrity: sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -4109,6 +4121,8 @@ snapshots:
|
||||
|
||||
'@types/resolve@1.20.2': {}
|
||||
|
||||
'@types/tinycolor2@1.4.6': {}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.11.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.13.0(jiti@1.21.6))(typescript@5.6.2)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.11.1
|
||||
@@ -7431,7 +7445,7 @@ snapshots:
|
||||
terser@5.36.0:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.6
|
||||
acorn: 8.13.0
|
||||
acorn: 8.14.0
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
@@ -7445,6 +7459,8 @@ snapshots:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
|
||||
tinycolor2@1.6.0: {}
|
||||
|
||||
to-fast-properties@1.0.3: {}
|
||||
|
||||
to-object-path@0.3.0:
|
||||
|
||||
Reference in New Issue
Block a user