Add reader view settings
This commit is contained in:
Generated
+176
-175
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"productName": "Readest",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.5",
|
||||
"identifier": "com.bilingify.digest",
|
||||
"build": {
|
||||
"frontendDist": "../out",
|
||||
|
||||
@@ -62,14 +62,16 @@ foliate-view {
|
||||
.rounded-window {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.rounded-window-left {
|
||||
.rounded-window-top-left {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
.rounded-window-bottom-left {
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
|
||||
.rounded-window-right {
|
||||
.rounded-window-top-right {
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
.rounded-window-bottom-right {
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,10 +45,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={headerRef}
|
||||
className='titlebar rounded-window fixed top-0 z-10 w-full bg-gray-100 px-6 py-4'
|
||||
>
|
||||
<div ref={headerRef} className='titlebar fixed top-2 z-10 w-full bg-gray-100 px-6 py-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='sm:w relative flex w-full items-center'>
|
||||
<span className='absolute left-4 text-gray-500'>
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -7,11 +7,10 @@ import { useEnv } from '@/context/EnvContext';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
|
||||
import ReaderContent from './components/ReaderContent';
|
||||
import { DEFAULT_READSETTINGS } from '@/services/constants';
|
||||
|
||||
const ReaderPage = () => {
|
||||
const { envConfig } = useEnv();
|
||||
const { settings, setLibrary, setSettings } = useReaderStore();
|
||||
const { library, settings, setLibrary, setSettings } = useReaderStore();
|
||||
const isInitiating = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -20,9 +19,6 @@ const ReaderPage = () => {
|
||||
const initLibrary = async () => {
|
||||
const appService = await envConfig.getAppService();
|
||||
const settings = await appService.loadSettings();
|
||||
if (!settings.globalReadSettings) {
|
||||
settings.globalReadSettings = DEFAULT_READSETTINGS;
|
||||
}
|
||||
setSettings(settings);
|
||||
console.log('initializing library in reader');
|
||||
setLibrary(await appService.loadLibraryBooks());
|
||||
@@ -32,6 +28,7 @@ const ReaderPage = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
library.length > 0 &&
|
||||
settings.globalReadSettings && (
|
||||
<div className='min-h-screen select-none'>
|
||||
<Suspense>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useRef, useState, isValidElement, ReactElement } from 'react';
|
||||
import React, { useRef, useState, isValidElement, ReactElement, useEffect } from 'react';
|
||||
|
||||
interface DropdownProps {
|
||||
className?: string;
|
||||
@@ -25,13 +25,38 @@ const Dropdown: React.FC<DropdownProps> = ({
|
||||
onToggle?.(newIsOpen);
|
||||
};
|
||||
|
||||
const closeDropdown = () => {
|
||||
setIsOpen(false);
|
||||
onToggle?.(false);
|
||||
};
|
||||
|
||||
const handleBlur = (event: React.FocusEvent<HTMLDivElement>) => {
|
||||
if (!dropdownRef.current?.contains(event.relatedTarget as Node)) {
|
||||
setIsOpen(false);
|
||||
onToggle?.(false);
|
||||
closeDropdown();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickOutside = (event: MouseEvent | Event) => {
|
||||
if (event instanceof KeyboardEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
closeDropdown();
|
||||
}
|
||||
} else if (event instanceof MessageEvent) {
|
||||
if (event.data && event.data.type === 'iframe-mousedown') {
|
||||
closeDropdown();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mousedown', handleClickOutside);
|
||||
window.addEventListener('message', handleClickOutside);
|
||||
return () => {
|
||||
window.removeEventListener('mousedown', handleClickOutside);
|
||||
window.removeEventListener('message', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const childrenWithToggle = isValidElement(children)
|
||||
? React.cloneElement(children, { toggleDropdown })
|
||||
: children;
|
||||
|
||||
@@ -25,6 +25,11 @@ const WindowButtons: React.FC<WindowButtonsProps> = ({
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMouseDown = async (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest('#exclude-title-bar-mousedown')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
if (e.buttons === 1) {
|
||||
if (e.detail === 2) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AppService, ToastType } from '@/types/system';
|
||||
|
||||
import { SystemSettings } from '@/types/settings';
|
||||
import { FileSystem, BaseDir } from '@/types/system';
|
||||
import { Book, BookConfig, BookContent, BookFormat } from '@/types/book';
|
||||
import { Book, BookConfig, BookContent, BookFormat, ViewSettings } from '@/types/book';
|
||||
import {
|
||||
getDir,
|
||||
getFilename,
|
||||
@@ -15,7 +15,13 @@ import {
|
||||
import { RemoteFile } from '@/utils/file';
|
||||
import { partialMD5 } from '@/utils/md5';
|
||||
import { BookDoc, DocumentLoader } from '@/libs/document';
|
||||
import { DEFAULT_READSETTINGS, SYSTEM_SETTINGS_VERSION } from './constants';
|
||||
import {
|
||||
DEFAULT_BOOK_LAYOUT,
|
||||
DEFAULT_BOOK_STYLE,
|
||||
DEFAULT_BOOK_FONT,
|
||||
DEFAULT_READSETTINGS,
|
||||
SYSTEM_SETTINGS_VERSION,
|
||||
} from './constants';
|
||||
|
||||
export abstract class BaseAppService implements AppService {
|
||||
localBooksDir: string = '';
|
||||
@@ -47,11 +53,23 @@ export abstract class BaseAppService implements AppService {
|
||||
settings.localBooksDir = await this.getInitBooksDir();
|
||||
settings.version = SYSTEM_SETTINGS_VERSION;
|
||||
}
|
||||
settings.globalReadSettings = { ...DEFAULT_READSETTINGS, ...settings.globalReadSettings };
|
||||
settings.globalViewSettings = {
|
||||
...DEFAULT_BOOK_LAYOUT,
|
||||
...DEFAULT_BOOK_STYLE,
|
||||
...DEFAULT_BOOK_FONT,
|
||||
...settings.globalViewSettings,
|
||||
};
|
||||
} catch {
|
||||
settings = {
|
||||
version: SYSTEM_SETTINGS_VERSION,
|
||||
localBooksDir: await this.getInitBooksDir(),
|
||||
globalReadSettings: DEFAULT_READSETTINGS,
|
||||
globalViewSettings: {
|
||||
...DEFAULT_BOOK_LAYOUT,
|
||||
...DEFAULT_BOOK_STYLE,
|
||||
...DEFAULT_BOOK_FONT,
|
||||
},
|
||||
};
|
||||
|
||||
await this.fs.createDir('', 'Books', true);
|
||||
@@ -151,22 +169,39 @@ export abstract class BaseAppService implements AppService {
|
||||
}
|
||||
}
|
||||
|
||||
async loadBookContent(book: Book): Promise<BookContent> {
|
||||
async loadBookContent(book: Book, settings: SystemSettings): Promise<BookContent> {
|
||||
const fp = getFilename(book);
|
||||
const file = await new RemoteFile(this.fs.getURL(`${this.localBooksDir}/${fp}`), fp).open();
|
||||
return { book, file, config: await this.loadBookConfig(book) };
|
||||
return { book, file, config: await this.loadBookConfig(book, settings) };
|
||||
}
|
||||
|
||||
async loadBookConfig(book: Book): Promise<BookConfig> {
|
||||
async loadBookConfig(book: Book, settings: SystemSettings): Promise<BookConfig> {
|
||||
try {
|
||||
const str = await this.fs.readFile(getConfigFilename(book), 'Books', 'text');
|
||||
return JSON.parse(str as string);
|
||||
const config = JSON.parse(str as string) as BookConfig;
|
||||
const { globalViewSettings } = settings;
|
||||
const { viewSettings } = config;
|
||||
config.viewSettings = { ...globalViewSettings, ...viewSettings };
|
||||
return config;
|
||||
} catch {
|
||||
return INIT_BOOK_CONFIG;
|
||||
}
|
||||
}
|
||||
|
||||
async saveBookConfig(book: Book, config: BookConfig) {
|
||||
async saveBookConfig(book: Book, config: BookConfig, settings?: SystemSettings) {
|
||||
if (settings) {
|
||||
const globalViewSettings = settings.globalViewSettings as ViewSettings;
|
||||
const viewSettings = config.viewSettings as Partial<ViewSettings>;
|
||||
config.viewSettings = Object.entries(viewSettings).reduce(
|
||||
(acc: Partial<Record<keyof ViewSettings, unknown>>, [key, value]) => {
|
||||
if (globalViewSettings[key as keyof ViewSettings] !== value) {
|
||||
acc[key as keyof ViewSettings] = value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Partial<Record<keyof ViewSettings, unknown>>,
|
||||
) as Partial<ViewSettings>;
|
||||
}
|
||||
await this.fs.writeFile(getConfigFilename(book), 'Books', JSON.stringify(config));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,42 @@
|
||||
import { BookFont, BookLayout, BookStyle } from '@/types/book';
|
||||
import { ReadSettings } from '@/types/settings';
|
||||
|
||||
export const LOCAL_BOOKS_SUBDIR = 'Readest/Books';
|
||||
export const CLOUD_BOOKS_SUBDIR = 'Readest/Books';
|
||||
|
||||
export const DEFAULT_READSETTINGS: ReadSettings = {
|
||||
themeType: 'auto',
|
||||
fontFamily: '',
|
||||
fontSize: 1.0,
|
||||
wordSpacing: 0.16,
|
||||
lineSpacing: 1.5,
|
||||
|
||||
sideBarWidth: '20%',
|
||||
sideBarWidth: '25%',
|
||||
isSideBarPinned: true,
|
||||
autohideCursor: true,
|
||||
};
|
||||
|
||||
export const DEFAULT_BOOK_FONT: BookFont = {
|
||||
serif: 'Serif 12',
|
||||
sansSerif: 'Sans 12',
|
||||
monospace: 'Monospace 12',
|
||||
defaultFont: 'serif',
|
||||
defaultSize: 16,
|
||||
minimumSize: 8,
|
||||
fontWeight: 400,
|
||||
};
|
||||
|
||||
export const DEFAULT_BOOK_LAYOUT: BookLayout = {
|
||||
gap: 0.05,
|
||||
scrolled: false,
|
||||
maxColumnCount: 2,
|
||||
maxInlineSize: 720,
|
||||
maxBlockSize: 1440,
|
||||
animated: true,
|
||||
};
|
||||
|
||||
export const DEFAULT_BOOK_STYLE: BookStyle = {
|
||||
lineHeight: 1.5,
|
||||
justify: true,
|
||||
hyphenate: true,
|
||||
invert: false,
|
||||
theme: 'light',
|
||||
overrideFont: false,
|
||||
userStylesheet: '',
|
||||
};
|
||||
|
||||
export const SYSTEM_SETTINGS_VERSION = 1;
|
||||
|
||||
@@ -34,14 +34,21 @@ interface ReaderStore {
|
||||
section: PageInfo,
|
||||
pageinfo: PageInfo,
|
||||
) => void;
|
||||
setConfig: (key: string, config: BookConfig) => void;
|
||||
setFoliateView: (key: string, view: FoliateView) => void;
|
||||
getFoliateView: (key: string) => FoliateView | null;
|
||||
|
||||
deleteBook: (envConfig: EnvConfigType, book: Book) => void;
|
||||
saveConfig: (envConfig: EnvConfigType, book: Book, config: BookConfig) => void;
|
||||
saveConfig: (
|
||||
envConfig: EnvConfigType,
|
||||
book: Book,
|
||||
config: BookConfig,
|
||||
settings: SystemSettings,
|
||||
) => void;
|
||||
saveSettings: (envConfig: EnvConfigType, settings: SystemSettings) => void;
|
||||
|
||||
initBookState: (envConfig: EnvConfigType, id: string, key: string, isPrimary?: boolean) => void;
|
||||
|
||||
closeBook: (key: string) => void;
|
||||
addBookmark: (key: string, bookmark: BookNote) => void;
|
||||
}
|
||||
|
||||
@@ -64,6 +71,25 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
|
||||
setLibrary: (books: Book[]) => set({ library: books }),
|
||||
setSettings: (settings: SystemSettings) => set({ settings }),
|
||||
setConfig: (key: string, config: BookConfig) => {
|
||||
set((state) => {
|
||||
const book = state.books[key];
|
||||
if (!book) return state;
|
||||
return {
|
||||
books: {
|
||||
...state.books,
|
||||
[key]: {
|
||||
...book,
|
||||
config: {
|
||||
...book.config,
|
||||
...config,
|
||||
lastUpdated: Date.now(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setFoliateView: (key: string, view) =>
|
||||
set((state) => ({ foliateViews: { ...state.foliateViews, [key]: view } })),
|
||||
@@ -81,7 +107,12 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
set({ library });
|
||||
appService.saveLibraryBooks(library);
|
||||
},
|
||||
saveConfig: async (envConfig: EnvConfigType, book: Book, config: BookConfig) => {
|
||||
saveConfig: async (
|
||||
envConfig: EnvConfigType,
|
||||
book: Book,
|
||||
config: BookConfig,
|
||||
settings: SystemSettings,
|
||||
) => {
|
||||
const appService = await envConfig.getAppService();
|
||||
const { library } = get();
|
||||
const bookIndex = library.findIndex((b) => b.hash === book.hash);
|
||||
@@ -91,7 +122,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
}
|
||||
set({ library });
|
||||
config.lastUpdated = Date.now();
|
||||
appService.saveBookConfig(book, config);
|
||||
appService.saveBookConfig(book, config, settings);
|
||||
appService.saveLibraryBooks(library);
|
||||
},
|
||||
saveSettings: async (envConfig: EnvConfigType, settings: SystemSettings) => {
|
||||
@@ -99,6 +130,13 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
await appService.saveSettings(settings);
|
||||
},
|
||||
|
||||
closeBook: (key: string) => {
|
||||
set((state) => {
|
||||
const books = { ...state.books };
|
||||
delete books[key];
|
||||
return { books };
|
||||
});
|
||||
},
|
||||
initBookState: async (envConfig: EnvConfigType, id: string, key: string, isPrimary = true) => {
|
||||
set((state) => ({
|
||||
books: {
|
||||
@@ -109,12 +147,12 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
|
||||
try {
|
||||
const appService = await envConfig.getAppService();
|
||||
const library = await appService.loadLibraryBooks();
|
||||
const { library, settings } = get();
|
||||
const book = library.find((b) => b.hash === id);
|
||||
if (!book) {
|
||||
throw new Error('Book not found');
|
||||
}
|
||||
const content = (await appService.loadBookContent(book)) as BookContent;
|
||||
const content = (await appService.loadBookContent(book, settings)) as BookContent;
|
||||
const { file, config } = content;
|
||||
const { book: bookDoc } = await new DocumentLoader(file).open();
|
||||
|
||||
|
||||
@@ -29,6 +29,37 @@ export interface BookNote {
|
||||
removalTimestamp?: number;
|
||||
}
|
||||
|
||||
export interface BookLayout {
|
||||
gap: number;
|
||||
scrolled: boolean;
|
||||
maxColumnCount: number;
|
||||
maxInlineSize: number;
|
||||
maxBlockSize: number;
|
||||
animated: boolean;
|
||||
}
|
||||
|
||||
export interface BookStyle {
|
||||
lineHeight: number;
|
||||
justify: boolean;
|
||||
hyphenate: boolean;
|
||||
invert: boolean;
|
||||
theme: string;
|
||||
overrideFont: boolean;
|
||||
userStylesheet: string;
|
||||
}
|
||||
|
||||
export interface BookFont {
|
||||
serif: string;
|
||||
sansSerif: string;
|
||||
monospace: string;
|
||||
defaultFont: string;
|
||||
defaultSize: number;
|
||||
minimumSize: number;
|
||||
fontWeight: number;
|
||||
}
|
||||
|
||||
export interface ViewSettings extends BookLayout, BookStyle, BookFont {}
|
||||
|
||||
export interface BookConfig {
|
||||
lastUpdated: number;
|
||||
progress?: number;
|
||||
@@ -37,9 +68,12 @@ export interface BookConfig {
|
||||
chapter?: string;
|
||||
section?: PageInfo;
|
||||
pageinfo?: PageInfo;
|
||||
|
||||
bookmarks?: BookNote[];
|
||||
annotations?: BookNote[];
|
||||
removedNotesTimestamps?: Record<string, number>;
|
||||
|
||||
viewSettings?: Partial<ViewSettings>;
|
||||
}
|
||||
|
||||
export interface BooksGroup {
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { ViewSettings } from './book';
|
||||
|
||||
export type ThemeType = 'light' | 'dark' | 'auto';
|
||||
|
||||
export interface ReadSettings {
|
||||
themeType: ThemeType;
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
wordSpacing: number;
|
||||
lineSpacing: number;
|
||||
|
||||
sideBarWidth: string;
|
||||
isSideBarPinned: boolean;
|
||||
autohideCursor: boolean;
|
||||
}
|
||||
|
||||
export interface SystemSettings {
|
||||
version: number;
|
||||
localBooksDir: string;
|
||||
|
||||
globalReadSettings: ReadSettings;
|
||||
globalViewSettings: ViewSettings;
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ export interface AppService {
|
||||
saveSettings(settings: SystemSettings): Promise<void>;
|
||||
importBook(file: string | File, books: Book[], overwrite?: boolean): Promise<Book[]>;
|
||||
deleteBook(book: Book): Promise<void>;
|
||||
loadBookConfig(book: Book): Promise<BookConfig>;
|
||||
saveBookConfig(book: Book, config: BookConfig): Promise<void>;
|
||||
loadBookContent(book: Book): Promise<BookContent>;
|
||||
loadBookConfig(book: Book, settings: SystemSettings): Promise<BookConfig>;
|
||||
saveBookConfig(book: Book, config: BookConfig, settings?: SystemSettings): Promise<void>;
|
||||
loadBookContent(book: Book, settings: SystemSettings): Promise<BookContent>;
|
||||
loadLibraryBooks(): Promise<Book[]>;
|
||||
saveLibraryBooks(books: Book[]): Promise<void>;
|
||||
getCoverImageUrl(book: Book): string;
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: ac36959a03...98e891d1b4
Reference in New Issue
Block a user