Compare commits

...

9 Commits

47 changed files with 471 additions and 381 deletions
+1
View File
@@ -137,6 +137,7 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
NODE_OPTIONS: '--max-old-space-size=8192'
with:
projectPath: apps/readest-app
releaseId: ${{ needs.get-release.outputs.release_id }}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.22",
"version": "0.9.23",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -244,5 +244,8 @@
"Contrast": "التباين",
"Sunset": "غروب الشمس",
"Double Border": "حدود مزدوجة",
"Border Color": "لون الحدود"
"Border Color": "لون الحدود",
"Border Frame": "إطار الحدود",
"Show Header": "عرض الرأس",
"Show Footer": "عرض التذييل"
}
@@ -244,5 +244,8 @@
"Contrast": "Kontrast",
"Sunset": "Sonnenuntergang",
"Double Border": "Doppelter Rand",
"Border Color": "Randfarbe"
"Border Color": "Randfarbe",
"Border Frame": "Rahmenrand",
"Show Header": "Kopfzeile anzeigen",
"Show Footer": "Fußzeile anzeigen"
}
@@ -244,5 +244,8 @@
"Contrast": "Αντίθεση",
"Sunset": "Ηλιοβασίλεμα",
"Double Border": "Διπλό περιθώριο",
"Border Color": "Χρώμα περιγράμματος"
"Border Color": "Χρώμα περιγράμματος",
"Border Frame": "Πλαίσιο περιγράμματος",
"Show Header": "Εμφάνιση κεφαλίδας",
"Show Footer": "Εμφάνιση υποσέλιδου"
}
@@ -244,5 +244,8 @@
"Contrast": "Contraste",
"Sunset": "Atardecer",
"Double Border": "Doble borde",
"Border Color": "Color del borde"
"Border Color": "Color del borde",
"Border Frame": "Marco de borde",
"Show Header": "Mostrar encabezado",
"Show Footer": "Mostrar pie de página"
}
@@ -244,5 +244,8 @@
"Contrast": "Contraste",
"Sunset": "Coucher de soleil",
"Double Border": "Double bordure",
"Border Color": "Couleur de la bordure"
"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"
}
@@ -244,5 +244,8 @@
"Contrast": "विरोधाभास",
"Sunset": "सनसेट",
"Double Border": "डबल बॉर्डर",
"Border Color": "बॉर्डर रंग"
"Border Color": "बॉर्डर रंग",
"Border Frame": "बॉर्डर फ्रेम",
"Show Header": "हेडर दिखाएं",
"Show Footer": "फ़ुटर दिखाएं"
}
@@ -244,5 +244,8 @@
"Contrast": "Kontras",
"Sunset": "Senja",
"Double Border": "Garis Ganda",
"Border Color": "Warna Garis"
"Border Color": "Warna Garis",
"Border Frame": "Bingkai Garis",
"Show Header": "Tampilkan Header",
"Show Footer": "Tampilkan Footer"
}
@@ -244,5 +244,8 @@
"Contrast": "Contrasto",
"Sunset": "Tramonto",
"Double Border": "Doppio bordo",
"Border Color": "Colore bordo"
"Border Color": "Colore bordo",
"Border Frame": "Cornice bordo",
"Show Header": "Mostra intestazione",
"Show Footer": "Mostra piè di pagina"
}
@@ -244,5 +244,8 @@
"Contrast": "コントラスト",
"Sunset": "サンセット",
"Double Border": "二重枠",
"Border Color": "ボーダーカラー"
"Border Color": "ボーダーカラー",
"Border Frame": "ボーダーフレーム",
"Show Header": "ヘッダーを表示",
"Show Footer": "フッターを表示"
}
@@ -244,5 +244,8 @@
"Contrast": "대비",
"Sunset": "일몰",
"Double Border": "이중 테두리",
"Border Color": "테두리 색상"
"Border Color": "테두리 색상",
"Border Frame": "테두리 프레임",
"Show Header": "헤더 표시",
"Show Footer": "푸터 표시"
}
@@ -244,5 +244,8 @@
"Contrast": "Kontrast",
"Sunset": "Zachód słońca",
"Double Border": "Podwójna ramka",
"Border Color": "Kolor ramki"
"Border Color": "Kolor ramki",
"Border Frame": "Ramka",
"Show Header": "Pokaż nagłówek",
"Show Footer": "Pokaż stopkę"
}
@@ -244,5 +244,8 @@
"Contrast": "Contraste",
"Sunset": "Pôr do Sol",
"Double Border": "Borda Dupla",
"Border Color": "Cor da Borda"
"Border Color": "Cor da Borda",
"Border Frame": "Moldura da Borda",
"Show Header": "Mostrar Cabeçalho",
"Show Footer": "Mostrar Rodapé"
}
@@ -244,5 +244,8 @@
"Contrast": "Контраст",
"Sunset": "Закат",
"Double Border": "Двойная граница",
"Border Color": "Цвет границы"
"Border Color": "Цвет границы",
"Border Frame": "Граница рамки",
"Show Header": "Показать заголовок",
"Show Footer": "Показать нижний колонтитул"
}
@@ -244,5 +244,8 @@
"Contrast": "Kontrast",
"Sunset": "Gün Batımı",
"Double Border": "Çift Sınır",
"Border Color": "Sınır Rengi"
"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"
}
@@ -244,5 +244,8 @@
"Contrast": "Контраст",
"Sunset": "Захід сонця",
"Double Border": "Подвійна рамка",
"Border Color": "Колір рамки"
"Border Color": "Колір рамки",
"Border Frame": "Рамка рамки",
"Show Header": "Показати заголовок",
"Show Footer": "Показати нижню частину"
}
@@ -244,5 +244,8 @@
"Contrast": "Tương phản",
"Sunset": "Hoàng hôn",
"Double Border": "Đôi viền",
"Border Color": "Màu viền"
"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"
}
@@ -243,6 +243,9 @@
"Preview": "预览",
"Contrast": "对比",
"Sunset": "日落",
"Double Border": "页面版框",
"Border Color": "版框颜色"
"Double Border": "显示版框",
"Border Color": "版框颜色",
"Border Frame": "页面版框",
"Show Header": "显示页眉",
"Show Footer": "显示页脚"
}
@@ -243,6 +243,9 @@
"Preview": "預覽",
"Contrast": "對比",
"Sunset": "日落",
"Double Border": "頁面邊框",
"Border Color": "邊框顏色"
"Double Border": "顯示邊框",
"Border Color": "邊框顏色",
"Border Frame": "頁面邊框",
"Show Header": "顯示頁眉",
"Show Footer": "顯示頁腳"
}
+9
View File
@@ -1,5 +1,14 @@
{
"releases": {
"0.9.23": {
"date": "2025-03-19",
"notes": [
"Add options to show/hide header and footer",
"Add responsive window size for popup footnotes",
"Fix theme color not applied on Safari browsers",
"Fix progress not updated to 100% when finished reading"
]
},
"0.9.22": {
"date": "2025-03-16",
"notes": [
@@ -13,7 +13,8 @@ const getProgressPercentage = (book: Book) => {
if (book.progress && book.progress[1] === 1) {
return 100;
}
return Math.round((book.progress[0] / book.progress[1]) * 100);
const percentage = Math.round((book.progress[0] / book.progress[1]) * 100);
return Math.max(0, Math.min(100, percentage));
};
const ReadingProgress: React.FC<ReadingProgressProps> = memo(
@@ -85,9 +85,8 @@ export const useBooksSync = ({ onSyncStart, onSyncEnd }: UseBooksSyncProps) => {
}
const mergedBook =
matchingBook.updatedAt > oldBook.updatedAt
? { ...oldBook, ...matchingBook, updatedAt: oldBook.updatedAt }
: { ...matchingBook, ...oldBook, updatedAt: oldBook.updatedAt };
mergedBook.progress = matchingBook.progress ?? oldBook.progress;
? { ...oldBook, ...matchingBook }
: { ...matchingBook, ...oldBook };
return mergedBook;
}
return oldBook;
@@ -81,37 +81,60 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
/>
<FoliateViewer bookKey={bookKey} bookDoc={bookDoc} config={config} />
<FootnotePopup bookKey={bookKey} bookDoc={bookDoc} />
{viewSettings.scrolled ? null : (
{viewSettings.vertical && (
<>
{viewSettings.vertical && viewSettings.doubleBorder && (
<DoubleBorder
borderColor={viewSettings.borderColor}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
/>
)}
<SectionInfo
section={sectionLabel}
isVertical={viewSettings.vertical}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
<div
className='bg-base-100 absolute left-0 top-0 h-full'
style={{
width: `calc(${horizontalGapPercent}%)`,
height: `calc(100% - ${verticalMarginPixels}px)`,
}}
/>
<HintInfo
bookKey={bookKey}
isVertical={viewSettings.vertical}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
/>
<PageInfoView
bookFormat={book.format}
section={section}
pageinfo={pageinfo}
isVertical={viewSettings.vertical}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
<div
className='bg-base-100 absolute right-0 top-0 h-full'
style={{
width: `calc(${horizontalGapPercent}%)`,
height: `calc(100% - ${verticalMarginPixels}px)`,
}}
/>
</>
)}
{viewSettings.vertical && viewSettings.doubleBorder && (
<DoubleBorder
showHeader={viewSettings.showHeader}
showFooter={viewSettings.showFooter}
borderColor={viewSettings.borderColor}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
/>
)}
{viewSettings.showHeader && (
<SectionInfo
section={sectionLabel}
showDoubleBorder={viewSettings.vertical && viewSettings.doubleBorder}
isVertical={viewSettings.vertical}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
/>
)}
<HintInfo
bookKey={bookKey}
showDoubleBorder={viewSettings.vertical && viewSettings.doubleBorder}
isVertical={viewSettings.vertical}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
/>
{viewSettings.showFooter && (
<PageInfoView
bookFormat={book.format}
section={section}
pageinfo={pageinfo}
showDoubleBorder={viewSettings.vertical && viewSettings.doubleBorder}
isVertical={viewSettings.vertical}
horizontalGap={horizontalGapPercent}
verticalMargin={verticalMarginPixels}
/>
)}
<Annotator bookKey={bookKey} />
<FooterBar
bookKey={bookKey}
@@ -2,6 +2,8 @@ interface DoubleBorderProps {
borderColor: string;
horizontalGap: number;
verticalMargin: number;
showHeader: boolean;
showFooter: boolean;
}
const paddingPx = 10;
@@ -10,51 +12,63 @@ const DoubleBorder: React.FC<DoubleBorderProps> = ({
borderColor,
horizontalGap,
verticalMargin,
showHeader,
showFooter,
}) => {
return (
<>
{/* outter frame */}
<div
className={'borderframe pointer-events-none absolute'}
style={{
border: `4px solid ${borderColor}`,
height: `calc(100% - ${verticalMargin * 2}px + ${paddingPx * 2}px)`,
top: `calc(${verticalMargin}px - ${paddingPx}px)`,
left: `calc(${horizontalGap * 2}% - 32px - ${paddingPx}px)`,
right: `calc(${horizontalGap * 2}% - 32px - ${paddingPx}px)`,
left: `calc(${horizontalGap}% - ${showFooter ? 32 : 0}px - ${paddingPx}px)`,
right: `calc(${horizontalGap}% - ${showHeader ? 32 : 0}px - ${paddingPx}px)`,
}}
></div>
{/* inner frame */}
<div
className={'borderframe pointer-events-none absolute'}
style={{
border: `1px solid ${borderColor}`,
height: `calc(100% - ${verticalMargin * 2}px)`,
top: `${verticalMargin}px`,
left: `${horizontalGap * 2}%`,
right: `${horizontalGap * 2}%`,
}}
/>
<div
className={'borderframe pointer-events-none absolute'}
style={{
border: `1px solid ${borderColor}`,
width: '32px',
height: `calc(100% - ${verticalMargin * 2}px)`,
top: `${verticalMargin}px`,
left: `calc(${horizontalGap * 2}% - 32px)`,
borderRight: 'none',
}}
/>
<div
className={'borderframe pointer-events-none absolute'}
style={{
border: `1px solid ${borderColor}`,
width: '32px',
height: `calc(100% - ${verticalMargin * 2}px)`,
top: `${verticalMargin}px`,
left: `calc(100% - ${horizontalGap * 2}%)`,
borderLeft: 'none',
left: showFooter ? `${horizontalGap}%` : `calc(${horizontalGap}%)`,
right: showHeader ? `${horizontalGap}%` : `calc(${horizontalGap}%)`,
}}
/>
{/* footer */}
{showFooter && (
<div
className={'borderframe pointer-events-none absolute'}
style={{
borderTop: `1px solid ${borderColor}`,
borderBottom: `1px solid ${borderColor}`,
borderLeft: `1px solid ${borderColor}`,
width: '32px',
height: `calc(100% - ${verticalMargin * 2}px)`,
top: `${verticalMargin}px`,
left: `calc(${horizontalGap}% - 32px)`,
}}
/>
)}
{/* header */}
{showHeader && (
<div
className={'borderframe pointer-events-none absolute'}
style={{
borderTop: `1px solid ${borderColor}`,
borderBottom: `1px solid ${borderColor}`,
borderRight: `1px solid ${borderColor}`,
width: '32px',
height: `calc(100% - ${verticalMargin * 2}px)`,
top: `${verticalMargin}px`,
left: `calc(100% - ${horizontalGap}%)`,
}}
/>
)}
</>
);
};
@@ -9,7 +9,7 @@ import { useClickEvent, useTouchEvent } from '../hooks/useIframeEvents';
import { useFoliateEvents } from '../hooks/useFoliateEvents';
import { useProgressSync } from '../hooks/useProgressSync';
import { useProgressAutoSave } from '../hooks/useProgressAutoSave';
import { getStyles, mountAdditionalFonts } from '@/utils/style';
import { applyColorScheme, getStyles, mountAdditionalFonts } from '@/utils/style';
import { getBookDirFromLanguage, getBookDirFromWritingMode } from '@/utils/book';
import { useUICSS } from '@/hooks/useUICSS';
import {
@@ -35,7 +35,7 @@ const FoliateViewer: React.FC<{
const { getView, setView: setFoliateView, setProgress } = useReaderStore();
const { getViewSettings, setViewSettings } = useReaderStore();
const { getParallels } = useParallelViewStore();
const { themeCode, isDarkMode } = useThemeStore();
const { themeCode, isDarkMode, getIsDarkMode } = useThemeStore();
const viewSettings = getViewSettings(bookKey)!;
const [toastMessage, setToastMessage] = useState('');
@@ -62,8 +62,9 @@ const FoliateViewer: React.FC<{
viewSettings.vertical =
writingDir?.vertical || viewSettings.writingMode.includes('vertical') || false;
viewSettings.rtl = writingDir?.rtl || viewSettings.writingMode.includes('rl') || false;
setViewSettings(bookKey, viewSettings);
setViewSettings(bookKey, { ...viewSettings });
applyColorScheme(detail.doc, getIsDarkMode());
mountAdditionalFonts(detail.doc);
if (!detail.doc.isEventListenersAdded) {
@@ -122,6 +123,9 @@ const FoliateViewer: React.FC<{
if (viewRef.current && viewRef.current.renderer) {
const viewSettings = getViewSettings(bookKey)!;
viewRef.current.renderer.setStyles?.(getStyles(viewSettings));
// Safari does not recognize color-scheme from the root document in iframe
const doc = viewRef.current.renderer.getContents()[0]!.doc;
applyColorScheme(doc, isDarkMode);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [themeCode, isDarkMode]);
@@ -70,7 +70,9 @@ const FooterBar: React.FC<FooterBarProps> = ({
const isVisible = hoveredBookKey === bookKey;
const progressInfo = bookFormat === 'PDF' ? section : pageinfo;
const progressValid = !!progressInfo;
const progressFraction = progressValid ? (progressInfo!.current + 1) / progressInfo!.total : 0;
const progressFraction = progressValid
? ((progressInfo!.next ?? progressInfo!.current) + 1) / progressInfo!.total
: 0;
return (
<>
@@ -1,10 +1,9 @@
import React, { useEffect, useRef, useState } from 'react';
import { BookDoc } from '@/libs/document';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
import { useFoliateEvents } from '../hooks/useFoliateEvents';
import { getFootnoteStyles, getStyles } from '@/utils/style';
import { getFootnoteStyles, getStyles, getThemeCode } from '@/utils/style';
import { getPopupPosition, getPosition, Position } from '@/utils/sel';
import { eventDispatcher } from '@/utils/event';
import { FoliateView } from '@/types/view';
@@ -26,12 +25,17 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
const [trianglePosition, setTrianglePosition] = useState<Position | null>();
const [popupPosition, setPopupPosition] = useState<Position | null>();
const [showPopup, setShowPopup] = useState(false);
const { getView, getViewSettings } = useReaderStore();
const { themeCode } = useThemeStore();
const { getView, getProgress, getViewSettings } = useReaderStore();
const view = getView(bookKey);
const progress = getProgress(bookKey)!;
const viewSettings = getViewSettings(bookKey)!;
const footnoteHandler = new FootnoteHandler();
const [gridRect, setGridRect] = useState<DOMRect | null>(null);
const [responsiveWidth, setResponsiveWidth] = useState(popupWidth);
const [responsiveHeight, setResponsiveHeight] = useState(popupHeight);
useEffect(() => {
const handleBeforeRender = (e: Event) => {
const detail = (e as CustomEvent).detail;
@@ -50,8 +54,9 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
const { renderer } = view;
renderer.setAttribute('flow', 'scrolled');
renderer.setAttribute('margin', '0px');
renderer.setAttribute('gap', '5%');
renderer.setAttribute('gap', '0%');
const viewSettings = getViewSettings(bookKey)!;
const themeCode = getThemeCode();
const popupTheme = { ...themeCode };
const popupContainer = document.getElementById('popup-container');
if (popupContainer) {
@@ -65,8 +70,17 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
const handleRender = (e: Event) => {
const detail = (e as CustomEvent).detail;
console.log('render footnote', detail);
setShowPopup(true);
// console.log('render footnote', detail);
const { view } = detail;
view.addEventListener('relocate', () => {
const { renderer } = view as FoliateView;
if (viewSettings.vertical) {
setResponsiveWidth(Math.min(renderer.viewSize, window.innerWidth / 2));
} else {
setResponsiveHeight(Math.min(renderer.viewSize, window.innerHeight / 2));
}
setShowPopup(true);
});
};
footnoteHandler.addEventListener('before-render', handleBeforeRender);
@@ -76,25 +90,46 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
footnoteHandler.removeEventListener('render', handleRender);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [view, themeCode]);
}, [view]);
useEffect(() => {
handleDismissPopup();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [progress]);
useEffect(() => {
if (viewSettings.vertical) {
setResponsiveWidth(popupHeight);
setResponsiveHeight(Math.max(360, window.innerHeight / 4));
} else {
setResponsiveWidth(Math.max(360, window.innerWidth / 4));
setResponsiveHeight(popupHeight);
}
}, [viewSettings]);
useEffect(() => {
if (trianglePosition && gridRect) {
const popupPos = getPopupPosition(
trianglePosition,
gridRect,
responsiveWidth,
responsiveHeight,
popupPadding,
);
setPopupPosition(popupPos);
}
}, [trianglePosition, gridRect, responsiveWidth, responsiveHeight]);
const docLinkHandler = async (event: Event) => {
const detail = (event as CustomEvent).detail;
console.log('doc link click', detail);
// console.log('doc link click', detail);
const gridFrame = document.querySelector(`#gridcell-${bookKey}`);
if (!gridFrame) return;
const rect = gridFrame.getBoundingClientRect();
const viewSettings = getViewSettings(bookKey)!;
const triangPos = getPosition(detail.a, rect, popupPadding, viewSettings.vertical);
const popupPos = getPopupPosition(
triangPos,
rect,
viewSettings.vertical ? popupHeight : popupWidth,
viewSettings.vertical ? popupWidth : popupHeight,
popupPadding,
);
setGridRect(rect);
setTrianglePosition(triangPos);
setPopupPosition(popupPos);
footnoteHandler.handle(bookDoc, event)?.catch((err) => {
console.warn(err);
@@ -111,11 +146,13 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
const handleDismissPopup = () => {
closePopup();
setGridRect(null);
setPopupPosition(null);
setTrianglePosition(null);
setShowPopup(false);
};
// Handle custom footnote popup event from iframe event
const handleFootnotePopupEvent = (event: CustomEvent) => {
const { element, footnote } = event.detail;
const gridFrame = document.querySelector(`#gridcell-${bookKey}`);
@@ -123,21 +160,30 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
const rect = gridFrame.getBoundingClientRect();
const viewSettings = getViewSettings(bookKey)!;
const triangPos = getPosition(element, rect, popupPadding, viewSettings.vertical);
const popupPos = getPopupPosition(
triangPos,
rect,
viewSettings.vertical ? popupHeight : popupWidth,
viewSettings.vertical ? popupWidth : popupHeight,
popupPadding,
);
if (footnoteRef.current) {
const elem = document.createElement('p');
elem.textContent = footnote;
elem.setAttribute('style', `padding: 16px; hanging-punctuation: allow-end last;`);
elem.setAttribute('style', `padding: 1em; hanging-punctuation: allow-end last;`);
elem.style.visibility = 'hidden';
if (viewSettings.vertical) {
elem.style.height = `${responsiveHeight}px`;
} else {
elem.style.width = `${responsiveWidth}px`;
}
document.body.appendChild(elem);
const popupSize = elem.getBoundingClientRect();
if (viewSettings.vertical) {
setResponsiveWidth(Math.min(popupSize.width, window.innerWidth / 2));
} else {
setResponsiveHeight(Math.min(popupSize.height, window.innerHeight / 2));
}
document.body.removeChild(elem);
elem.style.visibility = 'visible';
footnoteRef.current.replaceChildren(elem);
setShowPopup(true);
setGridRect(rect);
setTrianglePosition(triangPos);
setPopupPosition(popupPos);
setShowPopup(true);
}
};
@@ -159,9 +205,6 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
}
}, [footnoteRef]);
const width = viewSettings.vertical ? popupHeight : popupWidth;
const height = viewSettings.vertical ? popupWidth : popupHeight;
return (
<div>
{showPopup && (
@@ -172,8 +215,8 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
/>
)}
<Popup
width={width}
height={height}
width={responsiveWidth}
height={responsiveHeight}
position={showPopup ? popupPosition! : undefined}
trianglePosition={showPopup ? trianglePosition! : undefined}
className='select-text overflow-y-auto'
@@ -182,8 +225,8 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
className=''
ref={footnoteRef}
style={{
width: `${width}px`,
height: `${height}px`,
width: `${responsiveWidth}px`,
height: `${responsiveHeight}px`,
}}
></div>
</Popup>
@@ -1,11 +1,10 @@
import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';
import { useSidebarStore } from '@/store/sidebarStore';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import { eventDispatcher } from '@/utils/event';
interface SectionInfoProps {
bookKey: string;
showDoubleBorder: boolean;
isVertical: boolean;
horizontalGap: number;
verticalMargin: number;
@@ -13,12 +12,11 @@ interface SectionInfoProps {
const HintInfo: React.FC<SectionInfoProps> = ({
bookKey,
showDoubleBorder,
isVertical,
horizontalGap,
verticalMargin,
}) => {
const { isSideBarVisible } = useSidebarStore();
const { isTrafficLightVisible } = useTrafficLightStore();
const [hintMessage, setHintMessage] = React.useState<string | null>(null);
const hintTimeout = useRef(2000);
const dismissTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -49,26 +47,21 @@ const HintInfo: React.FC<SectionInfoProps> = ({
return (
<div
className={clsx(
'sectioninfo absolute flex justify-end overflow-hidden',
!isVertical && (isTrafficLightVisible && !isSideBarVisible ? 'h-[44px]' : 'h-[30px]'),
isVertical ? 'writing-vertical-rl w-[32px] items-center' : 'top-0 items-end',
isVertical ? 'max-h-[50%]' : 'max-w-[50%]',
'hintinfo absolute flex items-center justify-end overflow-hidden',
hintMessage ? 'bg-base-100' : 'bg-transparent',
isVertical ? 'writing-vertical-rl max-h-[50%]' : 'top-0 h-[44px] max-w-[50%]',
)}
style={
isVertical
? {
bottom: `calc(${horizontalGap / 2}% + ${verticalMargin}px)`,
left: `calc(100% - ${horizontalGap * 2}%)`,
height: `calc(100% - ${verticalMargin * 2}px)`,
bottom: `${verticalMargin * 1.5}px`,
left: `calc(100% - ${horizontalGap}%)`,
width: showDoubleBorder ? '30px' : `${horizontalGap}%`,
}
: { right: `${horizontalGap}%` }
: { insetInlineEnd: `${horizontalGap}%` }
}
>
<h2
className={clsx(
'text-neutral-content line-clamp-1 text-center font-sans text-xs font-light',
)}
>
<h2 className={clsx('text-neutral-content text-center font-sans text-xs font-light')}>
{hintMessage || ''}
</h2>
</div>
@@ -7,6 +7,7 @@ interface PageInfoProps {
bookFormat: string;
section?: PageInfo;
pageinfo?: PageInfo;
showDoubleBorder: boolean;
isVertical: boolean;
horizontalGap: number;
verticalMargin: number;
@@ -16,6 +17,7 @@ const PageInfoView: React.FC<PageInfoProps> = ({
bookFormat,
section,
pageinfo,
showDoubleBorder,
isVertical,
horizontalGap,
verticalMargin,
@@ -32,7 +34,7 @@ const PageInfoView: React.FC<PageInfoProps> = ({
? _(
isVertical ? '{{currentPage}} · {{totalPage}}' : 'Loc. {{currentPage}} / {{totalPage}}',
{
currentPage: pageinfo.current + 1,
currentPage: (pageinfo.next ?? pageinfo.current) + 1,
totalPage: pageinfo.total,
},
)
@@ -42,17 +44,17 @@ const PageInfoView: React.FC<PageInfoProps> = ({
<div
className={clsx(
'pageinfo absolute bottom-0 flex items-center justify-end',
isVertical ? 'left-0' : 'right-0',
isVertical ? 'writing-vertical-rl w-[32px]' : 'h-12',
isVertical ? 'writing-vertical-rl' : 'bg-base-100 h-12 w-full',
)}
style={
isVertical
? {
bottom: `calc(${horizontalGap / 2}% + ${verticalMargin}px)`,
left: `calc(${horizontalGap * 2}% - 32px)`,
bottom: `${verticalMargin * 1.5}px`,
left: showDoubleBorder ? `calc(${horizontalGap}% - 32px)` : 0,
width: showDoubleBorder ? '32px' : `${horizontalGap}%`,
height: `calc(100% - ${verticalMargin * 2}px)`,
}
: { paddingRight: `${horizontalGap}%` }
: { insetInlineEnd: `${horizontalGap}%` }
}
>
<h2 className='text-neutral-content text-right font-sans text-xs font-extralight'>
@@ -1,11 +1,9 @@
import clsx from 'clsx';
import React from 'react';
import { useSidebarStore } from '@/store/sidebarStore';
import { useTrafficLightStore } from '@/store/trafficLightStore';
interface SectionInfoProps {
section?: string;
showDoubleBorder: boolean;
isVertical: boolean;
horizontalGap: number;
verticalMargin: number;
@@ -13,33 +11,32 @@ interface SectionInfoProps {
const SectionInfo: React.FC<SectionInfoProps> = ({
section,
showDoubleBorder,
isVertical,
horizontalGap,
verticalMargin,
}) => {
const { isSideBarVisible } = useSidebarStore();
const { isTrafficLightVisible } = useTrafficLightStore();
return (
<div
className={clsx(
'sectioninfo absolute flex overflow-hidden',
!isVertical && (isTrafficLightVisible && !isSideBarVisible ? 'h-[44px]' : 'h-[30px]'),
isVertical ? 'writing-vertical-rl w-[32px] items-center' : 'top-0 items-end',
isVertical ? 'max-h-[50%]' : 'max-w-[50%]',
'sectioninfo absolute flex items-center overflow-hidden',
isVertical ? 'writing-vertical-rl max-h-[85%]' : 'bg-base-100 top-0 h-[44px]',
)}
style={
isVertical
? {
top: `calc(${horizontalGap / 2}% + ${verticalMargin}px)`,
left: `calc(100% - ${horizontalGap * 2}%)`,
top: `${verticalMargin * 1.5}px`,
left: `calc(100% - ${horizontalGap}%)`,
width: showDoubleBorder ? '32px' : `${horizontalGap}%`,
height: `calc(100% - ${verticalMargin * 2}px)`,
}
: { left: `${horizontalGap}%` }
: { insetInlineStart: `${horizontalGap}%`, width: `calc(100% - ${horizontalGap * 2}%)` }
}
>
<h2
className={clsx(
'text-neutral-content line-clamp-1 text-center font-sans text-xs font-light',
'text-neutral-content text-center font-sans text-xs font-light',
isVertical ? '' : 'line-clamp-1',
)}
>
{section || ''}
@@ -14,13 +14,13 @@ import {
WINDOWS_FONTS,
} from '@/services/constants';
import { useReaderStore } from '@/store/readerStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useEnv } from '@/context/EnvContext';
import { getStyles } from '@/utils/style';
import { getOSPlatform } from '@/utils/misc';
import { getSysFontsList } from '@/utils/font';
import { isTauriAppPlatform } from '@/services/environment';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
interface FontFaceProps {
className?: string;
@@ -60,9 +60,8 @@ const FontFace = ({
const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const _ = useTranslation();
const { appService } = useEnv();
const { settings, isFontLayoutSettingsGlobal, setSettings } = useSettingsStore();
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const { envConfig, appService } = useEnv();
const { getView, getViewSettings } = useReaderStore();
const view = getView(bookKey);
const viewSettings = getViewSettings(bookKey)!;
@@ -118,89 +117,49 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
}, []);
useEffect(() => {
viewSettings.defaultFont = defaultFont;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.defaultFont = defaultFont;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'defaultFont', defaultFont);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultFont]);
useEffect(() => {
viewSettings.defaultFontSize = defaultFontSize;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.defaultFontSize = defaultFontSize;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'defaultFontSize', defaultFontSize);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultFontSize]);
useEffect(() => {
viewSettings.minimumFontSize = minFontSize;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.minimumFontSize = minFontSize;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'minimumFontSize', minFontSize);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minFontSize]);
useEffect(() => {
viewSettings.fontWeight = fontWeight;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.fontWeight = fontWeight;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'fontWeight', fontWeight);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fontWeight]);
useEffect(() => {
viewSettings.serifFont = serifFont;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.serifFont = serifFont;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'serifFont', serifFont);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [serifFont]);
useEffect(() => {
viewSettings.sansSerifFont = sansSerifFont;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.sansSerifFont = sansSerifFont;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'sansSerifFont', sansSerifFont);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sansSerifFont]);
useEffect(() => {
viewSettings.monospaceFont = monospaceFont;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.monospaceFont = monospaceFont;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'monospaceFont', monospaceFont);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [monospaceFont]);
useEffect(() => {
viewSettings.overrideFont = overrideFont;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.overrideFont = overrideFont;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'overrideFont', overrideFont);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [overrideFont]);
@@ -225,7 +184,6 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<div className='card border-base-200 border shadow'>
<div className='divide-base-200 divide-y'>
<NumberInput
className='config-item-top'
label={_('Default Font Size')}
value={defaultFontSize}
onChange={setDefaultFontSize}
@@ -233,7 +191,6 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
max={120}
/>
<NumberInput
className='config-item-bottom'
label={_('Minimum Font Size')}
value={minFontSize}
onChange={setMinFontSize}
@@ -249,7 +206,6 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<div className='card border-base-200 border shadow'>
<div className='divide-base-200 divide-y'>
<NumberInput
className='config-item-top'
label={_('Font Weight')}
value={fontWeight}
onChange={setFontWeight}
@@ -265,7 +221,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<h2 className='mb-2 font-medium'>{_('Font Family')}</h2>
<div className='card border-base-200 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item config-item-top'>
<div className='config-item'>
<span className=''>{_('Default Font')}</span>
<FontDropdown
options={fontFamilyOptions}
@@ -275,7 +231,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
/>
</div>
<div className='config-item config-item-bottom'>
<div className='config-item'>
<span className=''>{_('Override Book Font')}</span>
<input
type='checkbox'
@@ -3,7 +3,7 @@ import { MdOutlineAutoMode } from 'react-icons/md';
import { MdOutlineTextRotationNone, MdTextRotateVertical } from 'react-icons/md';
import { TbTextDirectionRtl } from 'react-icons/tb';
import { useSettingsStore } from '@/store/settingsStore';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useTranslation } from '@/hooks/useTranslation';
@@ -12,11 +12,12 @@ import { getStyles } from '@/utils/style';
import { getMaxInlineSize } from '@/utils/config';
import { getBookDirFromWritingMode, getBookLangCode } from '@/utils/book';
import { MIGHT_BE_RTL_LANGS } from '@/services/constants';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import NumberInput from './NumberInput';
const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const _ = useTranslation();
const { settings, isFontLayoutSettingsGlobal, setSettings } = useSettingsStore();
const { envConfig } = useEnv();
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const { getBookData } = useBookDataStore();
const view = getView(bookKey);
@@ -40,102 +41,59 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [isScrolledMode, setScrolledMode] = useState(viewSettings.scrolled!);
const [doubleBorder, setDoubleBorder] = useState(viewSettings.doubleBorder!);
const [borderColor, setBorderColor] = useState(viewSettings.borderColor!);
const [showHeader, setShowHeader] = useState(viewSettings.showHeader!);
const [showFooter, setShowFooter] = useState(viewSettings.showFooter!);
useEffect(() => {
viewSettings.paragraphMargin = paragraphMargin;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.paragraphMargin = paragraphMargin;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'paragraphMargin', paragraphMargin);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paragraphMargin]);
useEffect(() => {
viewSettings.lineHeight = lineHeight;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.lineHeight = lineHeight;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'lineHeight', lineHeight);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lineHeight]);
useEffect(() => {
viewSettings.wordSpacing = wordSpacing;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.wordSpacing = wordSpacing;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'wordSpacing', wordSpacing);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wordSpacing]);
useEffect(() => {
viewSettings.letterSpacing = letterSpacing;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.letterSpacing = letterSpacing;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'letterSpacing', letterSpacing);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [letterSpacing]);
useEffect(() => {
viewSettings.textIndent = textIndent;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.textIndent = textIndent;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'textIndent', textIndent);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [textIndent]);
useEffect(() => {
viewSettings.fullJustification = fullJustification;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.fullJustification = fullJustification;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'fullJustification', fullJustification);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fullJustification]);
useEffect(() => {
viewSettings.hyphenation = hyphenation;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.hyphenation = hyphenation;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'hyphenation', hyphenation);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hyphenation]);
useEffect(() => {
viewSettings.marginPx = marginPx;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.marginPx = marginPx;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'marginPx', marginPx);
view?.renderer.setAttribute('margin', `${marginPx}px`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [marginPx]);
useEffect(() => {
viewSettings.gapPercent = gapPercent;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.gapPercent = gapPercent;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'gapPercent', gapPercent);
view?.renderer.setAttribute('gap', `${gapPercent}%`);
if (viewSettings.scrolled) {
view?.renderer.setAttribute('flow', 'scrolled');
@@ -144,35 +102,20 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
}, [gapPercent]);
useEffect(() => {
viewSettings.maxColumnCount = maxColumnCount;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.maxColumnCount = maxColumnCount;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'maxColumnCount', maxColumnCount);
view?.renderer.setAttribute('max-column-count', maxColumnCount);
view?.renderer.setAttribute('max-inline-size', `${getMaxInlineSize(viewSettings)}px`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [maxColumnCount]);
useEffect(() => {
viewSettings.maxInlineSize = maxInlineSize;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.maxInlineSize = maxInlineSize;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'maxInlineSize', maxInlineSize);
view?.renderer.setAttribute('max-inline-size', `${getMaxInlineSize(viewSettings)}px`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [maxInlineSize]);
useEffect(() => {
viewSettings.maxBlockSize = maxBlockSize;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.maxBlockSize = maxBlockSize;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'maxBlockSize', maxBlockSize);
view?.renderer.setAttribute('max-block-size', `${maxBlockSize}px`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [maxBlockSize]);
@@ -180,18 +123,18 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
useEffect(() => {
// global settings are not supported for writing mode
const prevWritingMode = viewSettings.writingMode;
viewSettings.writingMode = writingMode;
if (writingMode.includes('vertical')) {
viewSettings.vertical = true;
if (!writingMode.includes('vertical')) {
viewSettings.vertical = false;
}
setViewSettings(bookKey, viewSettings);
saveViewSettings(envConfig, bookKey, 'writingMode', writingMode, true);
if (view) {
view.renderer.setStyles?.(getStyles(viewSettings));
view.book.dir = getBookDirFromWritingMode(writingMode);
}
if (
prevWritingMode !== writingMode &&
(writingMode === 'horizontal-rl' || prevWritingMode === 'horizontal-rl')
(['horizontal-rl', 'vertical-rl'].includes(writingMode) ||
['horizontal-rl', 'vertical-rl'].includes(prevWritingMode))
) {
setTimeout(() => window.location.reload(), 100);
}
@@ -199,52 +142,49 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
}, [writingMode]);
useEffect(() => {
viewSettings.overrideLayout = overrideLayout;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.overrideLayout = overrideLayout;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'overrideLayout', overrideLayout);
view?.renderer.setStyles?.(getStyles(viewSettings));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [overrideLayout]);
useEffect(() => {
viewSettings!.scrolled = isScrolledMode;
saveViewSettings(envConfig, bookKey, 'scrolled', isScrolledMode);
getView(bookKey)?.renderer.setAttribute('flow', isScrolledMode ? 'scrolled' : 'paginated');
getView(bookKey)?.renderer.setAttribute(
'max-inline-size',
`${getMaxInlineSize(viewSettings)}px`,
);
getView(bookKey)?.renderer.setStyles?.(getStyles(viewSettings!));
setViewSettings(bookKey, viewSettings!);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.scrolled = isScrolledMode;
setSettings(settings);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isScrolledMode]);
useEffect(() => {
viewSettings.doubleBorder = doubleBorder;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.doubleBorder = doubleBorder;
setSettings(settings);
if (doubleBorder && viewSettings.vertical) {
viewSettings.gapPercent = Math.max(
viewSettings.gapPercent,
Math.ceil(4800 / window.innerWidth),
);
setViewSettings(bookKey, viewSettings);
}
saveViewSettings(envConfig, bookKey, 'doubleBorder', doubleBorder);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [doubleBorder]);
useEffect(() => {
viewSettings.borderColor = borderColor;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.borderColor = borderColor;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'borderColor', borderColor);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [borderColor]);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'showHeader', showHeader);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showHeader]);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'showFooter', showFooter);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showFooter]);
const langCode = getBookLangCode(bookData.bookDoc?.metadata?.language);
const mightBeRTLBook = MIGHT_BE_RTL_LANGS.includes(langCode) || isCJKEnv();
@@ -308,36 +248,37 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
)}
{viewSettings.vertical && (
<>
<div className='w-full'>
<div className='flex items-center justify-between'>
<h2 className='font-medium'>{_('Double Border')}</h2>
<input
type='checkbox'
className='toggle'
checked={doubleBorder}
onChange={() => setDoubleBorder(!doubleBorder)}
/>
</div>
</div>
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Border Frame')}</h2>
<div className='card bg-base-100 border-base-200 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item'>
<span className=''>{_('Double Border')}</span>
<input
type='checkbox'
className='toggle'
checked={doubleBorder}
onChange={() => setDoubleBorder(!doubleBorder)}
/>
</div>
<div className='w-full'>
<div className='flex items-center justify-between'>
<h2 className='font-medium'>{_('Border Color')}</h2>
<div className='flex gap-4'>
<button
className={`btn btn-circle btn-sm bg-red-300 hover:bg-red-500 ${borderColor === 'red' ? 'btn-active !bg-red-500' : ''}`}
onClick={() => setBorderColor('red')}
></button>
<div className='config-item'>
<span className=''>{_('Border Color')}</span>
<div className='flex gap-4'>
<button
className={`btn btn-circle btn-sm bg-red-300 hover:bg-red-500 ${borderColor === 'red' ? 'btn-active !bg-red-500' : ''}`}
onClick={() => setBorderColor('red')}
></button>
<button
className={`btn btn-circle btn-sm bg-black/50 hover:bg-black ${borderColor === 'black' ? 'btn-active !bg-black' : ''}`}
onClick={() => setBorderColor('black')}
></button>
<button
className={`btn btn-circle btn-sm bg-black/50 hover:bg-black ${borderColor === 'black' ? 'btn-active !bg-black' : ''}`}
onClick={() => setBorderColor('black')}
></button>
</div>
</div>
</div>
</div>
</>
</div>
)}
<div className='w-full'>
@@ -345,7 +286,6 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<div className='card bg-base-100 border-base-200 border shadow'>
<div className='divide-base-200 divide-y'>
<NumberInput
className='config-item-top'
label={_('Paragraph Margin')}
value={paragraphMargin}
onChange={setParagraphMargin}
@@ -385,7 +325,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
max={4}
step={1}
/>
<div className='config-item config-item-bottom'>
<div className='config-item'>
<span className=''>{_('Full Justification')}</span>
<input
type='checkbox'
@@ -394,7 +334,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
onChange={() => setFullJustification(!fullJustification)}
/>
</div>
<div className='config-item config-item-bottom'>
<div className='config-item'>
<span className=''>{_('Hyphenation')}</span>
<input
type='checkbox'
@@ -403,7 +343,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
onChange={() => setHyphenation(!hyphenation)}
/>
</div>
<div className='config-item config-item-bottom'>
<div className='config-item'>
<span className=''>{_('Override Book Layout')}</span>
<input
type='checkbox'
@@ -420,8 +360,25 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<h2 className='mb-2 font-medium'>{_('Page')}</h2>
<div className='card bg-base-100 border-base-200 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item'>
<span className=''>{_('Show Header')}</span>
<input
type='checkbox'
className='toggle'
checked={showHeader}
onChange={() => setShowHeader(!showHeader)}
/>
</div>
<div className='config-item'>
<span className=''>{_('Show Footer')}</span>
<input
type='checkbox'
className='toggle'
checked={showFooter}
onChange={() => setShowFooter(!showFooter)}
/>
</div>
<NumberInput
className='config-item-top'
label={_('Vertical Margins (px)')}
value={marginPx}
onChange={setMarginPx}
@@ -433,7 +390,11 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
label={_('Horizontal Margins (%)')}
value={gapPercent}
onChange={setGapPercent}
min={viewSettings.vertical ? 2 : 0}
min={
viewSettings.vertical && (showFooter || showHeader)
? Math.ceil(4800 / window.innerWidth)
: 0
}
max={30}
/>
<NumberInput
@@ -1,14 +1,17 @@
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { getStyles } from '@/utils/style';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import cssbeautify from 'cssbeautify';
import cssValidate from '@/utils/css';
const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const _ = useTranslation();
const { envConfig } = useEnv();
const { settings, isFontLayoutSettingsGlobal, setSettings } = useSettingsStore();
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey)!;
@@ -67,12 +70,7 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
};
useEffect(() => {
viewSettings.animated = animated;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.animated = animated;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'animated', animated);
if (animated) {
getView(bookKey)?.renderer.setAttribute('animated', '');
} else {
@@ -82,22 +80,12 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
}, [animated]);
useEffect(() => {
viewSettings.disableClick = isDisableClick;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.disableClick = isDisableClick;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'disableClick', isDisableClick);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDisableClick]);
useEffect(() => {
viewSettings.continuousScroll = isContinuousScroll;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.continuousScroll = isContinuousScroll;
setSettings(settings);
}
saveViewSettings(envConfig, bookKey, 'continuousScroll', isContinuousScroll);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isContinuousScroll]);
@@ -107,7 +95,7 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<h2 className='mb-2 font-medium'>{_('Animation')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item config-item-top config-item-bottom'>
<div className='config-item'>
<span className=''>{_('Paging Animation')}</span>
<input
type='checkbox'
@@ -124,7 +112,7 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<h2 className='mb-2 font-medium'>{_('Behavior')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item config-item-top'>
<div className='config-item'>
<span className=''>{_('Continuous Scroll')}</span>
<input
type='checkbox'
@@ -133,7 +121,7 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
onChange={() => setIsContinuousScroll(!isContinuousScroll)}
/>
</div>
<div className='config-item config-item-bottom'>
<div className='config-item'>
<span className=''>{_('Disable Click-to-Flip')}</span>
<input
type='checkbox'
@@ -28,7 +28,9 @@ type TabConfig = {
const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ bookKey }) => {
const _ = useTranslation();
const [activePanel, setActivePanel] = useState<SettingsPanelType>('Font');
const [activePanel, setActivePanel] = useState<SettingsPanelType>(
(localStorage.getItem('lastConfigPanel') || 'Font') as SettingsPanelType,
);
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
const tabConfig = [
@@ -54,6 +56,11 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
},
] as TabConfig[];
const handleSetActivePanel = (tab: SettingsPanelType) => {
setActivePanel(tab);
localStorage.setItem('lastConfigPanel', tab);
};
const handleClose = () => {
setFontLayoutSettingsDialogOpen(false);
};
@@ -76,7 +83,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
>
<MdArrowBackIosNew />
</button>
<div className='dialog-tabs flex h-10 max-w-[100%] flex-grow items-center pl-4 gap-2'>
<div className='dialog-tabs flex h-10 max-w-[100%] flex-grow items-center gap-2 pl-4'>
{tabConfig.map(({ tab, icon: Icon, label }) => (
<button
key={tab}
@@ -84,7 +91,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
'btn btn-ghost text-base-content btn-sm',
activePanel === tab ? 'btn-active' : '',
)}
onClick={() => setActivePanel(tab)}
onClick={() => handleSetActivePanel(tab)}
>
<Icon className='mr-0' />
{window.innerWidth >= 500 ? label : ''}
@@ -0,0 +1,29 @@
import { ViewSettings } from '@/types/book';
import { EnvConfigType } from '@/services/environment';
import { useBookDataStore } from '@/store/bookDataStore';
import { useReaderStore } from '@/store/readerStore';
import { useSettingsStore } from '@/store/settingsStore';
export const saveViewSettings = async <K extends keyof ViewSettings>(
envConfig: EnvConfigType,
bookKey: string,
key: K,
value: ViewSettings[K],
skipGlobal?: boolean,
) => {
const { settings, isFontLayoutSettingsGlobal, setSettings, saveSettings } =
useSettingsStore.getState();
const { getViewSettings, setViewSettings } = useReaderStore.getState();
const { getConfig, saveConfig } = useBookDataStore.getState();
const viewSettings = getViewSettings(bookKey)!;
const config = getConfig(bookKey)!;
viewSettings[key] = value;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal && !skipGlobal) {
settings.globalViewSettings[key] = value;
setSettings(settings);
}
await saveConfig(envConfig, bookKey, config, settings);
await saveSettings(envConfig, settings);
};
+2 -1
View File
@@ -2,6 +2,7 @@ import { useEffect } from 'react';
import { useThemeStore } from '@/store/themeStore';
import { useSettingsStore } from '@/store/settingsStore';
import { applyCustomTheme } from '@/styles/themes';
import { applyColorScheme } from '@/utils/style';
export const useTheme = () => {
const { settings } = useSettingsStore();
@@ -15,7 +16,7 @@ export const useTheme = () => {
useEffect(() => {
const colorScheme = isDarkMode ? 'dark' : 'light';
document.documentElement.style.setProperty('color-scheme', colorScheme);
document.documentElement.setAttribute('data-theme', `${themeColor}-${colorScheme}`);
applyColorScheme(document, isDarkMode);
}, [themeColor, isDarkMode]);
};
+3 -1
View File
@@ -71,8 +71,10 @@ export const DEFAULT_BOOK_LAYOUT: BookLayout = {
writingMode: 'auto',
vertical: false,
rtl: false,
doubleBorder: true,
doubleBorder: false,
borderColor: 'red',
showHeader: true,
showFooter: true,
};
export const DEFAULT_BOOK_STYLE: BookStyle = {
@@ -66,6 +66,7 @@ export const useBookDataStore = create<BookDataState>((set, get) => ({
const bookIndex = library.findIndex((b) => b.hash === bookKey.split('-')[0]);
if (bookIndex == -1) return;
const book = library.splice(bookIndex, 1)[0]!;
book.progress = config.progress;
book.updatedAt = Date.now();
library.unshift(book);
setLibrary(library);
+1 -1
View File
@@ -222,7 +222,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
const viewState = state.viewStates[key];
if (!viewState || !bookData) return state;
const progress: [number, number] = [pageinfo.current, pageinfo.total];
const progress: [number, number] = [(pageinfo.next ?? pageinfo.current) + 1, pageinfo.total];
// Update library book progress
const { library, setLibrary } = useLibraryStore.getState();
+2
View File
@@ -10,6 +10,7 @@ interface ThemeState {
systemIsDarkMode: boolean;
themeCode: ThemeCode;
isDarkMode: boolean;
getIsDarkMode: () => boolean;
setThemeMode: (mode: ThemeMode) => void;
setThemeColor: (color: string) => void;
updateAppTheme: (color: keyof Palette) => void;
@@ -61,6 +62,7 @@ export const useThemeStore = create<ThemeState>((set, get) => {
systemIsDarkMode,
isDarkMode,
themeCode,
getIsDarkMode: () => get().isDarkMode,
setThemeMode: (mode) => {
if (typeof window !== 'undefined' && localStorage) {
localStorage.setItem('themeMode', mode);
-8
View File
@@ -191,14 +191,6 @@ foliate-view {
@apply hover:bg-base-100/50;
}
.config-item-top {
@apply rounded-t-2xl;
}
.config-item-bottom {
@apply rounded-b-2xl;
}
.collapse-arrow > .collapse-title:after {
top: var(--top-override, default-value);
inset-inline-end: var(--end-override, default-value);
+4 -2
View File
@@ -24,7 +24,7 @@ export interface Book {
downloadedAt?: number | null;
lastUpdated?: number; // deprecated in favor of updatedAt
progress?: [number, number]; // Add progress field: [current, total]
progress?: [number, number]; // Add progress field: [current, total], 1-based page number
}
export interface BookGroupType {
@@ -70,6 +70,8 @@ export interface BookLayout {
rtl: boolean;
doubleBorder: boolean;
borderColor: string;
showHeader: boolean;
showFooter: boolean;
}
export interface BookStyle {
@@ -147,7 +149,7 @@ export interface BookSearchResult {
export interface BookConfig {
bookHash?: string;
progress?: [number, number];
progress?: [number, number]; // [current pagenum, total pagenum], 1-based page number
location?: string;
booknotes?: BookNote[];
searchConfig?: Partial<BookSearchConfig>;
+1
View File
@@ -46,6 +46,7 @@ export interface FoliateView extends HTMLElement {
prev: () => Promise<void>;
goTo?: (params: { index: number; anchor: number }) => void;
setStyles?: (css: string) => void;
getContents: () => { doc: Document; index?: number }[];
addEventListener: (type: string, listener: EventListener) => void;
removeEventListener: (type: string, listener: EventListener) => void;
};
-1
View File
@@ -18,7 +18,6 @@ export const getSysFontsList = async (): Promise<string[]> => {
if (FONT_ENUM_SUPPORTED_OS_PLATFORMS.includes(osPlatform)) {
const fonts = await invoke<string[]>('list_fonts');
cachedSysFonts = [...new Set(fonts.filter((font) => !isSymbolicFontName(font)))].sort();
console.log('Fetched font list:', cachedSysFonts);
return cachedSysFonts;
} else {
console.warn(`Unsupported platform: ${osPlatform}`);
+8 -1
View File
@@ -254,7 +254,9 @@ export const getFootnoteStyles = () => `
}
p, li, blockquote, dd {
text-indent: 0;
padding: 1em !important;
margin: 0 !important;
text-indent: 0 !important;
}
`;
@@ -359,3 +361,8 @@ export const mountAdditionalFonts = (document: Document) => {
style.textContent = getAdditionalFontFaces();
document.head.appendChild(style);
};
export const applyColorScheme = (document: Document, isDarkMode: boolean) => {
const colorScheme = isDarkMode ? 'dark' : 'light';
document.documentElement.style.setProperty('color-scheme', colorScheme);
};