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
+176 -175
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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",
+6 -4
View File
@@ -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'>
+2 -5
View File
@@ -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>
+28 -3
View File
@@ -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) {
+42 -7
View File
@@ -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));
}
+32 -7
View File
@@ -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;
+44 -6
View File
@@ -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();
+34
View File
@@ -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 {
+5 -6
View File
@@ -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;
}
+3 -3
View File
@@ -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;