Compare commits

...

5 Commits

Author SHA1 Message Date
Huang Xin 98870d9f3e release: version 0.9.59 (#1429) 2025-06-19 17:10:14 +02:00
Huang Xin 31aeb2289c settings: more reasonable constraints for top and bottom margins (#1428) 2025-06-19 17:01:20 +02:00
Huang Xin 0f66ecbba1 layout: remove unnecessary inset for header on landscape screen in iOS (#1425) 2025-06-19 12:01:42 +02:00
Huang Xin f0d3c9dd4e fix: transform vw/vh to pixels correctly, closes #1420 (#1421)
* i18n: update translations

* fix: transform vw/vh to pixels correctly, closes #1420
2025-06-19 10:53:58 +02:00
Huang Xin 92aae340d9 settings: unset deprecated margin settings (#1419) 2025-06-18 18:56:41 +02:00
36 changed files with 182 additions and 89 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.58",
"version": "0.9.59",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -264,7 +264,7 @@
"Clear Search": "مسح البحث",
"Header & Footer": "الترويسة والتذييل",
"Apply also in Scrolled Mode": "تطبيق أيضًا في وضع التمرير",
"A new version of Readest is Available!": "يتوفر إصدار جديد من Readest!",
"A new version of Readest is available!": "يتوفر إصدار جديد من Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "إصدار {{newVersion}} من ريديست متاح للتنزيل (الإصدار المثبت حاليًا {{currentVersion}}).",
"Download and install now?": "هل ترغب في تنزيله وتثبيته الآن؟",
"Downloading {{downloaded}} of {{contentLength}}": "جارٍ تنزيل {{downloaded}} من {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "Suche löschen",
"Header & Footer": "Kopf- & Fußzeile",
"Apply also in Scrolled Mode": "Auch im Scroll-Modus anwenden",
"A new version of Readest is Available!": "Eine neue Version von Readest ist verfügbar!",
"A new version of Readest is available!": "Eine neue Version von Readest ist verfügbar!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} ist verfügbar (installierte Version {{currentVersion}}).",
"Download and install now?": "Jetzt herunterladen und installieren?",
"Downloading {{downloaded}} of {{contentLength}}": "Herunterladen {{downloaded}} von {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "Καθαρισμός αναζήτησης",
"Header & Footer": "Κεφαλίδα & Υποσέλιδο",
"Apply also in Scrolled Mode": "Εφαρμογή και σε λειτουργία κύλισης",
"A new version of Readest is Available!": "Μια νέα έκδοση του Readest είναι διαθέσιμη!",
"A new version of Readest is available!": "Μια νέα έκδοση του Readest είναι διαθέσιμη!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Η έκδοση Readest {{newVersion}} είναι διαθέσιμη (έκδοση που είναι εγκατεστημένη {{currentVersion}}).",
"Download and install now?": "Θέλετε να την κατεβάσετε και να την εγκαταστήσετε τώρα;",
"Downloading {{downloaded}} of {{contentLength}}": "Κατεβάζω {{downloaded}} από {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "Limpiar búsqueda",
"Header & Footer": "Encabezado y pie de página",
"Apply also in Scrolled Mode": "Aplicar también en modo desplazamiento",
"A new version of Readest is Available!": "¡Una nueva versión de Readest está disponible!",
"A new version of Readest is available!": "¡Una nueva versión de Readest está disponible!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} está disponible (versión instalada {{currentVersion}}).",
"Download and install now?": "Descargar e instalar ahora?",
"Downloading {{downloaded}} of {{contentLength}}": "Descargando {{downloaded}} de {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "Effacer la recherche",
"Header & Footer": "En-tête et pied de page",
"Apply also in Scrolled Mode": "Appliquer également en mode défilement",
"A new version of Readest is Available!": "Avis de mise à jour",
"A new version of Readest is available!": "Avis de mise à jour",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} est disponible (version installée {{currentVersion}}).",
"Download and install now?": "Télécharger et installer maintenant ?",
"Downloading {{downloaded}} of {{contentLength}}": "Téléchargement {{downloaded}} sur {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "खोज साफ करें",
"Header & Footer": "हेडर और फ़ुटर",
"Apply also in Scrolled Mode": "स्क्रॉल मोड में भी लागू करें",
"A new version of Readest is Available!": "Readest का एक नया संस्करण उपलब्ध है!",
"A new version of Readest is available!": "Readest का एक नया संस्करण उपलब्ध है!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} उपलब्ध है (स्थापित संस्करण {{currentVersion}})।",
"Download and install now?": "अब डाउनलोड और इंस्टॉल करें?",
"Downloading {{downloaded}} of {{contentLength}}": "डाउनलोड कर रहा है {{downloaded}} का {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "Hapus Pencarian",
"Header & Footer": "Header & Footer",
"Apply also in Scrolled Mode": "Terapkan juga di Mode Gulir",
"A new version of Readest is Available!": "Versi baru Readest Tersedia!",
"A new version of Readest is available!": "Versi baru Readest Tersedia!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} tersedia (versi terinstal {{currentVersion}}).",
"Download and install now?": "Unduh dan instal sekarang?",
"Downloading {{downloaded}} of {{contentLength}}": "Mengunduh {{downloaded}} dari {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "Cancella ricerca",
"Header & Footer": "Intestazione e piè di pagina",
"Apply also in Scrolled Mode": "Applica anche in modalità scorrimento",
"A new version of Readest is Available!": "È disponibile una nuova versione di Readest!",
"A new version of Readest is available!": "È disponibile una nuova versione di Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} è disponibile (versione installata {{currentVersion}}).",
"Download and install now?": "Scarica e installa ora?",
"Downloading {{downloaded}} of {{contentLength}}": "Scaricando {{downloaded}} di {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "検索をクリア",
"Header & Footer": "ヘッダーとフッター",
"Apply also in Scrolled Mode": "スクロールモードでも適用",
"A new version of Readest is Available!": "新しいバージョンのReadestが利用可能です!",
"A new version of Readest is available!": "新しいバージョンのReadestが利用可能です!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}}が利用可能です(インストールされたバージョン{{currentVersion}})。",
"Download and install now?": "今すぐダウンロードしてインストールしますか?",
"Downloading {{downloaded}} of {{contentLength}}": "ダウンロード中 {{downloaded}} / {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "검색 지우기",
"Header & Footer": "헤더 및 푸터",
"Apply also in Scrolled Mode": "스크롤 모드에서도 적용",
"A new version of Readest is Available!": "Readest의 새 버전이 있습니다!",
"A new version of Readest is available!": "Readest의 새 버전이 있습니다!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}}가 사용 가능 (설치된 버전 {{currentVersion}}).",
"Download and install now?": "지금 다운로드하고 설치하시겠습니까?",
"Downloading {{downloaded}} of {{contentLength}}": "다운로드 중 {{downloaded}} / {{contentLength}}",
@@ -275,7 +275,7 @@
"Description:": "Beschrijving:",
"No description available": "Geen beschrijving beschikbaar",
"Drop to Import Books": "Sleep om boeken te importeren",
"A new version of Readest is Available!": "Een nieuwe versie van Readest is beschikbaar!",
"A new version of Readest is available!": "Een nieuwe versie van Readest is beschikbaar!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} is beschikbaar (geïnstalleerde versie {{currentVersion}}).",
"Download and install now?": "Nu downloaden en installeren?",
"Downloading {{downloaded}} of {{contentLength}}": "Downloaden {{downloaded}} van {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "Wyczyść wyszukiwanie",
"Header & Footer": "Nagłówek i stopka",
"Apply also in Scrolled Mode": "Zastosuj również w trybie przewijania",
"A new version of Readest is Available!": "Nowa wersja Readest jest dostępna!",
"A new version of Readest is available!": "Nowa wersja Readest jest dostępna!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} jest dostępny (zainstalowana wersja {{currentVersion}}).",
"Download and install now?": "Pobierz i zainstaluj teraz?",
"Downloading {{downloaded}} of {{contentLength}}": "Pobieranie {{downloaded}} z {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "Limpar Pesquisa",
"Header & Footer": "Cabeçalho e Rodapé",
"Apply also in Scrolled Mode": "Aplicar também no Modo Rolado",
"A new version of Readest is Available!": "Uma nova versão do Readest está disponível!",
"A new version of Readest is available!": "Uma nova versão do Readest está disponível!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} está disponível (versão instalada {{currentVersion}}).",
"Download and install now?": "Baixar e instalar agora?",
"Downloading {{downloaded}} of {{contentLength}}": "Baixando {{downloaded}} de {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "Очистить поиск",
"Header & Footer": "Заголовок и нижний колонтитул",
"Apply also in Scrolled Mode": "Применить также в прокручиваемом режиме",
"A new version of Readest is Available!": "Доступна новая версия Readest!",
"A new version of Readest is available!": "Доступна новая версия Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступен (установленная версия {{currentVersion}}).",
"Download and install now?": "Скачать и установить сейчас?",
"Downloading {{downloaded}} of {{contentLength}}": "Скачано {{downloaded}} из {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "Aramayı Temizle",
"Header & Footer": "Başlık ve Altbilgi",
"Apply also in Scrolled Mode": "Kaydırılmış Modda da Uygula",
"A new version of Readest is Available!": "Yeni bir Readest sürümü mevcut!",
"A new version of Readest is available!": "Yeni bir Readest sürümü mevcut!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} mevcut (yüklü sürüm {{currentVersion}}).",
"Download and install now?": "Şimdi indirip yükleyelim mi?",
"Downloading {{downloaded}} of {{contentLength}}": "İndiriliyor {{downloaded}}/{{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "Очистити пошук",
"Header & Footer": "Заголовок і нижній колонтитул",
"Apply also in Scrolled Mode": "Застосувати також у прокрученому режимі",
"A new version of Readest is Available!": "Доступна нова версія Readest!",
"A new version of Readest is available!": "Доступна нова версія Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступна (встановлена версія {{currentVersion}}).",
"Download and install now?": "Завантажити та встановити зараз?",
"Downloading {{downloaded}} of {{contentLength}}": "Завантаження {{downloaded}} з {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "Xóa tìm kiếm",
"Header & Footer": "Đầu trang & Chân trang",
"Apply also in Scrolled Mode": "Áp dụng cũng trong chế độ cuộn",
"A new version of Readest is Available!": "Đã có phiên bản mới của Readest!",
"A new version of Readest is available!": "Đã có phiên bản mới của Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} đã có sẵn (phiên bản đã cài đặt {{currentVersion}}).",
"Download and install now?": "Tải xuống và cài đặt ngay bây giờ?",
"Downloading {{downloaded}} of {{contentLength}}": "Đang tải {{downloaded}} trong {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "清除搜索",
"Header & Footer": "页眉页脚",
"Apply also in Scrolled Mode": "应用到滚动模式",
"A new version of Readest is Available!": "有新版本的 Readest 可更新!",
"A new version of Readest is available!": "有新版本的 Readest 可更新!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} 可更新(已安装版本 {{currentVersion}})。",
"Download and install now?": "立即下载并安装?",
"Downloading {{downloaded}} of {{contentLength}}": "正在下载 {{downloaded}} / {{contentLength}}",
@@ -264,7 +264,7 @@
"Clear Search": "清除搜索",
"Header & Footer": "頁眉頁腳",
"Apply also in Scrolled Mode": "滾動模式同樣適用",
"A new version of Readest is Available!": "有新版本的 Readest 可更新!",
"A new version of Readest is available!": "有新版本的 Readest 可更新!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} 可更新(已安裝版本 {{currentVersion}})。",
"Download and install now?": "立即下載並安裝?",
"Downloading {{downloaded}} of {{contentLength}}": "正在下載 {{downloaded}} / {{contentLength}}",
+8
View File
@@ -1,5 +1,13 @@
{
"releases": {
"0.9.59": {
"date": "2025-06-19",
"notes": [
"Layout: Image size now adjusts more accurately by converting screen units to pixels properly",
"Layout: Improved header layout on iOS landscape mode — extra spacing has been removed",
"Settings: Top and bottom margin settings are now more intuitive and better constrained"
]
},
"0.9.58": {
"date": "2025-06-18",
"notes": [
@@ -29,7 +29,8 @@ interface BooksGridProps {
const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
const { appService } = useEnv();
const { getConfig, getBookData } = useBookDataStore();
const { getProgress, getViewState, getViewSettings, hoveredBookKey } = useReaderStore();
const { getProgress, getViewState, getViewSettings } = useReaderStore();
const { getGridInsets, setGridInsets, hoveredBookKey } = useReaderStore();
const { isSideBarVisible, sideBarBookKey } = useSidebarStore();
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
@@ -45,6 +46,21 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sideBarBookKey]);
useEffect(() => {
if (!screenInsets) return;
bookKeys.forEach((bookKey, index) => {
const { top, right, bottom, left } = getInsetEdges(index, bookKeys.length, aspectRatio);
const gridInsets = {
top: top ? screenInsets.top : 0,
right: right ? screenInsets.right : 0,
bottom: bottom ? screenInsets.bottom : 0,
left: left ? screenInsets.left : 0,
};
setGridInsets(bookKey, gridInsets);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bookKeys, screenInsets]);
if (!screenInsets) return null;
return (
@@ -56,26 +72,18 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
}}
>
{bookKeys.map((bookKey, index) => {
const { top, right, bottom, left } = getInsetEdges(index, bookKeys.length, aspectRatio);
const bookData = getBookData(bookKey);
const config = getConfig(bookKey);
const progress = getProgress(bookKey);
const viewSettings = getViewSettings(bookKey);
const gridInsets = getGridInsets(bookKey);
const { book, bookDoc } = bookData || {};
if (!book || !config || !bookDoc || !viewSettings) return null;
if (!book || !config || !bookDoc || !viewSettings || !gridInsets) return null;
const { section, pageinfo, timeinfo, sectionLabel } = progress || {};
const isBookmarked = getViewState(bookKey)?.ribbonVisible;
const horizontalGapPercent = viewSettings.gapPercent;
const viewInsets = getViewInsets(viewSettings);
// insets for the grid cell
const gridInsets = {
top: top ? screenInsets.top : 0,
right: right ? screenInsets.right : 0,
bottom: bottom ? screenInsets.bottom : 0,
left: left ? screenInsets.left : 0,
};
// insets for the content inside the grid cell
const contentInsets = {
top: gridInsets.top + viewInsets.top,
right: gridInsets.right + viewInsets.right,
@@ -104,12 +112,13 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
isHoveredAnim={bookKeys.length > 2}
onCloseBook={onCloseBook}
onSetSettingsDialogOpen={setFontLayoutSettingsDialogOpen}
gridInsets={gridInsets}
/>
<FoliateViewer
bookKey={bookKey}
bookDoc={bookDoc}
config={config}
insets={contentInsets}
contentInsets={contentInsets}
/>
{viewSettings.vertical && viewSettings.scrolled && (
<>
@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { BookDoc, getDirection } from '@/libs/document';
import { BookConfig } from '@/types/book';
import { FoliateView, wrappedFoliateView } from '@/types/view';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
@@ -38,8 +39,8 @@ const FoliateViewer: React.FC<{
bookKey: string;
bookDoc: BookDoc;
config: BookConfig;
insets: { top: number; right: number; bottom: number; left: number };
}> = ({ bookKey, bookDoc, config, insets }) => {
contentInsets: Insets;
}> = ({ bookKey, bookDoc, config, contentInsets: insets }) => {
const { getView, setView: setFoliateView, setProgress } = useReaderStore();
const { getViewSettings, setViewSettings } = useReaderStore();
const { getParallels } = useParallelViewStore();
@@ -83,7 +84,7 @@ const FoliateViewer: React.FC<{
.then((data) => {
const viewSettings = getViewSettings(bookKey);
if (viewSettings && detail.type === 'text/css')
return transformStylesheet(viewSettings, width, height, data);
return transformStylesheet(width, height, data);
if (viewSettings && detail.type === 'application/xhtml+xml') {
const ctx = {
bookKey,
@@ -206,9 +207,10 @@ const FoliateViewer: React.FC<{
detail.allowScript = viewSettings.allowScript ?? false;
}
});
const containerRect = containerRef.current?.getBoundingClientRect();
const width = containerRect?.width || window.innerWidth;
const height = containerRect?.height || window.innerHeight;
const viewWidth = appService?.isMobile ? screen.width : window.innerWidth;
const viewHeight = appService?.isMobile ? screen.height : window.innerHeight;
const width = viewWidth - insets.left - insets.right;
const height = viewHeight - insets.top - insets.bottom;
book.transformTarget?.addEventListener('data', getDocTransformHandler({ width, height }));
view.renderer.setStyles?.(getStyles(viewSettings));
@@ -247,11 +249,16 @@ const FoliateViewer: React.FC<{
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
const showDoubleBorderHeader = showDoubleBorder && viewSettings.showHeader;
const showDoubleBorderFooter = showDoubleBorder && viewSettings.showFooter;
const showTopHeader = viewSettings.showHeader && !viewSettings.vertical;
const showBottomFooter = viewSettings.showFooter && !viewSettings.vertical;
const moreTopInset = showTopHeader ? Math.max(0, 44 - insets.top) : 0;
const moreBottomInset = showBottomFooter ? Math.max(0, 44 - insets.bottom) : 0;
const moreRightInset = showDoubleBorderHeader ? 32 : 0;
const moreLeftInset = showDoubleBorderFooter ? 32 : 0;
viewRef.current?.renderer.setAttribute('margin-top', `${insets.top}px`);
viewRef.current?.renderer.setAttribute('margin-top', `${insets.top + moreTopInset}px`);
viewRef.current?.renderer.setAttribute('margin-right', `${insets.right + moreRightInset}px`);
viewRef.current?.renderer.setAttribute('margin-bottom', `${insets.bottom}px`);
viewRef.current?.renderer.setAttribute('margin-bottom', `${insets.bottom + moreBottomInset}px`);
viewRef.current?.renderer.setAttribute('margin-left', `${insets.left + moreLeftInset}px`);
viewRef.current?.renderer.setAttribute('gap', `${viewSettings.gapPercent}%`);
if (viewSettings.scrolled) {
@@ -2,6 +2,7 @@ import clsx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
@@ -22,6 +23,7 @@ interface HeaderBarProps {
bookTitle: string;
isTopLeft: boolean;
isHoveredAnim: boolean;
gridInsets: Insets;
onCloseBook: (bookKey: string) => void;
onSetSettingsDialogOpen: (open: boolean) => void;
}
@@ -31,6 +33,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
bookTitle,
isTopLeft,
isHoveredAnim,
gridInsets,
onCloseBook,
onSetSettingsDialogOpen,
}) => {
@@ -77,10 +80,10 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
return (
<div
className={clsx(
'bg-base-100 absolute top-0 w-full',
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
)}
className={clsx('bg-base-100 absolute top-0 w-full')}
style={{
paddingTop: appService?.hasSafeAreaInset ? `${gridInsets.top}px` : '0px',
}}
>
<div
className={clsx('absolute top-0 z-10 hidden h-11 w-full sm:flex')}
@@ -89,11 +92,11 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
/>
<div
className={clsx(
'bg-base-100 absolute left-0 right-0 top-0 z-10 h-[env(safe-area-inset-top)]',
'bg-base-100 absolute left-0 right-0 top-0 z-10',
isVisible ? 'visible' : 'hidden',
)}
style={{
height: systemUIVisible ? `max(env(safe-area-inset-top), ${statusBarHeight}px)` : '',
height: systemUIVisible ? `${Math.max(gridInsets.top, statusBarHeight)}px` : '0px',
}}
/>
<div
@@ -110,8 +113,8 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
)}
style={{
marginTop: systemUIVisible
? `max(env(safe-area-inset-top), ${statusBarHeight}px)`
: 'env(safe-area-inset-top)',
? `${Math.max(gridInsets.top, statusBarHeight)}px`
: `${gridInsets.top}px`,
}}
onMouseLeave={() => !appService?.isMobile && setHoveredBookKey('')}
>
@@ -1,6 +1,8 @@
import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { eventDispatcher } from '@/utils/event';
interface SectionInfoProps {
@@ -22,6 +24,13 @@ const HintInfo: React.FC<SectionInfoProps> = ({
contentInsets,
gridInsets,
}) => {
const { appService } = useEnv();
const { systemUIVisible, statusBarHeight } = useThemeStore();
const topInset = Math.max(
gridInsets.top,
appService?.isAndroidApp && systemUIVisible ? statusBarHeight / 2 : 0,
);
const [hintMessage, setHintMessage] = React.useState<string | null>(null);
const hintTimeout = useRef(2000);
const dismissTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -57,7 +66,7 @@ const HintInfo: React.FC<SectionInfoProps> = ({
hintMessage ? 'bg-base-100' : 'bg-transparent',
)}
style={{
height: `${gridInsets.top}px`,
height: `${topInset}px`,
}}
/>
<div
@@ -83,7 +92,7 @@ const HintInfo: React.FC<SectionInfoProps> = ({
width: showDoubleBorder ? '30px' : `${horizontalGap}%`,
}
: {
top: `${gridInsets.top}px`,
top: `${topInset}px`,
insetInlineEnd: `calc(${horizontalGap / 2}% + ${contentInsets.right}px)`,
}
}
@@ -33,7 +33,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
const { settings, setSettings } = useSettingsStore();
const { isSideBarVisible, setSideBarVisible } = useSidebarStore();
const { isNotebookVisible, setNotebookVisible } = useNotebookStore();
const { isDarkMode, showSystemUI, dismissSystemUI } = useThemeStore();
const { isDarkMode, systemUIAlwaysHidden, showSystemUI, dismissSystemUI } = useThemeStore();
const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore();
const [libraryLoaded, setLibraryLoaded] = useState(false);
const isInitiating = useRef(false);
@@ -96,8 +96,9 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
useEffect(() => {
if (!appService?.isMobileApp) return;
const systemUIVisible = !!hoveredBookKey || settings.alwaysShowStatusBar;
setSystemUIVisibility({ visible: systemUIVisible, darkMode: isDarkMode });
if (systemUIVisible) {
const visible = systemUIVisible && !systemUIAlwaysHidden;
setSystemUIVisibility({ visible, darkMode: isDarkMode });
if (visible) {
showSystemUI();
} else {
dismissSystemUI();
@@ -1,6 +1,8 @@
import clsx from 'clsx';
import React from 'react';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
interface SectionInfoProps {
section?: string;
@@ -21,6 +23,13 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
contentInsets,
gridInsets,
}) => {
const { appService } = useEnv();
const { systemUIVisible, statusBarHeight } = useThemeStore();
const topInset = Math.max(
gridInsets.top,
appService?.isAndroidApp && systemUIVisible ? statusBarHeight / 2 : 0,
);
return (
<>
<div
@@ -29,7 +38,7 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
isScrolled && !isVertical && 'bg-base-100',
)}
style={{
height: `${gridInsets.top}px`,
height: `${topInset}px`,
}}
/>
<div
@@ -49,7 +58,7 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
height: `calc(100% - ${contentInsets.top + contentInsets.bottom}px)`,
}
: {
top: `${gridInsets.top}px`,
top: `${topInset}px`,
insetInlineStart: `calc(${horizontalGap / 2}% + ${contentInsets.left}px)`,
width: `calc(100% - ${contentInsets.left + contentInsets.right}px)`,
}
@@ -18,11 +18,12 @@ import NumberInput from './NumberInput';
const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const _ = useTranslation();
const { envConfig } = useEnv();
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const { getView, getViewSettings, getGridInsets, setViewSettings } = useReaderStore();
const { getBookData } = useBookDataStore();
const view = getView(bookKey);
const bookData = getBookData(bookKey)!;
const viewSettings = getViewSettings(bookKey)!;
const gridInsets = getGridInsets(bookKey)!;
const [paragraphMargin, setParagraphMargin] = useState(viewSettings.paragraphMargin!);
const [lineHeight, setLineHeight] = useState(viewSettings.lineHeight!);
@@ -98,42 +99,54 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
useEffect(() => {
if (marginTopPx === viewSettings.marginTopPx) return;
viewSettings.marginPx = undefined;
if (viewSettings.marginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'marginTopPx', marginTopPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [marginTopPx]);
useEffect(() => {
if (marginBottomPx === viewSettings.marginBottomPx) return;
viewSettings.marginPx = undefined;
if (viewSettings.marginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'marginBottomPx', marginBottomPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [marginBottomPx]);
useEffect(() => {
if (marginRightPx === viewSettings.marginRightPx) return;
viewSettings.marginPx = undefined;
if (viewSettings.marginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'marginRightPx', marginRightPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [marginRightPx]);
useEffect(() => {
if (marginLeftPx === viewSettings.marginLeftPx) return;
viewSettings.marginPx = undefined;
if (viewSettings.marginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'marginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'marginLeftPx', marginLeftPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [marginLeftPx]);
useEffect(() => {
if (compactMarginTopPx === viewSettings.compactMarginTopPx) return;
viewSettings.compactMarginPx = undefined;
if (viewSettings.compactMarginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'compactMarginTopPx', compactMarginTopPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compactMarginTopPx]);
useEffect(() => {
if (compactMarginBottomPx === viewSettings.compactMarginBottomPx) return;
viewSettings.compactMarginPx = undefined;
if (viewSettings.compactMarginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
}
saveViewSettings(
envConfig,
bookKey,
@@ -147,7 +160,9 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
useEffect(() => {
if (compactMarginRightPx === viewSettings.compactMarginRightPx) return;
viewSettings.compactMarginPx = undefined;
if (viewSettings.compactMarginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
}
saveViewSettings(
envConfig,
bookKey,
@@ -161,7 +176,9 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
useEffect(() => {
if (compactMarginLeftPx === viewSettings.compactMarginLeftPx) return;
viewSettings.compactMarginPx = undefined;
if (viewSettings.compactMarginPx !== undefined) {
saveViewSettings(envConfig, bookKey, 'compactMarginPx', undefined, false, false);
}
saveViewSettings(envConfig, bookKey, 'compactMarginLeftPx', compactMarginLeftPx, false, false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [compactMarginLeftPx]);
@@ -257,7 +274,8 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
useEffect(() => {
if (showHeader === viewSettings.showHeader) return;
if (showHeader && !viewSettings.vertical) {
viewSettings.marginTopPx = Math.max(viewSettings.marginTopPx, 44);
const minMarginTop = Math.max(0, Math.round((44 - gridInsets.top) / 4) * 4);
viewSettings.marginTopPx = Math.max(viewSettings.marginTopPx, minMarginTop);
setMarginTopPx(viewSettings.marginTopPx);
setViewSettings(bookKey, viewSettings);
}
@@ -269,7 +287,8 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
useEffect(() => {
if (showFooter === viewSettings.showFooter) return;
if (showFooter && !viewSettings.vertical) {
viewSettings.marginBottomPx = Math.max(viewSettings.marginBottomPx, 44);
const minMarginBottom = Math.max(0, Math.round((44 - gridInsets.bottom) / 4) * 4);
viewSettings.marginBottomPx = Math.max(viewSettings.marginBottomPx, minMarginBottom);
setMarginBottomPx(viewSettings.marginBottomPx);
setViewSettings(bookKey, viewSettings);
}
@@ -446,7 +465,11 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
label={_('Top Margin (px)')}
value={showHeader && !isVertical ? marginTopPx : compactMarginTopPx}
onChange={showHeader && !isVertical ? setMarginTopPx : setCompactMarginTopPx}
min={0}
min={
showHeader && !isVertical
? Math.max(0, Math.round((44 - gridInsets.top) / 4) * 4)
: 0
}
max={88}
step={4}
/>
@@ -454,7 +477,11 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
label={_('Bottom Margin (px)')}
value={showFooter && !isVertical ? marginBottomPx : compactMarginBottomPx}
onChange={showFooter && !isVertical ? setMarginBottomPx : setCompactMarginBottomPx}
min={0}
min={
showFooter && !isVertical
? Math.max(0, Math.round((44 - gridInsets.bottom) / 4) * 4)
: 0
}
max={88}
step={4}
/>
@@ -133,7 +133,9 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
>
<div className='flex cursor-default items-center justify-between'>
<div className='flex items-center'>
<span className='text-gray-500'>{dayjs(item.createdAt).fromNow()}</span>
<span className='text-sm text-gray-500 sm:text-xs'>
{dayjs(item.createdAt).fromNow()}
</span>
</div>
<div className='flex items-center justify-end space-x-3 p-2' dir='ltr'>
{item.note && (
+1 -1
View File
@@ -192,7 +192,7 @@ const Dialog: React.FC<DialogProps> = ({
>
<div className='bg-base-content/50 h-1 w-10 rounded-full'></div>
</div>
<div className='dialog-header bg-base-100 sticky top-1 z-10 flex items-center justify-between px-4'>
<div className='dialog-header bg-base-100 sticky top-1 z-10 flex items-center justify-between px-2 sm:px-4'>
{header ? (
header
) : (
@@ -274,7 +274,7 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
<div className='text-base-content flex-grow text-sm'>
<h2 className='mb-4 text-center font-bold sm:text-start'>
{_('A new version of Readest is Available!')}
{_('A new version of Readest is available!')}
</h2>
<p className='mb-2'>
{_('Readest {{newVersion}} is available (installed version {{currentVersion}}).', {
+8 -7
View File
@@ -24,6 +24,8 @@ export const useTheme = ({
dismissSystemUI,
updateAppTheme,
setStatusBarHeight,
systemUIAlwaysHidden,
setSystemUIAlwaysHidden,
} = useThemeStore();
useEffect(() => {
@@ -41,12 +43,7 @@ export const useTheme = ({
const handleSystemUIVisibility = useCallback(() => {
if (!appService?.isMobileApp) return;
// This is a workaround for iPhone apps where the system UI is not visible in landscape mode
// when the app is in fullscreen mode until we find a better solution to override the prefersStatusBarHidden
// in the ViewController.
const isIPhoneApp = appService.isIOSApp && getOSPlatform() === 'ios';
const systemUINeverVisible = isIPhoneApp && screen.orientation.type.includes('landscape');
const visible = systemUIVisible && !systemUINeverVisible;
const visible = systemUIVisible && !systemUIAlwaysHidden;
if (visible) {
showSystemUI();
} else {
@@ -73,8 +70,12 @@ export const useTheme = ({
}
};
const handleOrientationChange = () => {
// Only handle orientation change for iPhone apps
if (appService?.isIOSApp && getOSPlatform() === 'ios') {
// FIXME: This is a workaround for iPhone apps where the system UI is not visible in landscape mode
// when the app is in fullscreen mode until we find a better solution to override the prefersStatusBarHidden
// in the ViewController. Note that screen.orientation.type is not abailable in iOS before 16.4.
const systemUIAlwaysHidden = screen.orientation?.type.includes('landscape');
setSystemUIAlwaysHidden(systemUIAlwaysHidden);
handleSystemUIVisibility();
}
};
+2 -1
View File
@@ -89,7 +89,7 @@ export const DEFAULT_BOOK_FONT: BookFont = {
export const DEFAULT_BOOK_LAYOUT: BookLayout = {
marginTopPx: 44,
marginBottomPx: 16,
marginBottomPx: 44,
marginLeftPx: 16,
marginRightPx: 16,
compactMarginTopPx: 16,
@@ -136,6 +136,7 @@ export const DEFAULT_MOBILE_VIEW_SETTINGS: Partial<ViewSettings> = {
fullJustification: false,
animated: true,
defaultFont: 'Sans-serif',
marginBottomPx: 16,
};
export const DEFAULT_CJK_VIEW_SETTINGS: Partial<ViewSettings> = {
+19
View File
@@ -8,6 +8,7 @@ import {
ViewSettings,
TimeInfo,
} from '@/types/book';
import { Insets } from '@/types/misc';
import { EnvConfigType } from '@/services/environment';
import { FoliateView } from '@/types/view';
import { DocumentLoader, TOCItem } from '@/libs/document';
@@ -27,6 +28,7 @@ interface ViewState {
progress: BookProgress | null;
ribbonVisible: boolean;
ttsEnabled: boolean;
gridInsets: Insets | null;
/* View settings for the view:
generally view settings have a hierarchy of global settings < book settings < view settings
view settings for primary view are saved to book config which is persisted to config file
@@ -67,6 +69,8 @@ interface ReaderStore {
) => Promise<void>;
clearViewState: (key: string) => void;
getViewState: (key: string) => ViewState | null;
getGridInsets: (key: string) => Insets | null;
setGridInsets: (key: string, insets: Insets | null) => void;
}
export const useReaderStore = create<ReaderStore>((set, get) => ({
@@ -114,6 +118,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
progress: null,
ribbonVisible: false,
ttsEnabled: false,
gridInsets: null,
viewSettings: null,
},
},
@@ -159,6 +164,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
progress: null,
ribbonVisible: false,
ttsEnabled: false,
gridInsets: null,
viewSettings: { ...globalViewSettings, ...configViewSettings },
},
},
@@ -178,6 +184,7 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
progress: null,
ribbonVisible: false,
ttsEnabled: false,
gridInsets: null,
viewSettings: null,
},
},
@@ -306,4 +313,16 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
},
},
})),
getGridInsets: (key: string) => get().viewStates[key]?.gridInsets || null,
setGridInsets: (key: string, insets: Insets | null) =>
set((state) => ({
viewStates: {
...state.viewStates,
[key]: {
...state.viewStates[key]!,
gridInsets: insets,
},
},
})),
}));
+4
View File
@@ -12,6 +12,8 @@ interface ThemeState {
isDarkMode: boolean;
systemUIVisible: boolean;
statusBarHeight: number;
systemUIAlwaysHidden: boolean;
setSystemUIAlwaysHidden: (hidden: boolean) => void;
setStatusBarHeight: (height: number) => void;
showSystemUI: () => void;
dismissSystemUI: () => void;
@@ -69,9 +71,11 @@ export const useThemeStore = create<ThemeState>((set, get) => {
themeCode,
systemUIVisible: false,
statusBarHeight: 24,
systemUIAlwaysHidden: false,
showSystemUI: () => set({ systemUIVisible: true }),
dismissSystemUI: () => set({ systemUIVisible: false }),
setStatusBarHeight: (height: number) => set({ statusBarHeight: height }),
setSystemUIAlwaysHidden: (hidden: boolean) => set({ systemUIAlwaysHidden: hidden }),
getIsDarkMode: () => get().isDarkMode,
setThemeMode: (mode) => {
if (typeof window !== 'undefined' && localStorage) {
+3 -10
View File
@@ -420,16 +420,9 @@ export const getStyles = (viewSettings: ViewSettings, themeCode?: ThemeCode) =>
return `${layoutStyles}\n${fontStyles}\n${colorStyles}\n${translationStyles}\n${userStylesheet}`;
};
export const transformStylesheet = (
viewSettings: ViewSettings,
width: number,
height: number,
css: string,
) => {
export const transformStylesheet = (vw: number, vh: number, css: string) => {
const isMobile = ['ios', 'android'].includes(getOSPlatform());
const fontScale = isMobile ? 1.25 : 1;
const w = width - viewSettings.marginLeftPx - viewSettings.marginRightPx;
const h = height - viewSettings.marginTopPx - viewSettings.marginBottomPx;
const ruleRegex = /([^{]+)({[^}]+})/g;
css = css.replace(ruleRegex, (match, selector, block) => {
const hasTextAlignCenter = /text-align\s*:\s*center\s*[;$]/.test(block);
@@ -467,8 +460,8 @@ export const transformStylesheet = (
const rem = parseFloat(pt) / fontScale / 12;
return `font-size: ${rem}rem`;
})
.replace(/(\d*\.?\d+)vw/gi, (_, d) => (parseFloat(d) * w) / 100 + 'px')
.replace(/(\d*\.?\d+)vh/gi, (_, d) => (parseFloat(d) * h) / 100 + 'px')
.replace(/(\d*\.?\d+)vw/gi, (_, d) => (parseFloat(d) * vw) / 100 + 'px')
.replace(/(\d*\.?\d+)vh/gi, (_, d) => (parseFloat(d) * vh) / 100 + 'px')
.replace(/[\s;]color\s*:\s*black/gi, 'color: var(--theme-fg-color)')
.replace(/[\s;]color\s*:\s*#000000/gi, 'color: var(--theme-fg-color)')
.replace(/[\s;]color\s*:\s*#000/gi, 'color: var(--theme-fg-color)')