From ef864a32c2c8afa10d613f5280b9f07adb270ea7 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sun, 23 Mar 2025 15:56:57 +0800 Subject: [PATCH] feat: action bar at the bottom on mobile devices (#681) --- .../public/locales/ar/translation.json | 4 +- .../public/locales/de/translation.json | 4 +- .../public/locales/el/translation.json | 4 +- .../public/locales/es/translation.json | 4 +- .../public/locales/fr/translation.json | 4 +- .../public/locales/hi/translation.json | 4 +- .../public/locales/id/translation.json | 4 +- .../public/locales/it/translation.json | 4 +- .../public/locales/ja/translation.json | 4 +- .../public/locales/ko/translation.json | 4 +- .../public/locales/pl/translation.json | 4 +- .../public/locales/pt/translation.json | 4 +- .../public/locales/ru/translation.json | 4 +- .../public/locales/tr/translation.json | 4 +- .../public/locales/uk/translation.json | 4 +- .../public/locales/vi/translation.json | 4 +- .../public/locales/zh-CN/translation.json | 4 +- .../public/locales/zh-TW/translation.json | 4 +- .../src/app/reader/components/FooterBar.tsx | 264 +++++++++++++++--- .../src/app/reader/components/HeaderBar.tsx | 6 +- .../src/app/reader/components/PageInfo.tsx | 7 +- .../reader/components/annotator/Annotator.tsx | 2 +- .../components/annotator/WikipediaPopup.tsx | 2 +- .../components/annotator/WiktionaryPopup.tsx | 2 +- .../app/reader/components/sidebar/Header.tsx | 5 +- .../app/reader/components/sidebar/TOCView.tsx | 2 +- .../components/sidebar/TabNavigation.tsx | 12 +- apps/readest-app/src/app/user/page.tsx | 6 +- apps/readest-app/src/components/Slider.tsx | 93 ++++++ apps/readest-app/src/utils/style.ts | 4 +- 30 files changed, 402 insertions(+), 75 deletions(-) create mode 100644 apps/readest-app/src/components/Slider.tsx diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 247218da..27c8abce 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -247,5 +247,7 @@ "Border Color": "لون الحدود", "Border Frame": "إطار الحدود", "Show Header": "عرض الرأس", - "Show Footer": "عرض التذييل" + "Show Footer": "عرض التذييل", + "Small": "صغير", + "Large": "كبير" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index ae358774..0b73cce5 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -247,5 +247,7 @@ "Border Color": "Randfarbe", "Border Frame": "Rahmenrand", "Show Header": "Kopfzeile anzeigen", - "Show Footer": "Fußzeile anzeigen" + "Show Footer": "Fußzeile anzeigen", + "Small": "Klein", + "Large": "Groß" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 6c67d949..2405a8c8 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -247,5 +247,7 @@ "Border Color": "Χρώμα περιγράμματος", "Border Frame": "Πλαίσιο περιγράμματος", "Show Header": "Εμφάνιση κεφαλίδας", - "Show Footer": "Εμφάνιση υποσέλιδου" + "Show Footer": "Εμφάνιση υποσέλιδου", + "Small": "Μικρό", + "Large": "Μεγάλο" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index 02f5bda5..606a7b37 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -247,5 +247,7 @@ "Border Color": "Color del borde", "Border Frame": "Marco de borde", "Show Header": "Mostrar encabezado", - "Show Footer": "Mostrar pie de página" + "Show Footer": "Mostrar pie de página", + "Small": "Pequeño", + "Large": "Grande" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 39f0146f..c5ef1f10 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -247,5 +247,7 @@ "Border Color": "Couleur de la bordure", "Border Frame": "Cadre de bordure", "Show Header": "Afficher l'en-tête", - "Show Footer": "Afficher le pied de page" + "Show Footer": "Afficher le pied de page", + "Small": "Petit", + "Large": "Grand" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 1b95e672..31e590bd 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -247,5 +247,7 @@ "Border Color": "बॉर्डर रंग", "Border Frame": "बॉर्डर फ्रेम", "Show Header": "हेडर दिखाएं", - "Show Footer": "फ़ुटर दिखाएं" + "Show Footer": "फ़ुटर दिखाएं", + "Small": "छोटा", + "Large": "बड़ा" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 6707db12..10daa80b 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -247,5 +247,7 @@ "Border Color": "Warna Garis", "Border Frame": "Bingkai Garis", "Show Header": "Tampilkan Header", - "Show Footer": "Tampilkan Footer" + "Show Footer": "Tampilkan Footer", + "Small": "Kecil", + "Large": "Besar" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index a5b2c026..909d99fa 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -247,5 +247,7 @@ "Border Color": "Colore bordo", "Border Frame": "Cornice bordo", "Show Header": "Mostra intestazione", - "Show Footer": "Mostra piè di pagina" + "Show Footer": "Mostra piè di pagina", + "Small": "Piccolo", + "Large": "Grande" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index ee5d1f55..e0445bca 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -247,5 +247,7 @@ "Border Color": "ボーダーカラー", "Border Frame": "ボーダーフレーム", "Show Header": "ヘッダーを表示", - "Show Footer": "フッターを表示" + "Show Footer": "フッターを表示", + "Small": "小", + "Large": "大" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index 0c5f7d7f..7b828f3a 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -247,5 +247,7 @@ "Border Color": "테두리 색상", "Border Frame": "테두리 프레임", "Show Header": "헤더 표시", - "Show Footer": "푸터 표시" + "Show Footer": "푸터 표시", + "Small": "작음", + "Large": "큼" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 51daba94..0d485f13 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -247,5 +247,7 @@ "Border Color": "Kolor ramki", "Border Frame": "Ramka", "Show Header": "Pokaż nagłówek", - "Show Footer": "Pokaż stopkę" + "Show Footer": "Pokaż stopkę", + "Small": "Mały", + "Large": "Duży" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index a0149691..7862ee3d 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -247,5 +247,7 @@ "Border Color": "Cor da Borda", "Border Frame": "Moldura da Borda", "Show Header": "Mostrar Cabeçalho", - "Show Footer": "Mostrar Rodapé" + "Show Footer": "Mostrar Rodapé", + "Small": "Pequeno", + "Large": "Grande" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 8627e5b8..9009e52d 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -247,5 +247,7 @@ "Border Color": "Цвет границы", "Border Frame": "Граница рамки", "Show Header": "Показать заголовок", - "Show Footer": "Показать нижний колонтитул" + "Show Footer": "Показать нижний колонтитул", + "Small": "Маленький", + "Large": "Большой" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 8ee96f9b..565253c5 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -247,5 +247,7 @@ "Border Color": "Sınır Rengi", "Border Frame": "Sınır Çerçevesi", "Show Header": "Başlık Göster", - "Show Footer": "Altbilgi Göster" + "Show Footer": "Altbilgi Göster", + "Small": "Küçük", + "Large": "Büyük" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index e4454804..7c3c3c13 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -247,5 +247,7 @@ "Border Color": "Колір рамки", "Border Frame": "Рамка рамки", "Show Header": "Показати заголовок", - "Show Footer": "Показати нижню частину" + "Show Footer": "Показати нижню частину", + "Small": "Малий", + "Large": "Великий" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index 447b88e5..e4a416ae 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -247,5 +247,7 @@ "Border Color": "Màu viền", "Border Frame": "Khung viền", "Show Header": "Hiển thị tiêu đề", - "Show Footer": "Hiển thị chân trang" + "Show Footer": "Hiển thị chân trang", + "Small": "Nhỏ", + "Large": "Lớn" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index 687cd969..56231b10 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -247,5 +247,7 @@ "Border Color": "版框颜色", "Border Frame": "页面版框", "Show Header": "显示页眉", - "Show Footer": "显示页脚" + "Show Footer": "显示页脚", + "Small": "小", + "Large": "大" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 1891449e..74275151 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -247,5 +247,7 @@ "Border Color": "邊框顏色", "Border Frame": "頁面邊框", "Show Header": "顯示頁眉", - "Show Footer": "顯示頁腳" + "Show Footer": "顯示頁腳", + "Small": "小", + "Large": "大" } diff --git a/apps/readest-app/src/app/reader/components/FooterBar.tsx b/apps/readest-app/src/app/reader/components/FooterBar.tsx index 2ae28591..d9d238d5 100644 --- a/apps/readest-app/src/app/reader/components/FooterBar.tsx +++ b/apps/readest-app/src/app/reader/components/FooterBar.tsx @@ -3,14 +3,25 @@ import clsx from 'clsx'; import { RiArrowLeftWideLine, RiArrowRightWideLine } from 'react-icons/ri'; import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri'; import { FaHeadphones } from 'react-icons/fa6'; +import { IoIosList as TOCIcon } from 'react-icons/io'; +import { PiNotePencil as NoteIcon } from 'react-icons/pi'; +import { RxSlider as SliderIcon } from 'react-icons/rx'; +import { RiFontFamily as FontIcon } from 'react-icons/ri'; +import { MdOutlineHeadphones as TTSIcon } from 'react-icons/md'; +import { TbBoxMargin } from 'react-icons/tb'; +import { RxLineHeight } from 'react-icons/rx'; import { useEnv } from '@/context/EnvContext'; import { useReaderStore } from '@/store/readerStore'; import { useSidebarStore } from '@/store/sidebarStore'; import { useTranslation } from '@/hooks/useTranslation'; +import { useResponsiveSize } from '@/hooks/useResponsiveSize'; import { eventDispatcher } from '@/utils/event'; +import { saveViewSettings } from '../utils/viewSettingsHelper'; import { PageInfo } from '@/types/book'; +import { getStyles } from '@/utils/style'; import Button from '@/components/Button'; +import Slider from '@/components/Slider'; interface FooterBarProps { bookKey: string; @@ -28,17 +39,44 @@ const FooterBar: React.FC = ({ isHoveredAnim, }) => { const _ = useTranslation(); - const { appService } = useEnv(); + const { envConfig, appService } = useEnv(); const { hoveredBookKey, setHoveredBookKey, getView, getProgress, getViewSettings } = useReaderStore(); - const { isSideBarVisible } = useSidebarStore(); + const { isSideBarVisible, setSideBarVisible } = useSidebarStore(); + const [actionTab, setActionTab] = React.useState(''); + const sliderHeight = useResponsiveSize(28); + const tocIconSize = useResponsiveSize(23); + const fontIconSize = useResponsiveSize(18); + const marginIconSize = useResponsiveSize(20); + const view = getView(bookKey); const progress = getProgress(bookKey); const viewSettings = getViewSettings(bookKey); - const handleProgressChange = (event: React.ChangeEvent) => { - const newProgress = parseInt((event.target as HTMLInputElement).value, 10); - view?.goToFraction(newProgress / 100.0); + const handleProgressChange = (value: number) => { + view?.goToFraction(value / 100.0); + }; + + const handleFontSizeChange = (value: number) => { + saveViewSettings(envConfig, bookKey, 'defaultFontSize', value); + view?.renderer.setStyles?.(getStyles(viewSettings!)); + }; + + const handleMarginChange = (value: number) => { + const marginPx = Math.round((value / 100) * 88); + const gapPercent = Math.round((value / 100) * 10); + saveViewSettings(envConfig, bookKey, 'marginPx', marginPx); + saveViewSettings(envConfig, bookKey, 'gapPercent', gapPercent); + view?.renderer.setAttribute('margin', `${marginPx}px`); + view?.renderer.setAttribute('gap', `${gapPercent}%`); + if (viewSettings?.scrolled) { + view?.renderer.setAttribute('flow', 'scrolled'); + } + }; + + const handleLineHeightChange = (value: number) => { + saveViewSettings(envConfig, bookKey, 'lineHeight', value / 10); + view?.renderer.setStyles?.(getStyles(viewSettings!)); }; const handleGoPrev = () => { @@ -67,6 +105,31 @@ const FooterBar: React.FC = ({ } }; + const handleSetActionTab = (tab: string) => { + console.log('handleSetActionTab', tab); + setActionTab(actionTab === tab ? '' : tab); + if (tab === 'tts') { + setHoveredBookKey(''); + handleSpeakText(); + } else if (tab === 'toc') { + setHoveredBookKey(''); + if (viewSettings) { + viewSettings.sideBarTab = 'toc'; + } + setSideBarVisible(true); + } else if (tab === 'note') { + setHoveredBookKey(''); + setSideBarVisible(true); + if (viewSettings) { + viewSettings.sideBarTab = 'annotations'; + } + } + }; + + const getMarginProgressValue = (marginPx: number, gapPercent: number) => { + return (marginPx / 88 + gapPercent / 10) * 50; + }; + const isVisible = hoveredBookKey === bookKey; const progressInfo = bookFormat === 'PDF' ? section : pageinfo; const progressValid = !!progressInfo; @@ -78,59 +141,182 @@ const FooterBar: React.FC = ({ <>
!appService?.isMobile && setHoveredBookKey(bookKey)} onTouchStart={() => !appService?.isMobile && setHoveredBookKey(bookKey)} />
setHoveredBookKey('')} + onMouseLeave={() => window.innerWidth >= 640 && setHoveredBookKey('')} aria-hidden={!isVisible} > -
+ {/* Mobile footer bar */} +
-
+ {/* Desktop footer bar */} +
+
diff --git a/apps/readest-app/src/app/reader/components/sidebar/Header.tsx b/apps/readest-app/src/app/reader/components/sidebar/Header.tsx index 81b7f5ab..6b1b5ae9 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/Header.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/Header.tsx @@ -38,7 +38,10 @@ const SidebarHeader: React.FC<{ > -
diff --git a/apps/readest-app/src/app/reader/components/sidebar/TOCView.tsx b/apps/readest-app/src/app/reader/components/sidebar/TOCView.tsx index e5cce995..baba6e57 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/TOCView.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/TOCView.tsx @@ -68,7 +68,7 @@ const TOCItemView: React.FC<{ aria-selected={isActive ? 'true' : 'false'} data-href={item.href ? getContentMd5(item.href) : undefined} className={`flex w-full cursor-pointer items-center rounded-md py-2 ${ - isActive ? 'bg-base-300/85 hover:bg-base-300' : 'hover:bg-base-300/85' + isActive ? 'bg-base-300/85 hover:bg-base-300' : 'sm:hover:bg-base-300/85' }`} > {item.subitems && ( diff --git a/apps/readest-app/src/app/reader/components/sidebar/TabNavigation.tsx b/apps/readest-app/src/app/reader/components/sidebar/TabNavigation.tsx index fdc90ddf..b16d6ae2 100644 --- a/apps/readest-app/src/app/reader/components/sidebar/TabNavigation.tsx +++ b/apps/readest-app/src/app/reader/components/sidebar/TabNavigation.tsx @@ -1,6 +1,8 @@ import clsx from 'clsx'; import React from 'react'; -import { MdToc, MdEditNote, MdBookmarkBorder } from 'react-icons/md'; +import { MdBookmarkBorder as BookmarkIcon } from 'react-icons/md'; +import { IoIosList as TOCIcon } from 'react-icons/io'; +import { PiNotePencil as NoteIcon } from 'react-icons/pi'; import { useTranslation } from '@/hooks/useTranslation'; @@ -34,13 +36,13 @@ const TabNavigation: React.FC<{ tab === 'toc' ? _('TOC') : tab === 'annotations' ? _('Annotate') : _('Bookmark') } > -
onTabChange(tab)}> +
onTabChange(tab)}> {tab === 'toc' ? ( - + ) : tab === 'annotations' ? ( - + ) : ( - + )}
diff --git a/apps/readest-app/src/app/user/page.tsx b/apps/readest-app/src/app/user/page.tsx index c1f03d7b..8e4f479d 100644 --- a/apps/readest-app/src/app/user/page.tsx +++ b/apps/readest-app/src/app/user/page.tsx @@ -169,9 +169,9 @@ const ProfilePage = () => { > -
+
-
+
{avatarUrl ? ( @@ -202,7 +202,7 @@ const ProfilePage = () => {
-
+

{_('Features')}

    diff --git a/apps/readest-app/src/components/Slider.tsx b/apps/readest-app/src/components/Slider.tsx new file mode 100644 index 00000000..aed5bf88 --- /dev/null +++ b/apps/readest-app/src/components/Slider.tsx @@ -0,0 +1,93 @@ +import React, { useEffect, useState } from 'react'; + +interface SliderProps { + min?: number; + max?: number; + step?: number; + initialValue?: number; + heightPx?: number; + minLabel?: string; + maxLabel?: string; + bubbleElement?: React.ReactNode; + bubbleLabel?: string; + className?: string; + minClassName?: string; + maxClassName?: string; + bubbleClassName?: string; + onChange?: (value: number) => void; +} + +const Slider: React.FC = ({ + min = 0, + max = 100, + step = 1, + initialValue = 50, + heightPx = 40, + minLabel = '', + maxLabel = '', + bubbleElement, + bubbleLabel = '', + className = '', + minClassName = '', + maxClassName = '', + bubbleClassName = '', + onChange, +}) => { + const [value, setValue] = useState(initialValue); + + const handleChange = (e: React.ChangeEvent) => { + const newValue = parseInt((e.target as HTMLInputElement).value, 10); + setValue(newValue); + if (onChange) { + onChange(newValue); + } + }; + + useEffect(() => { + setValue(initialValue); + }, [initialValue]); + + const percentage = ((value - min) / (max - min)) * 100; + + return ( +
    +
    +
    +
    0 ? `calc(${percentage}% + ${heightPx / 2}px)` : '0px' }} + >
    +
    + {minLabel} + {maxLabel} +
    +
    +
    + {bubbleElement || bubbleLabel} +
    +
    + +
    +
    + ); +}; + +export default Slider; diff --git a/apps/readest-app/src/utils/style.ts b/apps/readest-app/src/utils/style.ts index a5f1ca65..7345ab69 100644 --- a/apps/readest-app/src/utils/style.ts +++ b/apps/readest-app/src/utils/style.ts @@ -268,8 +268,8 @@ export const getFootnoteStyles = () => ` } p, li, blockquote, dd { - margin: unset; - text-indent: unset; + margin: unset !important; + text-indent: unset !important; } `;