forked from akai/readest
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 948303c43a | |||
| 82f303fcfe | |||
| 359fa8df35 | |||
| 72e6c058f5 | |||
| 7648aa47b2 | |||
| 48074f0f53 | |||
| 66bd8d74f3 | |||
| 37f3c8b829 | |||
| 8775f90bc1 |
@@ -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,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": "顯示頁腳"
|
||||
}
|
||||
|
||||
@@ -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,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]);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: c50960743b...b4a0bbca87
Reference in New Issue
Block a user