forked from akai/readest
Add reader view settings
This commit is contained in:
@@ -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'>
|
||||
|
||||
Reference in New Issue
Block a user