Add reader view settings

This commit is contained in:
chrox
2024-10-30 18:08:54 +01:00
parent d835b13fd2
commit f5a233ba31
20 changed files with 500 additions and 284 deletions
@@ -45,6 +45,7 @@ const getCSS = (spacing: number, justify: boolean, hyphenate: boolean) => `
export interface FoliateView extends HTMLElement {
open: (book: BookDoc) => Promise<void>;
close: () => void;
init: (options: { lastLocation: string }) => void;
goTo: (href: string) => void;
goToFraction: (fraction: number) => void;
@@ -98,12 +99,29 @@ const FoliateViewer: React.FC<{
);
};
const handleMousedown = (event: MouseEvent) => {
window.postMessage(
{
type: 'iframe-mousedown',
button: event.button,
clientX: event.clientX,
clientY: event.clientY,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
altKey: event.altKey,
metaKey: event.metaKey,
},
'*',
);
};
const docLoadHandler = (event: Event) => {
const detail = (event as CustomEvent).detail;
if (detail.doc) {
if (!detail.doc.isKeydownListenerAdded) {
detail.doc?.addEventListener('keydown', handleKeydown);
detail.doc.isKeydownListenerAdded = true;
if (!detail.doc.isEventListenersAdded) {
detail.doc.addEventListener('keydown', handleKeydown);
detail.doc.addEventListener('mousedown', handleMousedown);
detail.doc.isEventListenersAdded = true;
}
}
};
@@ -122,11 +140,18 @@ const FoliateViewer: React.FC<{
setFoliateView(bookKey, view);
await view.open(bookDoc);
const viewSettings = bookConfig.viewSettings!;
if ('setStyles' in view.renderer) {
view.renderer.setStyles(getCSS(2.4, true, true));
const lineHeight = viewSettings.lineHeight!;
const justify = viewSettings.justify!;
const hyphenate = viewSettings.hyphenate!;
view.renderer.setStyles(getCSS(lineHeight, justify, hyphenate));
}
const isScrolled = viewSettings.scrolled!;
const gap = viewSettings.gap!;
view.renderer.setAttribute('flow', isScrolled ? 'scrolled' : 'paginated');
view.renderer.setAttribute('margin', '44px');
view.renderer.setAttribute('gap', '4%');
view.renderer.setAttribute('gap', `${gap * 100}%`);
const lastLocation = bookConfig.location;
if (lastLocation) {
view.init({ lastLocation });
@@ -41,7 +41,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
<div
className={clsx(
'footer-bar absolute bottom-0 z-10 flex h-12 w-full items-center px-4',
'shadow-xs bg-base-100 rounded-window transition-opacity duration-300',
'shadow-xs bg-base-100 rounded-window-bottom-right transition-opacity duration-300',
isHoveredAnim && 'hover-bar-anim',
hoveredBookKey === bookKey ? `opacity-100` : `opacity-0`,
)}
@@ -49,7 +49,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
ref={headerRef}
className={clsx(
`header-bar absolute top-0 z-10 flex h-11 w-full items-center px-4`,
`shadow-xs bg-base-100 rounded-window transition-opacity duration-300`,
`shadow-xs bg-base-100 rounded-window-top-right transition-opacity duration-300`,
isHoveredAnim && 'hover-bar-anim',
hoveredBookKey === bookKey || isDropdownOpen ? `opacity-100` : `opacity-0`,
isDropdownOpen && 'header-bar-pinned',
@@ -78,7 +78,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
toggleButton={<PiDotsThreeVerticalBold size={16} />}
onToggle={setIsDropdownOpen}
>
<ViewMenu />
<ViewMenu bookKey={bookKey} />
</Dropdown>
<WindowButtons
@@ -22,14 +22,12 @@ const ReaderContent = () => {
const ids = (searchParams.get('ids') || '').split(',');
const { envConfig } = useEnv();
const { books, settings, initBookState, saveConfig, saveSettings } = useReaderStore();
const { getFoliateView } = useReaderStore();
const { books, settings } = useReaderStore();
const { initBookState, closeBook, saveConfig, saveSettings, getFoliateView } = useReaderStore();
const [sideBarWidth, setSideBarWidth] = useState(
settings.globalReadSettings.sideBarWidth ?? '25%',
);
const [sideBarWidth, setSideBarWidth] = useState(settings.globalReadSettings.sideBarWidth);
const [isSideBarPinned, setIsSideBarPinned] = useState(
settings.globalReadSettings.isSideBarPinned ?? true,
settings.globalReadSettings.isSideBarPinned,
);
const [isSideBarVisible, setSideBarVisibility] = useState(isSideBarPinned);
@@ -41,22 +39,27 @@ const ReaderContent = () => {
useEffect(() => {
if (ids.length === 0) return;
const uniqueIds = new Set<string>();
console.log('fetching books', ids);
ids.forEach((id, index) => {
const isPrimary = !uniqueIds.has(id);
uniqueIds.add(id);
const key = getKey(id, index);
if (books[key]) return;
console.log('fetching book', key);
console.log('initing state', key);
initBookState(envConfig, id, key, isPrimary);
});
}, [ids, settings]);
}, [searchParams]);
const getNextBookKey = (bookKey: string) => {
const bookKeys = ids.map((id, index) => getKey(id, index));
const index = bookKeys.findIndex((key) => key === bookKey);
const nextIndex = (index + 1) % bookKeys.length;
return bookKeys[nextIndex]!;
};
const switchSidebar = () => {
setSideBarBookKey((prevKey: string) => {
const bookKeys = ids.map((id, index) => getKey(id, index));
const index = bookKeys.findIndex((key) => prevKey === key);
const nextIndex = (index + 1) % bookKeys.length;
return bookKeys[nextIndex]!;
return getNextBookKey(prevKey);
});
};
@@ -65,7 +68,6 @@ const ReaderContent = () => {
};
const openSplitView = () => {
console.log('open split view');
const params = new URLSearchParams(searchParams.toString());
const sideBarBookId = sideBarBookKey.split('-')[0];
const updatedIds = [...ids, sideBarBookId].join(',');
@@ -110,40 +112,41 @@ const ReaderContent = () => {
settings.globalReadSettings.isSideBarPinned = !isSideBarPinned;
};
const closeBook = (bookKey: string) => {
const saveConfigAndCloseBook = (bookKey: string) => {
getFoliateView(bookKey)?.close();
getFoliateView(bookKey)?.remove();
const bookState = books[bookKey];
if (!bookState) return;
const { book, config, isPrimary } = bookState;
if (isPrimary && book && config) {
saveConfig(envConfig, book, config);
saveConfig(envConfig, book, config, settings);
}
closeBook(bookKey);
};
const saveSettingsAndGoToLibrary = () => {
saveSettings(envConfig, settings);
router.replace('/library');
};
const handleCloseBooks = () => {
ids.forEach((id, index) => {
const key = getKey(id, index);
closeBook(key);
saveConfigAndCloseBook(key);
});
saveSettings(envConfig, settings);
router.replace('/library');
saveSettingsAndGoToLibrary();
};
const handleCloseBook = (bookKey: string) => {
closeBook(bookKey);
saveConfigAndCloseBook(bookKey);
setSideBarBookKey((prevKey: string) => {
if (prevKey === bookKey) {
const index = ids.findIndex((id) => prevKey.startsWith(id));
const nextIndex = (index + 1) % ids.length;
return getKey(ids[nextIndex]!, nextIndex);
}
return prevKey;
return prevKey === bookKey ? getNextBookKey(prevKey) : prevKey;
});
const newIds = ids.filter((id, index) => getKey(id, index) !== bookKey);
if (newIds.length > 0) {
router.replace(`/reader?ids=${newIds.join(',')}`);
router.push(`/reader?ids=${newIds.join(',')}`);
} else {
saveSettings(envConfig, settings);
router.back();
saveSettingsAndGoToLibrary();
}
};
@@ -174,7 +177,7 @@ const ReaderContent = () => {
const bookState = bookStates[0];
if (bookStates.length !== ids.length || !bookState || !bookState.book || !bookState.bookDoc) {
return (
<div className={'flex-1'}>
<div className={'hero hero-content min-h-screen'}>
<Spinner loading={true} />
{bookState?.error && (
<div className='text-center'>
@@ -214,6 +217,7 @@ const ReaderContent = () => {
const { book, config, bookDoc } = bookState;
if (!book || !config || !bookDoc) return null;
const { section, pageinfo, progress, chapter } = config;
const scrolled = config.viewSettings!.scrolled;
return (
<div key={key} className='relative h-full w-full overflow-hidden'>
<HeaderBar
@@ -229,8 +233,10 @@ const ReaderContent = () => {
onCloseBook={handleCloseBook}
/>
<FoliateViewer bookKey={key} bookDoc={bookDoc!} bookConfig={config!} />
<SectionInfo chapter={chapter} />
<PageInfo bookFormat={book.format} section={section} pageinfo={pageinfo} />
{!scrolled && <SectionInfo chapter={chapter} />}
{!scrolled && (
<PageInfo bookFormat={book.format} section={section} pageinfo={pageinfo} />
)}
<FooterBar
bookKey={key}
progress={progress}
@@ -88,7 +88,8 @@ const SideBar: React.FC<{
return isVisible ? (
<div
className={clsx(
'sidebar-container bg-base-200 rounded-window-left z-20 h-full select-none',
'sidebar-container bg-base-200 z-20 h-full select-none',
'rounded-window-top-left rounded-window-bottom-left',
!isPinned && 'shadow-2xl',
)}
style={{
@@ -1,31 +1,51 @@
import React from 'react';
import clsx from 'clsx';
import React, { useEffect } from 'react';
import { useState } from 'react';
import { BiMoon, BiSun } from 'react-icons/bi';
import { MdZoomOut, MdZoomIn, MdCheck } from 'react-icons/md';
import { useReaderStore } from '@/store/readerStore';
interface ViewMenuProps {
bookKey: string;
toggleDropdown?: () => void;
}
const ViewMenu: React.FC<ViewMenuProps> = ({}) => {
const [isDarkMode, setDarkMode] = useState(false);
const [isScrolledMode, setScrolledMode] = useState(false);
const [isInvertedColors, setInvertedColors] = useState(false);
const ViewMenu: React.FC<ViewMenuProps> = ({ bookKey }) => {
const { books, setConfig, getFoliateView } = useReaderStore();
const bookState = books[bookKey]!;
const config = bookState.config!;
const [isDarkMode, setDarkMode] = useState(config.viewSettings!.theme === 'dark');
const [isScrolledMode, setScrolledMode] = useState(config.viewSettings!.scrolled);
const [isInvertedColors, setInvertedColors] = useState(config.viewSettings!.invert);
const [zoomLevel, setZoomLevel] = useState(100);
const toggleDarkMode = () => setDarkMode(!isDarkMode);
const toggleScrolledMode = () => setScrolledMode(!isScrolledMode);
const toggleInvertedColors = () => setInvertedColors(!isInvertedColors);
const zoomIn = () => setZoomLevel((prev) => Math.min(prev + 10, 200));
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 = () => {
// open font layout menu
};
useEffect(() => {
const view = getFoliateView(bookKey);
view?.renderer.setAttribute('flow', isScrolledMode ? 'scrolled' : 'paginated');
config.viewSettings!.scrolled = isScrolledMode;
setConfig(bookKey, config);
}, [isScrolledMode]);
useEffect(() => {
document.body.classList.toggle('invert', isInvertedColors);
}, [isInvertedColors]);
return (
<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'
>
@@ -43,18 +63,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({}) => {
<MdZoomIn size={20} />
</button>
</div>
<hr className='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>
<hr className='my-1' />
<button
@@ -67,7 +76,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({}) => {
</div>
<span className='text-sm text-gray-400'>Shift+F</span>
</button>
<hr className='my-1' />
<button
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
onClick={toggleScrolledMode}
@@ -80,8 +89,25 @@ const ViewMenu: React.FC<ViewMenuProps> = ({}) => {
</div>
<span className='text-sm text-gray-400'>Shift+J</span>
</button>
<hr className='my-1' />
<button
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
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',
)}
onClick={toggleInvertedColors}
>
<div className='flex items-center'>