diff --git a/apps/readest-app/next.config.mjs b/apps/readest-app/next.config.mjs index 292a05b5..ad149364 100644 --- a/apps/readest-app/next.config.mjs +++ b/apps/readest-app/next.config.mjs @@ -14,7 +14,7 @@ const nextConfig = { unoptimized: true, }, // Configure assetPrefix or else the server won't properly resolve your assets. - assetPrefix: isProd ? null : `http://${internalHost}:3000`, + assetPrefix: isProd ? '' : `http://${internalHost}:3000`, reactStrictMode: true, }; diff --git a/apps/readest-app/src-tauri/tauri.conf.json b/apps/readest-app/src-tauri/tauri.conf.json index ba898c29..3689ab3f 100644 --- a/apps/readest-app/src-tauri/tauri.conf.json +++ b/apps/readest-app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Readest", - "version": "0.3.5", + "version": "0.4.2", "identifier": "com.bilingify.digest", "build": { "frontendDist": "../out", diff --git a/apps/readest-app/src/app/globals.css b/apps/readest-app/src/app/globals.css index 4ee675dc..625bcf1b 100644 --- a/apps/readest-app/src/app/globals.css +++ b/apps/readest-app/src/app/globals.css @@ -107,7 +107,7 @@ foliate-view { .dropdown-left::before, .dropdown-left::after { - left: 16px; + left: 20px; transform: translateX(0); } @@ -119,7 +119,7 @@ foliate-view { .dropdown-right::before, .dropdown-right::after { - right: 16px; + right: 20px; transform: translateX(0); } @@ -128,7 +128,7 @@ foliate-view { display: none; } -.modal, .dropdown-content { +.dropdown-content, .settings-content { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: 14px; font-weight: 400; diff --git a/apps/readest-app/src/app/reader/components/BookGrid.tsx b/apps/readest-app/src/app/reader/components/BookGrid.tsx index 8f180c2b..881e0c9c 100644 --- a/apps/readest-app/src/app/reader/components/BookGrid.tsx +++ b/apps/readest-app/src/app/reader/components/BookGrid.tsx @@ -35,7 +35,7 @@ const BookGrid: React.FC = ({ bookKeys, bookStates, onCloseBook } const { book, config, bookDoc } = bookState; if (!book || !config || !bookDoc) return null; const { section, pageinfo, progress, chapter } = config; - const marginGap = config.viewSettings ? `${config.viewSettings!.gap! * 100}%` : ''; + const marginGap = config.viewSettings ? `${config.viewSettings!.gapPercent!}%` : ''; return (
diff --git a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx index 234e7a45..2b563053 100644 --- a/apps/readest-app/src/app/reader/components/FoliateViewer.tsx +++ b/apps/readest-app/src/app/reader/components/FoliateViewer.tsx @@ -3,45 +3,7 @@ import { useFoliateEvents } from '../hooks/useFoliateEvents'; import { BookDoc } from '@/libs/document'; import { BookConfig } from '@/types/book'; import { useReaderStore } from '@/store/readerStore'; - -const getCSS = (spacing: number, justify: boolean, hyphenate: boolean) => ` - @namespace epub "http://www.idpf.org/2007/ops"; - html { - color-scheme: light dark; - } - /* https://github.com/whatwg/html/issues/5426 */ - @media (prefers-color-scheme: dark) { - a:link { - color: lightblue; - } - } - p, li, blockquote, dd { - line-height: ${spacing}; - text-align: ${justify ? 'justify' : 'start'}; - -webkit-hyphens: ${hyphenate ? 'auto' : 'manual'}; - hyphens: ${hyphenate ? 'auto' : 'manual'}; - -webkit-hyphenate-limit-before: 3; - -webkit-hyphenate-limit-after: 2; - -webkit-hyphenate-limit-lines: 2; - hanging-punctuation: allow-end last; - widows: 2; - } - /* prevent the above from overriding the align attribute */ - [align="left"] { text-align: left; } - [align="right"] { text-align: right; } - [align="center"] { text-align: center; } - [align="justify"] { text-align: justify; } - - pre { - white-space: pre-wrap !important; - } - aside[epub|type~="endnote"], - aside[epub|type~="footnote"], - aside[epub|type~="note"], - aside[epub|type~="rearnote"] { - display: none; - } -`; +import { getStyles } from '@/utils/style'; export interface FoliateView extends HTMLElement { open: (book: BookDoc) => Promise; @@ -52,8 +14,9 @@ export interface FoliateView extends HTMLElement { goLeft: () => void; goRight: () => void; renderer: { - setStyles: (css: string) => void; + setStyles?: (css: string) => void; setAttribute: (name: string, value: string | number) => void; + removeAttribute: (name: string) => void; next: () => Promise; prev: () => Promise; }; @@ -141,22 +104,27 @@ const FoliateViewer: React.FC<{ await view.open(bookDoc); const viewSettings = bookConfig.viewSettings!; - if ('setStyles' in view.renderer) { - const lineHeight = viewSettings.lineHeight!; - const justify = viewSettings.justify!; - const hyphenate = viewSettings.hyphenate!; - view.renderer.setStyles(getCSS(lineHeight, justify, hyphenate)); - // FIXME: zoom level is not working in paginated mode - if (viewSettings.scrolled) { - const zoomLevel = viewSettings.zoomLevel!; - view.renderer.setStyles(`body { zoom: ${zoomLevel}%; }`); - } - } + view.renderer.setStyles?.(getStyles(bookConfig)); const isScrolled = viewSettings.scrolled!; - const gap = viewSettings.gap!; + const marginPx = viewSettings.marginPx!; + const gapPercent = viewSettings.gapPercent!; + const animated = viewSettings.animated!; + const maxColumnCount = viewSettings.maxColumnCount!; + const maxInlineSize = viewSettings.maxInlineSize!; + const maxBlockSize = viewSettings.maxBlockSize!; + if (animated) { + view.renderer.setAttribute('animated', ''); + } else { + view.renderer.removeAttribute('animated'); + } + view.renderer.setAttribute('animated', animated ? 'animated' : ''); view.renderer.setAttribute('flow', isScrolled ? 'scrolled' : 'paginated'); - view.renderer.setAttribute('margin', '44px'); - view.renderer.setAttribute('gap', `${gap * 100}%`); + view.renderer.setAttribute('margin', `${marginPx}px`); + view.renderer.setAttribute('gap', `${gapPercent}%`); + view.renderer.setAttribute('max-column-count', maxColumnCount); + view.renderer.setAttribute('max-inline-size', `${maxInlineSize}px`); + view.renderer.setAttribute('max-block-size', `${maxBlockSize}px`); + const lastLocation = bookConfig.location; if (lastLocation) { view.init({ lastLocation }); diff --git a/apps/readest-app/src/app/reader/components/HeaderBar.tsx b/apps/readest-app/src/app/reader/components/HeaderBar.tsx index 74a7b189..fc95d518 100644 --- a/apps/readest-app/src/app/reader/components/HeaderBar.tsx +++ b/apps/readest-app/src/app/reader/components/HeaderBar.tsx @@ -37,6 +37,11 @@ const HeaderBar: React.FC = ({ } }; + const handleToggleDropdown = (isOpen: boolean) => { + setIsDropdownOpen(isOpen); + if (!isOpen) setHoveredBookKey(''); + }; + return (
= ({ className='dropdown-bottom dropdown-end' buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0' toggleButton={} - onToggle={setIsDropdownOpen} + onToggle={handleToggleDropdown} > diff --git a/apps/readest-app/src/app/reader/components/ViewMenu.tsx b/apps/readest-app/src/app/reader/components/ViewMenu.tsx index 6ac4b8f8..eb2351a3 100644 --- a/apps/readest-app/src/app/reader/components/ViewMenu.tsx +++ b/apps/readest-app/src/app/reader/components/ViewMenu.tsx @@ -5,16 +5,17 @@ import { BiMoon, BiSun } from 'react-icons/bi'; import { MdZoomOut, MdZoomIn, MdCheck } from 'react-icons/md'; import { useReaderStore } from '@/store/readerStore'; +import { getStyles } from '@/utils/style'; interface ViewMenuProps { bookKey: string; - toggleDropdown?: () => void; + setIsDropdownOpen?: (open: boolean) => void; onSetSettingsDialogOpen: (open: boolean) => void; } const ViewMenu: React.FC = ({ bookKey, - toggleDropdown, + setIsDropdownOpen, onSetSettingsDialogOpen, }) => { const { books, setConfig, getFoliateView } = useReaderStore(); @@ -34,7 +35,7 @@ const ViewMenu: React.FC = ({ const toggleInvertedColors = () => setInvertedColors(!isInvertedColors); const openFontLayoutMenu = () => { - toggleDropdown?.(); + setIsDropdownOpen?.(false); onSetSettingsDialogOpen(true); }; @@ -52,13 +53,12 @@ const ViewMenu: React.FC = ({ useEffect(() => { const view = getFoliateView(bookKey); if (!view) return; - if ('setStyles' in view.renderer) { - // FIXME: zoom level is not working in paginated mode - if (!config.viewSettings?.scrolled) return; - view.renderer.setStyles(`body { zoom: ${zoomLevel}%; }`); - config.viewSettings!.zoomLevel = zoomLevel; - setConfig(bookKey, config); + // FIXME: zoom level is not working in paginated mode + if (config.viewSettings?.scrolled) { + view.renderer.setStyles?.(getStyles(config)); } + config.viewSettings!.zoomLevel = zoomLevel; + setConfig(bookKey, config); }, [zoomLevel]); return ( diff --git a/apps/readest-app/src/app/reader/components/settings/ColorPanel.tsx b/apps/readest-app/src/app/reader/components/settings/ColorPanel.tsx index 305a46b9..ce198392 100644 --- a/apps/readest-app/src/app/reader/components/settings/ColorPanel.tsx +++ b/apps/readest-app/src/app/reader/components/settings/ColorPanel.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -const ColorPanel: React.FC = () => { +const ColorPanel: React.FC<{ bookKey: string }> = () => { const [selectedTheme, setSelectedTheme] = useState('Default'); const themes = [ 'Default', @@ -16,8 +16,7 @@ const ColorPanel: React.FC = () => { return (
-

Color Settings

- {/* Theme Selection */} +

Color Settings

{themes.map((theme) => ( +
} > @@ -61,10 +70,11 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({}) =
-
- {activePanel === 'Font' && } - {activePanel === 'Layout' && } - {activePanel === 'Color' && } +
+ {activePanel === 'Font' && } + {activePanel === 'Layout' && } + {activePanel === 'Color' && } + {activePanel === 'Misc' && }
diff --git a/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx b/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx index 70e09eec..bafc69d0 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/BookMenu.tsx @@ -2,20 +2,20 @@ import React from 'react'; interface BookMenuProps { openSplitView: () => void; - toggleDropdown?: () => void; + setIsDropdownOpen?: (isOpen: boolean) => void; } -const BookMenu: React.FC = ({ openSplitView, toggleDropdown }) => { +const BookMenu: React.FC = ({ openSplitView, setIsDropdownOpen }) => { const handleOpenSplitView = () => { openSplitView(); - toggleDropdown?.(); + setIsDropdownOpen?.(false); }; const handleReloadPage = () => { window.location.reload(); - toggleDropdown?.(); + setIsDropdownOpen?.(false); }; const showAboutReadest = () => { - toggleDropdown?.(); + setIsDropdownOpen?.(false); }; return ( diff --git a/apps/readest-app/src/components/Dropdown.tsx b/apps/readest-app/src/components/Dropdown.tsx index 279d7fcc..3496fd61 100644 --- a/apps/readest-app/src/components/Dropdown.tsx +++ b/apps/readest-app/src/components/Dropdown.tsx @@ -5,7 +5,7 @@ interface DropdownProps { className?: string; buttonClassName?: string; toggleButton: React.ReactNode; - children: ReactElement<{ toggleDropdown: () => void }>; + children: ReactElement<{ setIsDropdownOpen: (isOpen: boolean) => void }>; onToggle?: (isOpen: boolean) => void; } @@ -25,25 +25,25 @@ const Dropdown: React.FC = ({ onToggle?.(newIsOpen); }; - const closeDropdown = () => { - setIsOpen(false); - onToggle?.(false); + const setIsDropdownOpen = (isOpen: boolean) => { + setIsOpen(isOpen); + onToggle?.(isOpen); }; const handleBlur = (event: React.FocusEvent) => { if (!dropdownRef.current?.contains(event.relatedTarget as Node)) { - closeDropdown(); + setIsDropdownOpen(false); } }; const handleClickOutside = (event: MouseEvent | Event) => { if (event instanceof MouseEvent) { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - closeDropdown(); + setIsDropdownOpen(false); } } else if (event instanceof MessageEvent) { if (event.data && event.data.type === 'iframe-mousedown') { - closeDropdown(); + setIsDropdownOpen(false); } } }; @@ -58,7 +58,7 @@ const Dropdown: React.FC = ({ }, []); const childrenWithToggle = isValidElement(children) - ? React.cloneElement(children, { toggleDropdown }) + ? React.cloneElement(children, { setIsDropdownOpen }) : children; return ( diff --git a/apps/readest-app/src/services/constants.ts b/apps/readest-app/src/services/constants.ts index d4022477..031a428a 100644 --- a/apps/readest-app/src/services/constants.ts +++ b/apps/readest-app/src/services/constants.ts @@ -11,17 +11,18 @@ export const DEFAULT_READSETTINGS: ReadSettings = { }; export const DEFAULT_BOOK_FONT: BookFont = { - serif: 'Serif 12', - sansSerif: 'Sans 12', - monospace: 'Monospace 12', - defaultFont: 'serif', - defaultSize: 16, - minimumSize: 8, + serifFont: 'Literata', + sansSerifFont: 'Roboto', + monospaceFont: 'Consolas', + defaultFont: 'Serif', + defaultFontSize: 16, + minimumFontSize: 8, fontWeight: 400, }; export const DEFAULT_BOOK_LAYOUT: BookLayout = { - gap: 0.05, + marginPx: 44, + gapPercent: 5, scrolled: false, maxColumnCount: 2, maxInlineSize: 720, @@ -32,8 +33,8 @@ export const DEFAULT_BOOK_LAYOUT: BookLayout = { export const DEFAULT_BOOK_STYLE: BookStyle = { zoomLevel: 100, lineHeight: 1.6, - justify: true, - hyphenate: true, + fullJustification: true, + hyphenation: true, invert: false, theme: 'light', overrideFont: false, @@ -41,3 +42,24 @@ export const DEFAULT_BOOK_STYLE: BookStyle = { }; export const SYSTEM_SETTINGS_VERSION = 1; + +export const SERIF_FONTS = [ + 'Literata', + 'Vollkorn', + 'Aleo', + 'Crimson Text', + 'Merriweather', + 'Georgia', + 'Times New Roman', +]; + +export const SANS_SERIF_FONTS = [ + 'Roboto', + 'Open Sans', + 'Noto Sans', + 'Poppins', + 'Helvetica', + 'Arial', +]; + +export const MONOSPACE_FONTS = ['Fira Code', 'Lucida Console', 'Consolas', 'Courier New']; diff --git a/apps/readest-app/src/types/book.ts b/apps/readest-app/src/types/book.ts index bb7b5e74..6b383856 100644 --- a/apps/readest-app/src/types/book.ts +++ b/apps/readest-app/src/types/book.ts @@ -30,7 +30,8 @@ export interface BookNote { } export interface BookLayout { - gap: number; + marginPx: number; + gapPercent: number; scrolled: boolean; maxColumnCount: number; maxInlineSize: number; @@ -41,8 +42,8 @@ export interface BookLayout { export interface BookStyle { zoomLevel: number; lineHeight: number; - justify: boolean; - hyphenate: boolean; + fullJustification: boolean; + hyphenation: boolean; invert: boolean; theme: string; overrideFont: boolean; @@ -50,12 +51,12 @@ export interface BookStyle { } export interface BookFont { - serif: string; - sansSerif: string; - monospace: string; + serifFont: string; + sansSerifFont: string; + monospaceFont: string; defaultFont: string; - defaultSize: number; - minimumSize: number; + defaultFontSize: number; + minimumFontSize: number; fontWeight: number; } diff --git a/apps/readest-app/src/utils/style.ts b/apps/readest-app/src/utils/style.ts new file mode 100644 index 00000000..e14976e9 --- /dev/null +++ b/apps/readest-app/src/utils/style.ts @@ -0,0 +1,96 @@ +import { MONOSPACE_FONTS, SANS_SERIF_FONTS, SERIF_FONTS } from '@/services/constants'; +import { BookConfig } from '@/types/book'; + +const getFontStyles = ( + serif: string, + sansSerif: string, + monospace: string, + defaultFont: string, + fontSize: number, + overrideFont: boolean, +) => { + const serifFonts = [serif, ...SERIF_FONTS.filter((font) => font !== serif)]; + const sansSerifFonts = [sansSerif, ...SANS_SERIF_FONTS.filter((font) => font !== sansSerif)]; + const monospaceFonts = [monospace, ...MONOSPACE_FONTS.filter((font) => font !== monospace)]; + const fontStyles = ` + html { + --serif: ${serifFonts.map((font) => `"${font}"`).join(', ')}, serif; + --sans-serif: ${sansSerifFonts.map((font) => `"${font}"`).join(', ')}, sans-serif; + --monospace: ${monospaceFonts.map((font) => `"${font}"`).join(', ')}, monospace; + } + body * { + font-size: ${fontSize}px ${overrideFont ? '!important' : ''}; + font-family: revert ${overrideFont ? '!important' : ''}; + font-family: var(${defaultFont.toLowerCase() === 'serif' ? '--serif' : '--sans-serif'}) ${overrideFont ? '!important' : ''}; + } + `; + return fontStyles; +}; + +const getLayoutStyles = ( + spacing: number, + justify: boolean, + hyphenate: boolean, + zoomLevel: number, +) => ` + @namespace epub "http://www.idpf.org/2007/ops"; + html { + color-scheme: light dark; + } + /* https://github.com/whatwg/html/issues/5426 */ + @media (prefers-color-scheme: dark) { + a:link { + color: lightblue; + } + } + body { + zoom: ${zoomLevel}%; + } + p, li, blockquote, dd { + line-height: ${spacing}; + text-align: ${justify ? 'justify' : 'start'}; + -webkit-hyphens: ${hyphenate ? 'auto' : 'manual'}; + hyphens: ${hyphenate ? 'auto' : 'manual'}; + -webkit-hyphenate-limit-before: 3; + -webkit-hyphenate-limit-after: 2; + -webkit-hyphenate-limit-lines: 2; + hanging-punctuation: allow-end last; + widows: 2; + } + /* prevent the above from overriding the align attribute */ + [align="left"] { text-align: left; } + [align="right"] { text-align: right; } + [align="center"] { text-align: center; } + [align="justify"] { text-align: justify; } + + pre { + white-space: pre-wrap !important; + } + aside[epub|type~="endnote"], + aside[epub|type~="footnote"], + aside[epub|type~="note"], + aside[epub|type~="rearnote"] { + display: none; + } +`; + +export const getStyles = (config: BookConfig) => { + const viewSettings = config.viewSettings!; + const layoutStyles = getLayoutStyles( + viewSettings.lineHeight!, + viewSettings.fullJustification!, + viewSettings.hyphenation!, + // FIXME: zoom level is not working in paginated mode + viewSettings.scrolled ? viewSettings.zoomLevel! : 100, + ); + const fontStyles = getFontStyles( + viewSettings.serifFont!, + viewSettings.sansSerifFont!, + viewSettings.monospaceFont!, + viewSettings.defaultFont!, + viewSettings.defaultFontSize!, + viewSettings.overrideFont!, + ); + const userStylesheet = viewSettings.userStylesheet!; + return `${layoutStyles}\n${fontStyles}\n${userStylesheet}`; +};