From bac1c6648be9eacda8fd8c583ed2ac61d1e6385b Mon Sep 17 00:00:00 2001 From: chrox Date: Fri, 1 Nov 2024 20:49:53 +0100 Subject: [PATCH] Add font and layout settings UI --- apps/readest-app/src/app/globals.css | 29 +++- .../app/library/components/LibraryHeader.tsx | 10 +- .../src/app/reader/components/BookGrid.tsx | 4 + .../src/app/reader/components/HeaderBar.tsx | 6 +- .../src/app/reader/components/ViewMenu.tsx | 10 +- .../reader/components/settings/ColorPanel.tsx | 36 ++++ .../reader/components/settings/DialogMenu.tsx | 39 +++++ .../components/settings/FontDropDown.tsx | 48 ++++++ .../reader/components/settings/FontPanel.tsx | 155 ++++++++++++++++++ .../components/settings/LayoutPanel.tsx | 92 +++++++++++ .../components/settings/NumberInput.tsx | 90 ++++++++++ .../components/settings/SettingsDialog.tsx | 74 +++++++++ apps/readest-app/src/components/Dropdown.tsx | 2 +- .../src/components/WindowButtons.tsx | 4 +- apps/readest-app/src/services/constants.ts | 2 +- apps/readest-app/src/store/readerStore.ts | 10 ++ 16 files changed, 592 insertions(+), 19 deletions(-) create mode 100644 apps/readest-app/src/app/reader/components/settings/ColorPanel.tsx create mode 100644 apps/readest-app/src/app/reader/components/settings/DialogMenu.tsx create mode 100644 apps/readest-app/src/app/reader/components/settings/FontDropDown.tsx create mode 100644 apps/readest-app/src/app/reader/components/settings/FontPanel.tsx create mode 100644 apps/readest-app/src/app/reader/components/settings/LayoutPanel.tsx create mode 100644 apps/readest-app/src/app/reader/components/settings/NumberInput.tsx create mode 100644 apps/readest-app/src/app/reader/components/settings/SettingsDialog.tsx diff --git a/apps/readest-app/src/app/globals.css b/apps/readest-app/src/app/globals.css index 3edbb262..4ee675dc 100644 --- a/apps/readest-app/src/app/globals.css +++ b/apps/readest-app/src/app/globals.css @@ -81,10 +81,6 @@ foliate-view { padding: 10px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); border-radius: 14px; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - font-size: 14px; - font-weight: 300; - @apply text-gray-700; } .dropdown-content::before, @@ -111,7 +107,7 @@ foliate-view { .dropdown-left::before, .dropdown-left::after { - left: 20px; + left: 16px; transform: translateX(0); } @@ -123,7 +119,7 @@ foliate-view { .dropdown-right::before, .dropdown-right::after { - right: 20px; + right: 16px; transform: translateX(0); } @@ -131,3 +127,24 @@ foliate-view { .dropdown-content.no-triangle::after { display: none; } + +.modal, .dropdown-content { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 14px; + font-weight: 400; + @apply text-gray-700; +} + +.config-item { + @apply flex h-14 items-center justify-between p-4; + @apply hover:bg-gray-50; +} + +.config-item-top { + @apply rounded-t-2xl; +} + +.config-item-bottom { + @apply rounded-b-2xl; +} + diff --git a/apps/readest-app/src/app/library/components/LibraryHeader.tsx b/apps/readest-app/src/app/library/components/LibraryHeader.tsx index b9b163ee..e6df47e0 100644 --- a/apps/readest-app/src/app/library/components/LibraryHeader.tsx +++ b/apps/readest-app/src/app/library/components/LibraryHeader.tsx @@ -58,19 +58,19 @@ const LibraryHeader: React.FC = ({ />
- +
- +
- +
-
+
} onToggle={setIsDropdownOpen} > - + void; + onSetSettingsDialogOpen: (open: boolean) => void; } -const ViewMenu: React.FC = ({ bookKey }) => { +const ViewMenu: React.FC = ({ + bookKey, + toggleDropdown, + onSetSettingsDialogOpen, +}) => { const { books, setConfig, getFoliateView } = useReaderStore(); const bookState = books[bookKey]!; const config = bookState.config!; @@ -29,7 +34,8 @@ const ViewMenu: React.FC = ({ bookKey }) => { const toggleInvertedColors = () => setInvertedColors(!isInvertedColors); const openFontLayoutMenu = () => { - // open font layout menu + toggleDropdown?.(); + onSetSettingsDialogOpen(true); }; useEffect(() => { diff --git a/apps/readest-app/src/app/reader/components/settings/ColorPanel.tsx b/apps/readest-app/src/app/reader/components/settings/ColorPanel.tsx new file mode 100644 index 00000000..305a46b9 --- /dev/null +++ b/apps/readest-app/src/app/reader/components/settings/ColorPanel.tsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; + +const ColorPanel: React.FC = () => { + const [selectedTheme, setSelectedTheme] = useState('Default'); + const themes = [ + 'Default', + 'Gray', + 'Sepia', + 'Grass', + 'Cherry', + 'Sky', + 'Nord', + 'Solarized', + 'Gruvbox', + ]; + + return ( +
+

Color Settings

+ {/* Theme Selection */} +
+ {themes.map((theme) => ( + + ))} +
+
+ ); +}; + +export default ColorPanel; diff --git a/apps/readest-app/src/app/reader/components/settings/DialogMenu.tsx b/apps/readest-app/src/app/reader/components/settings/DialogMenu.tsx new file mode 100644 index 00000000..2e43846b --- /dev/null +++ b/apps/readest-app/src/app/reader/components/settings/DialogMenu.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { useReaderStore } from '@/store/readerStore'; +import { MdCheck } from 'react-icons/md'; + +interface DialogMenuProps { + toggleDropdown?: () => void; +} + +const DialogMenu: React.FC = ({ toggleDropdown }) => { + const { isFontLayoutSettingsGlobal, setFontLayoutSettingsGlobal } = useReaderStore(); + + const handleToggleGlobal = () => { + setFontLayoutSettingsGlobal(!isFontLayoutSettingsGlobal); + toggleDropdown?.(); + }; + + return ( +
+ +
+ ); +}; + +export default DialogMenu; diff --git a/apps/readest-app/src/app/reader/components/settings/FontDropDown.tsx b/apps/readest-app/src/app/reader/components/settings/FontDropDown.tsx new file mode 100644 index 00000000..bc393e09 --- /dev/null +++ b/apps/readest-app/src/app/reader/components/settings/FontDropDown.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { FiChevronDown } from 'react-icons/fi'; +import { MdCheck } from 'react-icons/md'; + +interface DropdownProps { + family?: string; + selected: string; + options: string[]; + onSelect: (option: string) => void; + onGetFontFamily: (option: string, family: string) => string; +} + +const FontDropdown: React.FC = ({ + family, + selected, + options, + onSelect, + onGetFontFamily, +}) => { + return ( +
+ +
    + {options.map((option) => ( +
  • onSelect(option)}> +
    + + {selected === option && } + + {option} +
    +
  • + ))} +
+
+ ); +}; + +export default FontDropdown; diff --git a/apps/readest-app/src/app/reader/components/settings/FontPanel.tsx b/apps/readest-app/src/app/reader/components/settings/FontPanel.tsx new file mode 100644 index 00000000..7ad3cdae --- /dev/null +++ b/apps/readest-app/src/app/reader/components/settings/FontPanel.tsx @@ -0,0 +1,155 @@ +import clsx from 'clsx'; +import React, { useState } from 'react'; + +import NumberInput from './NumberInput'; +import FontDropdown from './FontDropDown'; + +interface FontFaceProps { + className?: string; + family: string; + label: string; + options: string[]; + selected: string; + onSelect: (option: string) => void; +} + +const fontFamilyOptions = ['Serif', 'Sans-serif']; +const serifFonts = [ + 'Literata', + 'Vollkorn', + 'Aleo', + 'Crimson Text', + 'Merriweather', + 'Georgia', + 'Times New Roman', +]; +const sansSerifFonts = ['Roboto', 'Open Sans', 'Noto Sans', 'Poppins', 'Helvetica', 'Arial']; +const monospaceFonts = ['Fira Code', 'Lucida Console', 'Consolas', 'Courier New']; + +const FontPanel: React.FC = () => { + const [defaultFontSize, setDefaultFontSize] = useState(18); + const [minFontSize, setMinFontSize] = useState(1); + const [overridePublisherFont, setOverridePublisherFont] = useState(true); + const [defaultFont, setDefaultFont] = useState('Serif'); + const [serifFont, setSerifFont] = useState('Georgia'); + const [sansSerifFont, setSansSerifFont] = useState('Roboto'); + const [monospaceFont, setMonospaceFont] = useState('Consolas'); + + const handleFontFamilyFont = (option: string) => { + switch (option) { + case 'Serif': + return `'${serifFont}', serif`; + case 'Sans-serif': + return `'${sansSerifFont}', sans-serif`; + case 'Monospace': + return `'${monospaceFont}', monospace`; + default: + return ''; + } + }; + + const handleFontFaceFont = (option: string, family: string) => { + return `'${option}', ${family}`; + }; + + const FontFace = ({ className, family, label, options, selected, onSelect }: FontFaceProps) => ( +
+ {label} + +
+ ); + + return ( +
+
+

Font Size

+
+
+ + +
+
+
+ +
+

Font Family

+
+
+
+ Default Font + +
+ +
+ Override Publisher Font + setOverridePublisherFont(!overridePublisherFont)} + /> +
+
+
+
+ +
+

Font Face

+
+
+ + + +
+
+
+
+ ); +}; + +export default FontPanel; diff --git a/apps/readest-app/src/app/reader/components/settings/LayoutPanel.tsx b/apps/readest-app/src/app/reader/components/settings/LayoutPanel.tsx new file mode 100644 index 00000000..82b32b18 --- /dev/null +++ b/apps/readest-app/src/app/reader/components/settings/LayoutPanel.tsx @@ -0,0 +1,92 @@ +import React, { useState } from 'react'; +import NumberInput from './NumberInput'; + +const LayoutPanel: React.FC = () => { + const [lineHeight, setLineHeight] = useState(1.5); + const [fullJustification, setFullJustification] = useState(true); + const [hyphenation, setHyphenation] = useState(true); + const [margins, setMargins] = useState(0.05); + const [maxNumberOfColumns, setMaxNumberOfColumns] = useState(2); + const [maxInlineSize, setMaxInlineSize] = useState(720); + const [maxBlockSize, setMaxBlockSize] = useState(1440); + + return ( +
+
+

Paragraph

+
+
+ +
+ Full Justification + setFullJustification(!fullJustification)} + /> +
+
+ Hyphenation + setHyphenation(!hyphenation)} + /> +
+
+
+
+ +
+

Page

+
+
+ + + + +
+
+
+
+ ); +}; + +export default LayoutPanel; diff --git a/apps/readest-app/src/app/reader/components/settings/NumberInput.tsx b/apps/readest-app/src/app/reader/components/settings/NumberInput.tsx new file mode 100644 index 00000000..2febd9bd --- /dev/null +++ b/apps/readest-app/src/app/reader/components/settings/NumberInput.tsx @@ -0,0 +1,90 @@ +import clsx from 'clsx'; +import React, { useState } from 'react'; +import { FiMinus, FiPlus } from 'react-icons/fi'; + +interface NumberInputProps { + className?: string; + label: string; + value: number; + min: number; + max: number; + step?: number; + onChange: (value: number) => void; +} + +const NumberInput: React.FC = ({ + className, + label, + value, + onChange, + min, + max, + step, +}) => { + const [localValue, setLocalValue] = useState(value); + const numberStep = step || 1; + + const handleInput = (e: React.FormEvent) => { + e.stopPropagation(); + e.nativeEvent.stopImmediatePropagation(); + }; + + const handleChange = (e: React.ChangeEvent) => { + const newValue = e.target.value === '' ? 0 : parseInt(e.target.value); + if (!isNaN(newValue)) { + setLocalValue(newValue); + onChange(Math.max(min, Math.min(max, newValue))); + } + }; + + const increment = () => { + const newValue = Math.min(max, localValue + numberStep); + setLocalValue(newValue); + onChange(newValue); + }; + + const decrement = () => { + const newValue = Math.max(min, localValue - numberStep); + setLocalValue(newValue); + onChange(newValue); + }; + + const handleOnBlur = () => { + const newValue = Math.max(min, Math.min(max, localValue)); + setLocalValue(newValue); + onChange(newValue); + }; + + return ( +
+ {label} +
+ e.target.select()} + /> + + +
+
+ ); +}; + +export default NumberInput; diff --git a/apps/readest-app/src/app/reader/components/settings/SettingsDialog.tsx b/apps/readest-app/src/app/reader/components/settings/SettingsDialog.tsx new file mode 100644 index 00000000..ad836920 --- /dev/null +++ b/apps/readest-app/src/app/reader/components/settings/SettingsDialog.tsx @@ -0,0 +1,74 @@ +import React, { useState } from 'react'; +import { BookConfig } from '@/types/book'; +import { useReaderStore } from '@/store/readerStore'; +import { RiFontSize } from 'react-icons/ri'; +import { RiDashboardLine } from 'react-icons/ri'; +import { VscSymbolColor } from 'react-icons/vsc'; +import { PiDotsThreeVerticalBold } from 'react-icons/pi'; + +import FontPanel from './FontPanel'; +import LayoutPanel from './LayoutPanel'; +import ColorPanel from './ColorPanel'; +import WindowButtons from '@/components/WindowButtons'; +import Dropdown from '@/components/Dropdown'; +import DialogMenu from './DialogMenu'; + +const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({}) => { + const [activePanel, setActivePanel] = useState('Font'); + const { setFontLayoutSettingsDialogOpen } = useReaderStore(); + + return ( + +
+
+
+ + + +
+
+ } + > + + + setFontLayoutSettingsDialogOpen(false)} + /> +
+
+ +
+ {activePanel === 'Font' && } + {activePanel === 'Layout' && } + {activePanel === 'Color' && } +
+
+
+ ); +}; + +export default SettingsDialog; diff --git a/apps/readest-app/src/components/Dropdown.tsx b/apps/readest-app/src/components/Dropdown.tsx index b289acc3..279d7fcc 100644 --- a/apps/readest-app/src/components/Dropdown.tsx +++ b/apps/readest-app/src/components/Dropdown.tsx @@ -37,7 +37,7 @@ const Dropdown: React.FC = ({ }; const handleClickOutside = (event: MouseEvent | Event) => { - if (event instanceof KeyboardEvent) { + if (event instanceof MouseEvent) { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { closeDropdown(); } diff --git a/apps/readest-app/src/components/WindowButtons.tsx b/apps/readest-app/src/components/WindowButtons.tsx index c947cdd7..215e202b 100644 --- a/apps/readest-app/src/components/WindowButtons.tsx +++ b/apps/readest-app/src/components/WindowButtons.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useRef } from 'react'; interface WindowButtonsProps { className?: string; - headerRef: React.RefObject; + headerRef?: React.RefObject; showMinimize?: boolean; showMaximize?: boolean; showClose?: boolean; @@ -41,7 +41,7 @@ const WindowButtons: React.FC = ({ }; useEffect(() => { - const headerElement = headerRef.current; + const headerElement = headerRef?.current; headerElement?.addEventListener('mousedown', handleMouseDown); return () => { diff --git a/apps/readest-app/src/services/constants.ts b/apps/readest-app/src/services/constants.ts index de0f4f36..d4022477 100644 --- a/apps/readest-app/src/services/constants.ts +++ b/apps/readest-app/src/services/constants.ts @@ -31,7 +31,7 @@ export const DEFAULT_BOOK_LAYOUT: BookLayout = { export const DEFAULT_BOOK_STYLE: BookStyle = { zoomLevel: 100, - lineHeight: 1.5, + lineHeight: 1.6, justify: true, hyphenate: true, invert: false, diff --git a/apps/readest-app/src/store/readerStore.ts b/apps/readest-app/src/store/readerStore.ts index fe563a01..b32ddea7 100644 --- a/apps/readest-app/src/store/readerStore.ts +++ b/apps/readest-app/src/store/readerStore.ts @@ -39,6 +39,11 @@ interface ReaderStore { setSideBarVisibility: (visible: boolean) => void; setSideBarPin: (pinned: boolean) => void; + isFontLayoutSettingsDialogOpen: boolean; + isFontLayoutSettingsGlobal: boolean; + setFontLayoutSettingsDialogOpen: (open: boolean) => void; + setFontLayoutSettingsGlobal: (global: boolean) => void; + setLibrary: (books: Book[]) => void; setSettings: (settings: SystemSettings) => void; setProgress: ( @@ -101,6 +106,11 @@ export const useReaderStore = create((set, get) => ({ setSideBarVisibility: (visible: boolean) => set({ isSideBarVisible: visible }), setSideBarPin: (pinned: boolean) => set({ isSideBarPinned: pinned }), + isFontLayoutSettingsDialogOpen: false, + isFontLayoutSettingsGlobal: true, + setFontLayoutSettingsDialogOpen: (open: boolean) => set({ isFontLayoutSettingsDialogOpen: open }), + setFontLayoutSettingsGlobal: (global: boolean) => set({ isFontLayoutSettingsGlobal: global }), + setLibrary: (books: Book[]) => set({ library: books }), setSettings: (settings: SystemSettings) => set({ settings }), setConfig: (key: string, config: BookConfig) => {