mirror of
https://github.com/readest/readest.git
synced 2026-07-09 16:00:16 +00:00
fix(layout): show progress info on top of the page in scrolled mode (#5029)
* The progress bar interferring with scrolled text has been fixed. Toggling is working to remove individual elements on the bar. The progress bar isn't tappable it seems. The progress bar also is sfill getting obstructed when hovering over it by the footerbar, but if you are quick enough you * the hovering issue has been fixed with the progress bar * got the toggle for the footer progress bar to be for the whole bar but it seems to be overlapping with areas that you would tap to move forward and backward pages * refactor(reader): drop tap-to-toggle footer, keep it display-only Tapping to hide the footer had no dependable way back: the restore targets were invisible corner pads users could not discover, and the reveal also silently failed while auto-scroll or a text selection consumed the tap. Remove the option; hide the footer via Settings > Layout > Show Footer instead. - remove tapToToggleFooter from ViewConfig, defaults, and LayoutPanel - make ProgressBar display-only (pointer-events-none, no tap targets or restore pads) - revert the FooterBar hover zone to the 52px strip and delete useHoverIntent - keep the footerReservesBand obstruction fix and scrolled-mode pills - prune the orphaned "Tap to Toggle Footer" i18n key Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Huang Xin <chrox.huang@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
870a62e0e9
commit
9c4f9550b8
43 changed files with 291 additions and 274 deletions
|
|
@ -864,7 +864,6 @@
|
|||
"Show Results": "عرض النتائج",
|
||||
"Clear search": "مسح البحث",
|
||||
"Clear search history": "مسح سجل البحث",
|
||||
"Tap to Toggle Footer": "انقر لتبديل التذييل",
|
||||
"Show Current Time": "عرض الوقت الحالي",
|
||||
"Use 24 Hour Clock": "استخدام نظام 24 ساعة",
|
||||
"Show Current Battery Status": "عرض حالة البطارية الحالية",
|
||||
|
|
|
|||
|
|
@ -820,7 +820,6 @@
|
|||
"Show Results": "ফলাফল দেখুন",
|
||||
"Clear search": "অনুসন্ধান সাফ করুন",
|
||||
"Clear search history": "অনুসন্ধান ইতিহাস সাফ করুন",
|
||||
"Tap to Toggle Footer": "ফুটার টগল করতে আলতো চাপুন",
|
||||
"Show Current Time": "বর্তমান সময় দেখান",
|
||||
"Use 24 Hour Clock": "২৪ ঘণ্টার ঘড়ি ব্যবহার করুন",
|
||||
"Show Current Battery Status": "বর্তমান ব্যাটারি অবস্থা দেখান",
|
||||
|
|
|
|||
|
|
@ -809,7 +809,6 @@
|
|||
"Show Results": "འབྲས་བུ་སྟོན།",
|
||||
"Clear search": "འཚོལ་བཤེར་གཙང་སེལ།",
|
||||
"Clear search history": "འཚོལ་བཤེར་ལོ་རྒྱུས་གཙང་སེལ།",
|
||||
"Tap to Toggle Footer": "ཞབས་མཇུག་སྒོ་འབྱེད་བྱེད་པར་གནོན།",
|
||||
"Show Current Time": "ད་ལྟའི་དུས་ཚོད་སྟོན་པ།",
|
||||
"Use 24 Hour Clock": "ཆུ་ཚོད་ ༢༤ ཅན་གྱི་ཟློས་འཁོར་བཀོལ་བ།",
|
||||
"Show Current Battery Status": "ད་ལྟའི་གློག་རྫས་གནས་བབ་སྟོན་པ།",
|
||||
|
|
|
|||
|
|
@ -820,7 +820,6 @@
|
|||
"Show Results": "Ergebnisse anzeigen",
|
||||
"Clear search": "Suche löschen",
|
||||
"Clear search history": "Suchverlauf löschen",
|
||||
"Tap to Toggle Footer": "Tippen, um Fußzeile umzuschalten",
|
||||
"Show Current Time": "Aktuelle Uhrzeit anzeigen",
|
||||
"Use 24 Hour Clock": "24-Stunden-Format verwenden",
|
||||
"Show Current Battery Status": "Aktuellen Akkustand anzeigen",
|
||||
|
|
|
|||
|
|
@ -820,7 +820,6 @@
|
|||
"Show Results": "Εμφάνιση αποτελεσμάτων",
|
||||
"Clear search": "Εκκαθάριση αναζήτησης",
|
||||
"Clear search history": "Εκκαθάριση ιστορικού αναζήτησης",
|
||||
"Tap to Toggle Footer": "Πατήστε για εναλλαγή υποσέλιδου",
|
||||
"Show Current Time": "Εμφάνιση τρέχουσας ώρας",
|
||||
"Use 24 Hour Clock": "Χρήση 24ωρης μορφής ώρας",
|
||||
"Show Current Battery Status": "Εμφάνιση τρέχουσας κατάστασης μπαταρίας",
|
||||
|
|
|
|||
|
|
@ -831,7 +831,6 @@
|
|||
"Show Results": "Mostrar resultados",
|
||||
"Clear search": "Borrar búsqueda",
|
||||
"Clear search history": "Borrar historial de búsqueda",
|
||||
"Tap to Toggle Footer": "Toca para alternar pie de página",
|
||||
"Show Current Time": "Mostrar hora actual",
|
||||
"Use 24 Hour Clock": "Usar formato de 24 horas",
|
||||
"Show Current Battery Status": "Mostrar el estado actual de la batería",
|
||||
|
|
|
|||
|
|
@ -820,7 +820,6 @@
|
|||
"Show Results": "نمایش نتایج",
|
||||
"Clear search": "پاک کردن جستجو",
|
||||
"Clear search history": "پاک کردن تاریخچه جستجو",
|
||||
"Tap to Toggle Footer": "برای تغییر پاورقی ضربه بزنید",
|
||||
"Show Current Time": "نمایش زمان فعلی",
|
||||
"Use 24 Hour Clock": "استفاده از ساعت ۲۴ ساعته",
|
||||
"Show Current Battery Status": "نمایش وضعیت فعلی باتری",
|
||||
|
|
|
|||
|
|
@ -831,7 +831,6 @@
|
|||
"Show Results": "Afficher les résultats",
|
||||
"Clear search": "Effacer la recherche",
|
||||
"Clear search history": "Effacer l'historique de recherche",
|
||||
"Tap to Toggle Footer": "Appuyez pour afficher/masquer le pied de page",
|
||||
"Show Current Time": "Afficher l'heure actuelle",
|
||||
"Use 24 Hour Clock": "Utiliser le format 24 heures",
|
||||
"Show Current Battery Status": "Afficher l'état actuel de la batterie",
|
||||
|
|
|
|||
|
|
@ -948,7 +948,6 @@
|
|||
"Reading Progress Style": "סגנון התקדמות קריאה",
|
||||
"Page Number": "מספר דף",
|
||||
"Percentage": "אחוזים",
|
||||
"Tap to Toggle Footer": "הקש להצגת/הסתרת כותרת תחתונה",
|
||||
"Show Current Time": "הצגת השעה הנוכחית",
|
||||
"Use 24 Hour Clock": "השתמש בשעון 24 שעות",
|
||||
"Show Current Battery Status": "הצג מצב סוללה נוכחי",
|
||||
|
|
|
|||
|
|
@ -820,7 +820,6 @@
|
|||
"Show Results": "परिणाम दिखाएं",
|
||||
"Clear search": "खोज साफ़ करें",
|
||||
"Clear search history": "खोज इतिहास साफ़ करें",
|
||||
"Tap to Toggle Footer": "फुटर टॉगल करने के लिए टैप करें",
|
||||
"Show Current Time": "वर्तमान समय दिखाएं",
|
||||
"Use 24 Hour Clock": "24 घंटे की घड़ी का उपयोग करें",
|
||||
"Show Current Battery Status": "वर्तमान बैटरी स्थिति दिखाएं",
|
||||
|
|
|
|||
|
|
@ -1028,7 +1028,6 @@
|
|||
"Use 24 Hour Clock": "24 órás formátum használata",
|
||||
"Show Current Battery Status": "Akkumulátor állapotának megjelenítése",
|
||||
"Show Battery Percentage": "Akkumulátor százalék megjelenítése",
|
||||
"Tap to Toggle Footer": "Koppintás a lábléc váltásához",
|
||||
"Screen": "Képernyő",
|
||||
"Orientation": "Tájolás",
|
||||
"Portrait": "Álló",
|
||||
|
|
|
|||
|
|
@ -809,7 +809,6 @@
|
|||
"Show Results": "Tampilkan Hasil",
|
||||
"Clear search": "Hapus pencarian",
|
||||
"Clear search history": "Hapus riwayat pencarian",
|
||||
"Tap to Toggle Footer": "Ketuk untuk beralih footer",
|
||||
"Show Current Time": "Tampilkan Waktu Sekarang",
|
||||
"Use 24 Hour Clock": "Gunakan Format 24 Jam",
|
||||
"Show Current Battery Status": "Tampilkan Status Baterai Saat Ini",
|
||||
|
|
|
|||
|
|
@ -831,7 +831,6 @@
|
|||
"Show Results": "Mostra risultati",
|
||||
"Clear search": "Cancella ricerca",
|
||||
"Clear search history": "Cancella cronologia ricerche",
|
||||
"Tap to Toggle Footer": "Tocca per mostrare/nascondere il piè di pagina",
|
||||
"Show Current Time": "Mostra ora attuale",
|
||||
"Use 24 Hour Clock": "Usa formato 24 ore",
|
||||
"Show Current Battery Status": "Mostra lo stato attuale della batteria",
|
||||
|
|
|
|||
|
|
@ -809,7 +809,6 @@
|
|||
"Show Results": "結果を表示",
|
||||
"Clear search": "検索をクリア",
|
||||
"Clear search history": "検索履歴をクリア",
|
||||
"Tap to Toggle Footer": "タップでフッターを切り替え",
|
||||
"Show Current Time": "現在時刻を表示",
|
||||
"Use 24 Hour Clock": "24時間表示を使用",
|
||||
"Show Current Battery Status": "バッテリー残量を表示",
|
||||
|
|
|
|||
|
|
@ -809,7 +809,6 @@
|
|||
"Show Results": "결과 표시",
|
||||
"Clear search": "검색 지우기",
|
||||
"Clear search history": "검색 기록 지우기",
|
||||
"Tap to Toggle Footer": "탭하여 바닥글 전환",
|
||||
"Show Current Time": "현재 시간 표시",
|
||||
"Use 24 Hour Clock": "24시간제 사용",
|
||||
"Show Current Battery Status": "현재 배터리 상태 표시",
|
||||
|
|
|
|||
|
|
@ -809,7 +809,6 @@
|
|||
"Show Results": "Tunjukkan Hasil",
|
||||
"Clear search": "Kosongkan carian",
|
||||
"Clear search history": "Kosongkan sejarah carian",
|
||||
"Tap to Toggle Footer": "Ketik untuk togol pengaki",
|
||||
"Show Current Time": "Tunjukkan Waktu Sekarang",
|
||||
"Use 24 Hour Clock": "Gunakan Format 24 Jam",
|
||||
"Show Current Battery Status": "Paparkan Status Bateri Semasa",
|
||||
|
|
|
|||
|
|
@ -820,7 +820,6 @@
|
|||
"Show Results": "Resultaten tonen",
|
||||
"Clear search": "Zoekopdracht wissen",
|
||||
"Clear search history": "Zoekgeschiedenis wissen",
|
||||
"Tap to Toggle Footer": "Tik om voettekst te wisselen",
|
||||
"Show Current Time": "Huidige tijd weergeven",
|
||||
"Use 24 Hour Clock": "24-uurs klok gebruike",
|
||||
"Show Current Battery Status": "Toon huidige batterijstatus",
|
||||
|
|
|
|||
|
|
@ -842,7 +842,6 @@
|
|||
"Show Results": "Pokaż wyniki",
|
||||
"Clear search": "Wyczyść wyszukiwanie",
|
||||
"Clear search history": "Wyczyść historię wyszukiwania",
|
||||
"Tap to Toggle Footer": "Dotknij, aby przełączyć stopkę",
|
||||
"Show Current Time": "Pokaż aktualny czas",
|
||||
"Use 24 Hour Clock": "Używaj formatu 24-godzinnego",
|
||||
"Show Current Battery Status": "Pokaż aktualny stan baterii",
|
||||
|
|
|
|||
|
|
@ -1178,7 +1178,6 @@
|
|||
"Use 24 Hour Clock": "Usar formato de 24 horas",
|
||||
"Show Current Battery Status": "Mostrar o status atual da bateria",
|
||||
"Show Battery Percentage": "Mostrar porcentagem da bateria",
|
||||
"Tap to Toggle Footer": "Toque para alternar rodapé",
|
||||
"Screen": "Tela",
|
||||
"Orientation": "Orientação",
|
||||
"Portrait": "Retrato",
|
||||
|
|
|
|||
|
|
@ -831,7 +831,6 @@
|
|||
"Show Results": "Mostrar resultados",
|
||||
"Clear search": "Limpar pesquisa",
|
||||
"Clear search history": "Limpar histórico de pesquisa",
|
||||
"Tap to Toggle Footer": "Toque para alternar rodapé",
|
||||
"Show Current Time": "Mostrar hora atual",
|
||||
"Use 24 Hour Clock": "Usar formato de 24 horas",
|
||||
"Show Current Battery Status": "Mostrar o estado atual da bateria",
|
||||
|
|
|
|||
|
|
@ -1017,7 +1017,6 @@
|
|||
"Use 24 Hour Clock": "Folosește formatul de 24 de ore",
|
||||
"Show Current Battery Status": "Afișează starea bateriei",
|
||||
"Show Battery Percentage": "Afișează procentul bateriei",
|
||||
"Tap to Toggle Footer": "Apasă pentru a comuta subsolul",
|
||||
"Screen": "Ecran",
|
||||
"Orientation": "Orientare",
|
||||
"Portrait": "Portret",
|
||||
|
|
|
|||
|
|
@ -842,7 +842,6 @@
|
|||
"Show Results": "Показать результаты",
|
||||
"Clear search": "Очистить поиск",
|
||||
"Clear search history": "Очистить историю поиска",
|
||||
"Tap to Toggle Footer": "Нажмите для переключения нижнего колонтитула",
|
||||
"Show Current Time": "Показать текущее время",
|
||||
"Use 24 Hour Clock": "Использовать 24-часовой формат",
|
||||
"Show Current Battery Status": "Показать текущее состояние батареи",
|
||||
|
|
|
|||
|
|
@ -820,7 +820,6 @@
|
|||
"Show Results": "ප්රතිඵල පෙන්වන්න",
|
||||
"Clear search": "සෙවුම හිස් කරන්න",
|
||||
"Clear search history": "සෙවුම් ඉතිහාසය හිස් කරන්න",
|
||||
"Tap to Toggle Footer": "පාදකය ටොගල් කිරීමට තට්ටු කරන්න",
|
||||
"Show Current Time": "වත්මන් වේලාව පෙන්වන්න",
|
||||
"Use 24 Hour Clock": "පැය 24 ඔරලෝසුව භාවිතා කරන්න",
|
||||
"Show Current Battery Status": "වත්මන් බැටරි තත්ත්වය පෙන්වන්න",
|
||||
|
|
|
|||
|
|
@ -988,7 +988,6 @@
|
|||
"Show Progress Bar": "Prikaži vrstico napredka",
|
||||
"Reading Progress Style": "Slog napredka branja",
|
||||
"Percentage": "Odstotek",
|
||||
"Tap to Toggle Footer": "Tapnite za preklop noge",
|
||||
"Screen": "Zaslon",
|
||||
"Orientation": "Orientacija",
|
||||
"Portrait": "Pokončno",
|
||||
|
|
|
|||
|
|
@ -820,7 +820,6 @@
|
|||
"Show Results": "Visa resultat",
|
||||
"Clear search": "Rensa sökning",
|
||||
"Clear search history": "Rensa sökhistorik",
|
||||
"Tap to Toggle Footer": "Tryck för att växla sidfot",
|
||||
"Show Current Time": "Visa aktuell tid",
|
||||
"Use 24 Hour Clock": "Använd 24-timmarsklocka",
|
||||
"Show Current Battery Status": "Visa aktuell batteristatus",
|
||||
|
|
|
|||
|
|
@ -820,7 +820,6 @@
|
|||
"Show Results": "முடிவுகளைக் காட்டு",
|
||||
"Clear search": "தேடலை அழி",
|
||||
"Clear search history": "தேடல் வரலாற்றை அழி",
|
||||
"Tap to Toggle Footer": "அடிக்குறிப்பை மாற்ற தட்டவும்",
|
||||
"Show Current Time": "தற்போதைய நேரத்தைக் காண்பி",
|
||||
"Use 24 Hour Clock": "24 மணிநேர கடிகாரத்தைப் பயன்படுத்து",
|
||||
"Show Current Battery Status": "தற்போதைய பேட்டரி நிலையைக் காண்பி",
|
||||
|
|
|
|||
|
|
@ -809,7 +809,6 @@
|
|||
"Show Results": "แสดงผลลัพธ์",
|
||||
"Clear search": "ล้างการค้นหา",
|
||||
"Clear search history": "ล้างประวัติการค้นหา",
|
||||
"Tap to Toggle Footer": "แตะเพื่อสลับส่วนท้าย",
|
||||
"Show Current Time": "แสดงเวลาปัจจุบัน",
|
||||
"Use 24 Hour Clock": "ใช้รูปแบบเวลา 24 ชั่วโมง",
|
||||
"Show Current Battery Status": "แสดงสถานะแบตเตอรี่ปัจจุบัน",
|
||||
|
|
|
|||
|
|
@ -820,7 +820,6 @@
|
|||
"Show Results": "Sonuçları Göster",
|
||||
"Clear search": "Aramayı temizle",
|
||||
"Clear search history": "Arama geçmişini temizle",
|
||||
"Tap to Toggle Footer": "Altbilgiyi değiştirmek için dokunun",
|
||||
"Show Current Time": "Şu anki saati göster",
|
||||
"Use 24 Hour Clock": "24 Saatlik Saati Kullan",
|
||||
"Show Current Battery Status": "Mevcut Pil Durumunu Göster",
|
||||
|
|
|
|||
|
|
@ -842,7 +842,6 @@
|
|||
"Show Results": "Показати результати",
|
||||
"Clear search": "Очистити пошук",
|
||||
"Clear search history": "Очистити історію пошуку",
|
||||
"Tap to Toggle Footer": "Нижній колонтитул",
|
||||
"Show Current Time": "Показати поточний час",
|
||||
"Use 24 Hour Clock": "Використовувати 24-годинний формат",
|
||||
"Show Current Battery Status": "Показати поточний стан батареї",
|
||||
|
|
|
|||
|
|
@ -1156,7 +1156,6 @@
|
|||
"Use 24 Hour Clock": "24 soatlik formatdan foydalanish",
|
||||
"Show Current Battery Status": "Joriy batareya holatini koʻrsatish",
|
||||
"Show Battery Percentage": "Batareya foizini koʻrsatish",
|
||||
"Tap to Toggle Footer": "Pastki kolontitulni ochish/yopish uchun bosing",
|
||||
"Screen": "Ekran",
|
||||
"Orientation": "Yoʻnalish",
|
||||
"Portrait": "Vertikal",
|
||||
|
|
|
|||
|
|
@ -809,7 +809,6 @@
|
|||
"Show Results": "Hiển thị kết quả",
|
||||
"Clear search": "Xóa tìm kiếm",
|
||||
"Clear search history": "Xóa lịch sử tìm kiếm",
|
||||
"Tap to Toggle Footer": "Nhấn để bật/tắt chân trang",
|
||||
"Show Current Time": "Hiển thị thời gian hiện tại",
|
||||
"Use 24 Hour Clock": "Sử dụng định dạng 24 giờ",
|
||||
"Show Current Battery Status": "Hiển thị trạng thái pin hiện tại",
|
||||
|
|
|
|||
|
|
@ -810,7 +810,6 @@
|
|||
"Show Results": "显示结果",
|
||||
"Clear search": "清除搜索",
|
||||
"Clear search history": "清除搜索历史",
|
||||
"Tap to Toggle Footer": "点击切换页脚",
|
||||
"Show Current Time": "显示当前时间",
|
||||
"Use 24 Hour Clock": "使用 24 小时制",
|
||||
"Show Current Battery Status": "显示当前电量",
|
||||
|
|
|
|||
|
|
@ -809,7 +809,6 @@
|
|||
"Show Results": "顯示結果",
|
||||
"Clear search": "清除搜尋",
|
||||
"Clear search history": "清除搜尋歷史",
|
||||
"Tap to Toggle Footer": "點擊切換頁尾",
|
||||
"Show Current Time": "顯示目前時間",
|
||||
"Use 24 Hour Clock": "使用 24 小時制",
|
||||
"Show Current Battery Status": "顯示當前電池狀態",
|
||||
|
|
|
|||
|
|
@ -26,8 +26,9 @@ vi.mock('@/hooks/useTranslation', () => ({
|
|||
.replace('{{time}}', String(values?.['time'] ?? '{{time}}')),
|
||||
}));
|
||||
|
||||
let currentAppService = { isMobile: false, hasSafeAreaInset: false };
|
||||
vi.mock('@/context/EnvContext', () => ({
|
||||
useEnv: () => ({ envConfig: {}, appService: { isMobile: false, hasSafeAreaInset: false } }),
|
||||
useEnv: () => ({ envConfig: {}, appService: currentAppService }),
|
||||
}));
|
||||
|
||||
// Production code uses per-field selectors; mock must apply them so each
|
||||
|
|
@ -97,6 +98,7 @@ afterEach(() => {
|
|||
|
||||
beforeEach(() => {
|
||||
saveViewSettings.mockClear();
|
||||
currentAppService = { isMobile: false, hasSafeAreaInset: false };
|
||||
currentProgress = null;
|
||||
currentBookData = { isFixedLayout: false };
|
||||
currentRenderer = { page: 0, pages: 0 };
|
||||
|
|
@ -111,70 +113,12 @@ const makeProgress = (current: number, total: number): BookProgress =>
|
|||
timeinfo: { section: 0, total: 0 },
|
||||
}) as BookProgress;
|
||||
|
||||
describe('ProgressBar — tap-to-toggle disabled reverts hidden footer', () => {
|
||||
it("resets progressInfoMode to 'all' when the user disables tapToToggleFooter while mode was 'none'", () => {
|
||||
// Simulate a user who tapped the footer to dismiss it (mode='none')
|
||||
// while tapToToggleFooter was on. Now they have it switched off.
|
||||
currentViewSettings = {
|
||||
...baseSettings,
|
||||
tapToToggleFooter: false,
|
||||
progressInfoMode: 'none',
|
||||
} as ViewSettings;
|
||||
|
||||
renderProgressBar();
|
||||
|
||||
// The persisted progressInfoMode should be reset to the default
|
||||
// ('all') so the footer reverts to its default visibility.
|
||||
const persistCalls = saveViewSettings.mock.calls.filter(
|
||||
(args) => args[2] === 'progressInfoMode',
|
||||
);
|
||||
expect(persistCalls.length).toBeGreaterThanOrEqual(1);
|
||||
const lastCall = persistCalls[persistCalls.length - 1]!;
|
||||
expect(lastCall[3]).toBe('all');
|
||||
});
|
||||
|
||||
it("does not overwrite mode when tapToToggleFooter is on (user's cycled state stays)", () => {
|
||||
currentViewSettings = {
|
||||
...baseSettings,
|
||||
tapToToggleFooter: true,
|
||||
progressInfoMode: 'none',
|
||||
} as ViewSettings;
|
||||
|
||||
renderProgressBar();
|
||||
|
||||
// initial save mirrors the existing mode; importantly we never see
|
||||
// a save with 'all' overriding the user's tap-cycled choice.
|
||||
const persistCalls = saveViewSettings.mock.calls.filter(
|
||||
(args) => args[2] === 'progressInfoMode',
|
||||
);
|
||||
expect(persistCalls.every((args) => args[3] === 'none')).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves mode untouched when tapToToggleFooter is off but mode is already 'all'", () => {
|
||||
currentViewSettings = {
|
||||
...baseSettings,
|
||||
tapToToggleFooter: false,
|
||||
progressInfoMode: 'all',
|
||||
} as ViewSettings;
|
||||
|
||||
renderProgressBar();
|
||||
|
||||
const persistCalls = saveViewSettings.mock.calls.filter(
|
||||
(args) => args[2] === 'progressInfoMode',
|
||||
);
|
||||
// Either no save or a save matching the existing 'all' value — never
|
||||
// a transition through some intermediate state.
|
||||
expect(persistCalls.every((args) => args[3] === 'all')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProgressBar — fixed-layout remaining pages', () => {
|
||||
it('says "in book" with section-derived count for fixed-layout books', () => {
|
||||
currentViewSettings = {
|
||||
...baseSettings,
|
||||
showRemainingPages: true,
|
||||
showRemainingTime: false,
|
||||
progressInfoMode: 'all',
|
||||
} as ViewSettings;
|
||||
currentProgress = makeProgress(2, 5);
|
||||
currentBookData = { isFixedLayout: true, bookDoc: { metadata: {} } };
|
||||
|
|
@ -191,7 +135,6 @@ describe('ProgressBar — fixed-layout remaining pages', () => {
|
|||
...baseSettings,
|
||||
showRemainingPages: true,
|
||||
showRemainingTime: false,
|
||||
progressInfoMode: 'all',
|
||||
} as ViewSettings;
|
||||
currentProgress = makeProgress(2, 5);
|
||||
currentBookData = { isFixedLayout: false };
|
||||
|
|
@ -214,7 +157,6 @@ describe('ProgressBar — decorative footer is not focusable', () => {
|
|||
// not be focusable so it can never receive a focus ring.
|
||||
currentViewSettings = {
|
||||
...baseSettings,
|
||||
progressInfoMode: 'all',
|
||||
} as ViewSettings;
|
||||
currentProgress = makeProgress(2, 5);
|
||||
currentBookData = { isFixedLayout: false };
|
||||
|
|
@ -235,7 +177,6 @@ describe('ProgressBar — sticky progress bar', () => {
|
|||
currentViewSettings = {
|
||||
...baseSettings,
|
||||
showStickyProgressBar: true,
|
||||
progressInfoMode: 'all',
|
||||
...overrides,
|
||||
} as ViewSettings;
|
||||
// fraction (0.5) deliberately differs from the page fraction
|
||||
|
|
@ -287,6 +228,68 @@ describe('ProgressBar — sticky progress bar', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('ProgressBar — display-only footer overlay', () => {
|
||||
// The footer overlay is stacked above the book but is purely informational:
|
||||
// it must never intercept taps or text selection over book content, and it
|
||||
// exposes no clickable tap targets. In scrolled mode (no reserved band) each
|
||||
// info segment carries its own shrink-wrapped pill backdrop so it stays
|
||||
// legible floating over the text instead of a full-width bar.
|
||||
const readerSettings = (overrides?: Partial<ViewSettings>) => {
|
||||
currentViewSettings = {
|
||||
...baseSettings,
|
||||
...overrides,
|
||||
} as ViewSettings;
|
||||
currentProgress = makeProgress(2, 5);
|
||||
currentBookData = { isFixedLayout: false };
|
||||
currentRenderer = { page: 1, pages: 4 };
|
||||
};
|
||||
|
||||
it('keeps the full-width container pointer-events-none even on mobile', () => {
|
||||
readerSettings({ showRemainingPages: true });
|
||||
currentAppService = { isMobile: true, hasSafeAreaInset: false };
|
||||
|
||||
const { container } = renderProgressBar();
|
||||
|
||||
const progressInfo = container.querySelector('.progressinfo') as HTMLElement;
|
||||
expect(progressInfo.classList.contains('pointer-events-none')).toBe(true);
|
||||
expect(progressInfo.classList.contains('pointer-events-auto')).toBe(false);
|
||||
});
|
||||
|
||||
it('exposes no interactive targets (display-only, no tap-to-toggle)', () => {
|
||||
readerSettings({ showRemainingPages: true });
|
||||
|
||||
const { container } = renderProgressBar();
|
||||
|
||||
expect(container.querySelector('.pointer-events-auto')).toBeNull();
|
||||
expect(container.querySelector('.cursor-pointer')).toBeNull();
|
||||
expect(container.querySelector('.progress-restore-pad')).toBeNull();
|
||||
// No showFooter write ever originates from tapping the footer.
|
||||
expect(saveViewSettings.mock.calls.some((args) => args[2] === 'showFooter')).toBe(false);
|
||||
});
|
||||
|
||||
it('wraps each info segment in its own pill backdrop in scrolled mode', () => {
|
||||
// Scrolled mode reserves no bottom band — the info floats over the book
|
||||
// text, so each segment needs a shrink-wrapped backdrop to stay legible.
|
||||
readerSettings({ scrolled: true, showRemainingPages: true });
|
||||
|
||||
const { container } = renderProgressBar();
|
||||
|
||||
const pills = container.querySelectorAll('.progress-pill');
|
||||
expect(pills.length).toBeGreaterThanOrEqual(2); // remaining + progress
|
||||
for (const pill of pills) {
|
||||
expect((pill as HTMLElement).classList.contains('bg-base-100/85')).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('does not add pill backdrops in paginated mode (band holds the info)', () => {
|
||||
readerSettings({ scrolled: false, showRemainingPages: true });
|
||||
|
||||
const { container } = renderProgressBar();
|
||||
|
||||
expect(container.querySelector('.progress-pill')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProgressBar — contrast against the page (#4901)', () => {
|
||||
// A light-mode PDF under a dark theme keeps its white page, so the footer
|
||||
// progress/remaining text blends against the real backdrop (text-white/75 +
|
||||
|
|
@ -300,7 +303,6 @@ describe('ProgressBar — contrast against the page (#4901)', () => {
|
|||
isEink: false,
|
||||
showRemainingPages: true,
|
||||
showRemainingTime: false,
|
||||
progressInfoMode: 'all',
|
||||
} as ViewSettings;
|
||||
currentProgress = makeProgress(2, 5);
|
||||
currentBookData = { isFixedLayout: true };
|
||||
|
|
@ -322,7 +324,6 @@ describe('ProgressBar — contrast against the page (#4901)', () => {
|
|||
isEink: false,
|
||||
showRemainingPages: true,
|
||||
showRemainingTime: false,
|
||||
progressInfoMode: 'all',
|
||||
} as ViewSettings;
|
||||
currentProgress = makeProgress(2, 5);
|
||||
currentBookData = { isFixedLayout: false };
|
||||
|
|
@ -344,7 +345,6 @@ describe('ProgressBar — contrast against the page (#4901)', () => {
|
|||
isEink: true,
|
||||
showRemainingPages: true,
|
||||
showRemainingTime: false,
|
||||
progressInfoMode: 'all',
|
||||
} as ViewSettings;
|
||||
currentProgress = makeProgress(2, 5);
|
||||
currentBookData = { isFixedLayout: false };
|
||||
|
|
|
|||
104
apps/readest-app/src/__tests__/components/footer-band.test.ts
Normal file
104
apps/readest-app/src/__tests__/components/footer-band.test.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { footerInfoVisible, footerReservesBand } from '@/app/reader/utils/footerBand';
|
||||
import { DEFAULT_VIEW_CONFIG } from '@/services/constants';
|
||||
import type { ViewSettings } from '@/types/book';
|
||||
|
||||
// The book layout reserves a full-width bottom band (marginBottomPx of page
|
||||
// margin / scroll padding) for the footer. That band read as a "solid bar"
|
||||
// across the bottom of the screen: in scrolled mode the text clipped hard at
|
||||
// its edge, and it lingered even when the footer had nothing to show. Rules:
|
||||
// - scrolled mode never reserves the band; the info floats over the text
|
||||
// in shrink-wrapped pills instead
|
||||
// - paginated mode reserves it only while some widget is enabled
|
||||
// - the sticky progress bar (always-visible, display-only) keeps its band
|
||||
// Turning Show Footer off (Settings -> Layout) unmounts the footer and
|
||||
// releases the band through the showFooter gate.
|
||||
const settings = (overrides: Partial<ViewSettings>): ViewSettings =>
|
||||
({ ...DEFAULT_VIEW_CONFIG, ...overrides }) as ViewSettings;
|
||||
|
||||
describe('footerInfoVisible', () => {
|
||||
it('is true with default settings (progress info shown)', () => {
|
||||
expect(footerInfoVisible(settings({}))).toBe(true);
|
||||
});
|
||||
|
||||
it('is false when every footer widget is disabled in settings', () => {
|
||||
expect(
|
||||
footerInfoVisible(
|
||||
settings({
|
||||
showRemainingTime: false,
|
||||
showRemainingPages: false,
|
||||
showProgressInfo: false,
|
||||
showCurrentTime: false,
|
||||
showCurrentBatteryStatus: false,
|
||||
}),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('is true when any single widget is enabled', () => {
|
||||
expect(
|
||||
footerInfoVisible(
|
||||
settings({
|
||||
showRemainingTime: false,
|
||||
showRemainingPages: true,
|
||||
showProgressInfo: false,
|
||||
showCurrentTime: false,
|
||||
showCurrentBatteryStatus: false,
|
||||
}),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('footerReservesBand', () => {
|
||||
it('is false when Show Footer is off', () => {
|
||||
expect(footerReservesBand(settings({ showFooter: false }))).toBe(false);
|
||||
});
|
||||
|
||||
it('reserves the band in paginated mode while info is visible', () => {
|
||||
expect(footerReservesBand(settings({ showFooter: true, scrolled: false }))).toBe(true);
|
||||
});
|
||||
|
||||
it('releases the band in paginated mode when no widget is enabled', () => {
|
||||
expect(
|
||||
footerReservesBand(
|
||||
settings({
|
||||
showFooter: true,
|
||||
scrolled: false,
|
||||
showRemainingTime: false,
|
||||
showRemainingPages: false,
|
||||
showProgressInfo: false,
|
||||
showCurrentTime: false,
|
||||
showCurrentBatteryStatus: false,
|
||||
}),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('never reserves the band in scrolled mode, even with info visible', () => {
|
||||
expect(footerReservesBand(settings({ showFooter: true, scrolled: true }))).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps the band for the sticky progress bar, scrolled or not', () => {
|
||||
expect(
|
||||
footerReservesBand(
|
||||
settings({ showFooter: true, scrolled: true, showStickyProgressBar: true }),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
footerReservesBand(
|
||||
settings({
|
||||
showFooter: true,
|
||||
scrolled: false,
|
||||
showStickyProgressBar: true,
|
||||
showRemainingTime: false,
|
||||
showRemainingPages: false,
|
||||
showProgressInfo: false,
|
||||
showCurrentTime: false,
|
||||
showCurrentBatteryStatus: false,
|
||||
}),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -605,13 +605,11 @@ describe('services/constants', () => {
|
|||
expect(typeof DEFAULT_VIEW_CONFIG.showCurrentBatteryStatus).toBe('boolean');
|
||||
expect(typeof DEFAULT_VIEW_CONFIG.showBatteryPercentage).toBe('boolean');
|
||||
expect(typeof DEFAULT_VIEW_CONFIG.use24HourClock).toBe('boolean');
|
||||
expect(typeof DEFAULT_VIEW_CONFIG.tapToToggleFooter).toBe('boolean');
|
||||
expect(typeof DEFAULT_VIEW_CONFIG.showPaginationButtons).toBe('boolean');
|
||||
});
|
||||
|
||||
it('has progress style settings', () => {
|
||||
expect(typeof DEFAULT_VIEW_CONFIG.progressStyle).toBe('string');
|
||||
expect(typeof DEFAULT_VIEW_CONFIG.progressInfoMode).toBe('string');
|
||||
});
|
||||
|
||||
it('has animation and eink flags', () => {
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ import { useBookCoverAutoSave } from '../hooks/useAutoSaveBookCover';
|
|||
import { useDiscordPresence } from '@/hooks/useDiscordPresence';
|
||||
import { manageSyntaxHighlighting } from '@/utils/highlightjs';
|
||||
import { getViewInsets } from '@/utils/insets';
|
||||
import { footerReservesBand } from '../utils/footerBand';
|
||||
import { handleA11yNavigation } from '@/utils/a11y';
|
||||
import { isCJKLang } from '@/utils/lang';
|
||||
import { getLocale } from '@/utils/misc';
|
||||
|
|
@ -792,7 +793,11 @@ const FoliateViewer: React.FC<{
|
|||
const showDoubleBorderHeader = showDoubleBorder && viewSettings.showHeader;
|
||||
const showDoubleBorderFooter = showDoubleBorder && viewSettings.showFooter;
|
||||
const showTopHeader = viewSettings.showHeader && !viewSettings.vertical;
|
||||
const showBottomFooter = viewSettings.showFooter && !viewSettings.vertical;
|
||||
// The bottom band is reserved only while the footer displays something
|
||||
// there (and never in scrolled mode, where the info floats in pills) —
|
||||
// see footerReservesBand. Otherwise the empty reservation shows as a
|
||||
// full-width blank bar that steals space from the book text.
|
||||
const showBottomFooter = footerReservesBand(viewSettings) && !viewSettings.vertical;
|
||||
const moreTopInset = showTopHeader ? Math.max(0, 16 - insets.top) : 0;
|
||||
const miniPlayerClearance = viewState?.ttsEnabled
|
||||
? TTS_MINI_PLAYER_CLEARANCE + gridInsets.bottom * 0.33
|
||||
|
|
@ -935,6 +940,14 @@ const FoliateViewer: React.FC<{
|
|||
viewSettings?.scrolled,
|
||||
viewSettings?.noContinuousScroll,
|
||||
viewState?.ttsEnabled,
|
||||
// footerReservesBand inputs: the band must collapse/return live when the
|
||||
// user flips these settings.
|
||||
viewSettings?.showStickyProgressBar,
|
||||
viewSettings?.showRemainingTime,
|
||||
viewSettings?.showRemainingPages,
|
||||
viewSettings?.showProgressInfo,
|
||||
viewSettings?.showCurrentTime,
|
||||
viewSettings?.showCurrentBatteryStatus,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import clsx from 'clsx';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import type { Insets } from '@/types/misc';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
|
|
@ -13,10 +13,7 @@ import {
|
|||
getChapterTickFractions,
|
||||
getReferencePageInfo,
|
||||
} from '@/utils/progress';
|
||||
import { saveViewSettings } from '@/helpers/settings';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { SIZE_PER_LOC, SIZE_PER_TIME_UNIT } from '@/services/constants';
|
||||
import type { ProgressBarMode } from '@/types/book.ts';
|
||||
import StatusInfo from './StatusInfo.tsx';
|
||||
import StickyProgressBar from './StickyProgressBar.tsx';
|
||||
|
||||
|
|
@ -34,7 +31,7 @@ const ProgressBar: React.FC<ProgressBarProps> = ({
|
|||
gridInsets,
|
||||
}) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { appService } = useEnv();
|
||||
const getBookData = useBookDataStore((s) => s.getBookData);
|
||||
const getViewSettings = useReaderStore((s) => s.getViewSettings);
|
||||
const getView = useReaderStore((s) => s.getView);
|
||||
|
|
@ -131,106 +128,33 @@ const ProgressBar: React.FC<ProgressBarProps> = ({
|
|||
})
|
||||
: '';
|
||||
|
||||
const [progressBarMode, setProgressBarMode] = useState<string>(viewSettings.progressInfoMode);
|
||||
|
||||
const hasRemainingInfo = viewSettings.showRemainingTime || viewSettings.showRemainingPages;
|
||||
const hasProgressInfo = viewSettings.showProgressInfo;
|
||||
const hasTimeInfo = viewSettings.showCurrentTime;
|
||||
const hasBatteryInfo = viewSettings.showCurrentBatteryStatus;
|
||||
const cycleProgressInfoModes = () => {
|
||||
if (!viewSettings.tapToToggleFooter) return;
|
||||
|
||||
const modeSequence: string[] = [
|
||||
'all',
|
||||
`${hasRemainingInfo ? 'remaining+' : ''}${hasProgressInfo ? 'progress' : ''}`,
|
||||
`${hasRemainingInfo ? 'remaining' : ''}`,
|
||||
`${hasProgressInfo ? 'progress' : ''}`,
|
||||
`${hasBatteryInfo ? 'battery+' : ''}${hasTimeInfo ? 'time' : ''}`,
|
||||
`${hasBatteryInfo ? 'battery' : ''}`,
|
||||
`${hasTimeInfo ? 'time' : ''}`,
|
||||
'none',
|
||||
]
|
||||
.map((mode) => mode.replace(/^\+|\+$/g, ''))
|
||||
.filter((mode) => mode !== '')
|
||||
.filter((mode, index, self) => self.indexOf(mode) === index);
|
||||
|
||||
const currentMode = progressBarMode;
|
||||
const currentIndex = modeSequence.indexOf(currentMode);
|
||||
for (let i = 1; i <= modeSequence.length; i++) {
|
||||
const nextIndex = (currentIndex + i) % modeSequence.length;
|
||||
const nextMode = modeSequence[nextIndex]!;
|
||||
|
||||
const currentRenders = {
|
||||
remaining:
|
||||
currentMode === 'all' || currentMode.includes('remaining') ? hasRemainingInfo : false,
|
||||
progress:
|
||||
currentMode === 'all' || currentMode.includes('progress') ? hasProgressInfo : false,
|
||||
battery: currentMode === 'all' || currentMode.includes('battery') ? hasBatteryInfo : false,
|
||||
time: currentMode === 'all' || currentMode.includes('time') ? hasTimeInfo : false,
|
||||
none: currentMode === 'none',
|
||||
};
|
||||
|
||||
const nextRenders = {
|
||||
remaining: nextMode === 'all' || nextMode.includes('remaining') ? hasRemainingInfo : false,
|
||||
progress: nextMode === 'all' || nextMode.includes('progress') ? hasProgressInfo : false,
|
||||
battery: nextMode === 'all' || nextMode.includes('battery') ? hasBatteryInfo : false,
|
||||
time: nextMode === 'all' || nextMode.includes('time') ? hasTimeInfo : false,
|
||||
none: nextMode === 'none',
|
||||
};
|
||||
|
||||
const isDifferent =
|
||||
currentRenders.remaining !== nextRenders.remaining ||
|
||||
currentRenders.progress !== nextRenders.progress ||
|
||||
currentRenders.battery !== nextRenders.battery ||
|
||||
currentRenders.time !== nextRenders.time ||
|
||||
currentRenders.none !== nextRenders.none;
|
||||
if (isDifferent) {
|
||||
setProgressBarMode(nextMode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const nextIndex = (currentIndex + 1) % modeSequence.length;
|
||||
setProgressBarMode(modeSequence[nextIndex]!);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
saveViewSettings(envConfig, bookKey, 'progressInfoMode', progressBarMode as ProgressBarMode);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [progressBarMode]);
|
||||
|
||||
// Self-heal a stuck "none" (or partial) mode left over from a prior
|
||||
// tap-to-toggle session. Without this, dismissing the footer via tap
|
||||
// and then disabling the toggle in settings would leave the footer
|
||||
// permanently hidden — the user's only way back to a visible footer
|
||||
// would be to re-enable the toggle and tap through the cycle.
|
||||
useEffect(() => {
|
||||
if (!viewSettings.tapToToggleFooter && progressBarMode !== 'all') {
|
||||
setProgressBarMode('all');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [viewSettings.tapToToggleFooter]);
|
||||
|
||||
const isMobile = appService?.isMobile || window.innerWidth < 640;
|
||||
const showStatusInfo =
|
||||
(progressBarMode === 'all' ||
|
||||
progressBarMode.includes('battery') ||
|
||||
progressBarMode.includes('time')) &&
|
||||
(hasTimeInfo || hasBatteryInfo);
|
||||
// The footer is display-only: the full-width container stays
|
||||
// pointer-events-none so it never intercepts taps or text selection over
|
||||
// book content along the bottom of the page. To hide it, use Settings →
|
||||
// Layout → Show Footer.
|
||||
//
|
||||
// Scrolled mode reserves no bottom band (footerReservesBand) — the info
|
||||
// floats over the book text, so each segment carries its own shrink-wrapped
|
||||
// pill backdrop to stay legible instead of a full-width bar.
|
||||
const pillClass =
|
||||
viewSettings.scrolled &&
|
||||
!isVertical &&
|
||||
!stickyBarActive &&
|
||||
'progress-pill eink-bordered rounded-md bg-base-100/85 px-1.5';
|
||||
const showStatusInfo = hasTimeInfo || hasBatteryInfo;
|
||||
|
||||
return (
|
||||
<div
|
||||
role='presentation'
|
||||
className={clsx(
|
||||
'progressinfo absolute bottom-0 flex items-center justify-between font-sans',
|
||||
'progressinfo pointer-events-none absolute bottom-0 flex items-center justify-between font-sans',
|
||||
isEink ? 'text-sm font-normal' : 'text-neutral-content text-xs font-extralight',
|
||||
isVertical ? 'writing-vertical-rl' : 'w-full',
|
||||
isMobile ? 'pointer-events-auto' : 'pointer-events-none',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (eventDispatcher.dispatchSync('iframe-single-click')) return;
|
||||
cycleProgressInfoModes();
|
||||
}}
|
||||
aria-label={[
|
||||
progress
|
||||
? _('On {{current}} of {{total}} page', {
|
||||
|
|
@ -280,68 +204,64 @@ const ProgressBar: React.FC<ProgressBarProps> = ({
|
|||
isEink={isEink}
|
||||
/>
|
||||
)}
|
||||
{(progressBarMode === 'all' || progressBarMode.includes('remaining')) &&
|
||||
hasRemainingInfo && (
|
||||
<div
|
||||
className={clsx(
|
||||
'remaining-info whitespace-nowrap text-start',
|
||||
!stickyBarActive && 'flex-1',
|
||||
showStatusInfo && 'overflow-hidden',
|
||||
bookData?.isFixedLayout && !isEink
|
||||
? 'text-white/75 mix-blend-difference'
|
||||
: 'text-base-content',
|
||||
)}
|
||||
>
|
||||
{viewSettings.showRemainingTime ? (
|
||||
<span className='time-left-label text-start'>{timeLeftStr}</span>
|
||||
) : viewSettings.showRemainingPages && showPagesLeft ? (
|
||||
<span className='text-start'>
|
||||
{localize ? (
|
||||
remainingInBook ? (
|
||||
<Trans
|
||||
i18nKey='{{number}} pages left in book'
|
||||
values={{ number: formatNumber(pagesLeft, localize, lang) }}
|
||||
>
|
||||
<span className='pages-left-number'>{'{{number}}'}</span>
|
||||
<span className='pages-left-label'>{' pages left in book'}</span>
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey='{{number}} pages left in chapter'
|
||||
values={{ number: formatNumber(pagesLeft, localize, lang) }}
|
||||
>
|
||||
<span className='pages-left-number'>{'{{number}}'}</span>
|
||||
<span className='pages-left-label'>{' pages left in chapter'}</span>
|
||||
</Trans>
|
||||
)
|
||||
) : remainingInBook ? (
|
||||
<Trans i18nKey='{{count}} pages left in book' count={pagesLeft}>
|
||||
<span className='pages-left-number'>{'{{count}}'}</span>
|
||||
{hasRemainingInfo && (
|
||||
<div
|
||||
className={clsx(
|
||||
'remaining-info whitespace-nowrap text-start',
|
||||
!stickyBarActive && 'flex-1',
|
||||
showStatusInfo && 'overflow-hidden',
|
||||
bookData?.isFixedLayout && !isEink
|
||||
? 'text-white/75 mix-blend-difference'
|
||||
: 'text-base-content',
|
||||
)}
|
||||
>
|
||||
{viewSettings.showRemainingTime ? (
|
||||
<span className={clsx('time-left-label text-start', pillClass)}>{timeLeftStr}</span>
|
||||
) : viewSettings.showRemainingPages && showPagesLeft ? (
|
||||
<span className={clsx('text-start', pillClass)}>
|
||||
{localize ? (
|
||||
remainingInBook ? (
|
||||
<Trans
|
||||
i18nKey='{{number}} pages left in book'
|
||||
values={{ number: formatNumber(pagesLeft, localize, lang) }}
|
||||
>
|
||||
<span className='pages-left-number'>{'{{number}}'}</span>
|
||||
<span className='pages-left-label'>{' pages left in book'}</span>
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans i18nKey='{{count}} pages left in chapter' count={pagesLeft}>
|
||||
<span className='pages-left-number'>{'{{count}}'}</span>
|
||||
<Trans
|
||||
i18nKey='{{number}} pages left in chapter'
|
||||
values={{ number: formatNumber(pagesLeft, localize, lang) }}
|
||||
>
|
||||
<span className='pages-left-number'>{'{{number}}'}</span>
|
||||
<span className='pages-left-label'>{' pages left in chapter'}</span>
|
||||
</Trans>
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
) : remainingInBook ? (
|
||||
<Trans i18nKey='{{count}} pages left in book' count={pagesLeft}>
|
||||
<span className='pages-left-number'>{'{{count}}'}</span>
|
||||
<span className='pages-left-label'>{' pages left in book'}</span>
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans i18nKey='{{count}} pages left in chapter' count={pagesLeft}>
|
||||
<span className='pages-left-number'>{'{{count}}'}</span>
|
||||
<span className='pages-left-label'>{' pages left in chapter'}</span>
|
||||
</Trans>
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showStatusInfo && (
|
||||
<StatusInfo
|
||||
showTime={
|
||||
(progressBarMode === 'all' || progressBarMode.includes('time')) && hasTimeInfo
|
||||
}
|
||||
showTime={hasTimeInfo}
|
||||
use24Hour={viewSettings.use24HourClock}
|
||||
showBattery={
|
||||
(progressBarMode === 'all' || progressBarMode.includes('battery')) && hasBatteryInfo
|
||||
}
|
||||
showBattery={hasBatteryInfo}
|
||||
showBatteryPercentage={viewSettings.showBatteryPercentage}
|
||||
isVertical={isVertical}
|
||||
isEink={isEink}
|
||||
className={pillClass || undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -354,19 +274,16 @@ const ProgressBar: React.FC<ProgressBarProps> = ({
|
|||
: 'text-base-content',
|
||||
)}
|
||||
>
|
||||
{(progressBarMode === 'all' || progressBarMode.includes('progress')) && (
|
||||
<>
|
||||
{viewSettings.showProgressInfo && (
|
||||
<span
|
||||
className={clsx(
|
||||
'progress-info-label text-end',
|
||||
isVertical ? 'mt-auto' : 'ms-auto',
|
||||
)}
|
||||
>
|
||||
{progressInfo}
|
||||
</span>
|
||||
{viewSettings.showProgressInfo && (
|
||||
<span
|
||||
className={clsx(
|
||||
'progress-info-label text-end',
|
||||
isVertical ? 'mt-auto' : 'ms-auto',
|
||||
pillClass,
|
||||
)}
|
||||
</>
|
||||
>
|
||||
{progressInfo}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ interface StatusInfoProps {
|
|||
use24Hour?: boolean;
|
||||
isVertical?: boolean;
|
||||
isEink?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const StatusInfo: React.FC<StatusInfoProps> = ({
|
||||
|
|
@ -19,6 +20,7 @@ const StatusInfo: React.FC<StatusInfoProps> = ({
|
|||
showBatteryPercentage,
|
||||
isVertical,
|
||||
isEink,
|
||||
className,
|
||||
}) => {
|
||||
const formattedTime = useCurrentTime(showTime, use24Hour);
|
||||
const batteryLevel = useCurrentBatteryStatus(showBattery);
|
||||
|
|
@ -30,6 +32,7 @@ const StatusInfo: React.FC<StatusInfoProps> = ({
|
|||
className={clsx(
|
||||
'status-bar flex shrink-0 items-center gap-2 whitespace-nowrap tabular-nums text-base-content',
|
||||
isVertical ? 'my-auto' : 'flex-row',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{showTime && <span>{formattedTime}</span>}
|
||||
|
|
|
|||
33
apps/readest-app/src/app/reader/utils/footerBand.ts
Normal file
33
apps/readest-app/src/app/reader/utils/footerBand.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import type { ViewSettings } from '@/types/book';
|
||||
|
||||
/**
|
||||
* Whether the footer currently displays any info widget, mirroring the
|
||||
* per-widget gating in ProgressBar.
|
||||
*/
|
||||
export const footerInfoVisible = (viewSettings: ViewSettings): boolean =>
|
||||
!!(
|
||||
viewSettings.showRemainingTime ||
|
||||
viewSettings.showRemainingPages ||
|
||||
viewSettings.showProgressInfo ||
|
||||
viewSettings.showCurrentTime ||
|
||||
viewSettings.showCurrentBatteryStatus
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether the book layout must reserve the full-width bottom band
|
||||
* (marginBottomPx of page margin / scroll padding) for the footer.
|
||||
*
|
||||
* The band used to be reserved whenever Show Footer was on, which read as a
|
||||
* "solid bar" across the bottom of the screen: scrolled text clipped hard at
|
||||
* its edge, and it lingered even when the footer had nothing to show. Now:
|
||||
* - the sticky progress bar (always-visible, display-only) keeps its band
|
||||
* - scrolled mode never reserves it — the info floats over the text in
|
||||
* shrink-wrapped pills (see ProgressBar) instead of a full-width strip
|
||||
* - paginated mode reserves it only while some info actually renders
|
||||
*/
|
||||
export const footerReservesBand = (viewSettings: ViewSettings): boolean => {
|
||||
if (!viewSettings.showFooter) return false;
|
||||
if (viewSettings.showStickyProgressBar) return true;
|
||||
if (viewSettings.scrolled) return false;
|
||||
return footerInfoVisible(viewSettings);
|
||||
};
|
||||
|
|
@ -86,7 +86,6 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
|||
const [showBatteryPercentage, setShowBatteryPercentage] = useState(
|
||||
viewSettings.showBatteryPercentage,
|
||||
);
|
||||
const [tapToToggleFooter, setTapToToggleFooter] = useState(viewSettings.tapToToggleFooter);
|
||||
const [progressStyle, setProgressStyle] = useState(viewSettings.progressStyle);
|
||||
const [referencePageCount, setReferencePageCount] = useState(viewSettings.referencePageCount);
|
||||
const [screenOrientation, setScreenOrientation] = useState(viewSettings.screenOrientation);
|
||||
|
|
@ -128,7 +127,6 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
|||
use24HourClock: setUse24HourClock,
|
||||
showCurrentBatteryStatus: setShowCurrentBatteryStatus,
|
||||
showBatteryPercentage: setShowBatteryPercentage,
|
||||
tapToToggleFooter: setTapToToggleFooter,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -412,11 +410,6 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [referencePageCount]);
|
||||
|
||||
useEffect(() => {
|
||||
saveViewSettings(envConfig, bookKey, 'tapToToggleFooter', tapToToggleFooter, false, false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tapToToggleFooter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showHeader === viewSettings.showHeader) return;
|
||||
if (showHeader && !viewSettings.vertical) {
|
||||
|
|
@ -791,12 +784,6 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
|
|||
disabled={!showFooter || !showCurrentBatteryStatus}
|
||||
onChange={() => setShowBatteryPercentage(!showBatteryPercentage)}
|
||||
/>
|
||||
<SettingsSwitchRow
|
||||
label={_('Tap to Toggle Footer')}
|
||||
checked={tapToToggleFooter}
|
||||
disabled={!showFooter}
|
||||
onChange={() => setTapToToggleFooter(!tapToToggleFooter)}
|
||||
/>
|
||||
</BoxedList>
|
||||
|
||||
{appService?.hasOrientationLock && (
|
||||
|
|
|
|||
|
|
@ -384,11 +384,9 @@ export const DEFAULT_VIEW_CONFIG: ViewConfig = {
|
|||
showCurrentBatteryStatus: false,
|
||||
showBatteryPercentage: true,
|
||||
use24HourClock: false,
|
||||
tapToToggleFooter: false,
|
||||
showPaginationButtons: false,
|
||||
progressStyle: 'fraction',
|
||||
referencePageCount: 0,
|
||||
progressInfoMode: 'all',
|
||||
|
||||
animated: false,
|
||||
pageTurnStyle: 'push',
|
||||
|
|
|
|||
|
|
@ -273,7 +273,6 @@ export interface BookLanguage {
|
|||
convertChineseVariant: ConvertChineseVariant;
|
||||
}
|
||||
|
||||
export type ProgressBarMode = 'remaining' | 'progress' | 'battery' | 'time' | 'all' | 'none';
|
||||
// 'push' slides the whole strip; 'slide' and 'curl' layer the outgoing page
|
||||
// over the still incoming page (Apple Books style, needs View Transitions).
|
||||
export type PageTurnStyle = 'push' | 'slide' | 'curl';
|
||||
|
|
@ -295,11 +294,9 @@ export interface ViewConfig {
|
|||
use24HourClock: boolean;
|
||||
showCurrentBatteryStatus: boolean;
|
||||
showBatteryPercentage: boolean;
|
||||
tapToToggleFooter: boolean;
|
||||
showPaginationButtons: boolean;
|
||||
progressStyle: 'percentage' | 'fraction' | 'reference';
|
||||
referencePageCount: number;
|
||||
progressInfoMode: ProgressBarMode;
|
||||
|
||||
animated: boolean;
|
||||
pageTurnStyle: PageTurnStyle;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue