feat(reader): implement FEAT-01 Focus Zen Mode (100% vh/vw, auto-hiding HUD, hotkeys F/Esc, native fullscreen)
This commit is contained in:
@@ -28,6 +28,13 @@ let popupCurrentData = null;
|
||||
let readerScrollSaveTimer = null;
|
||||
let popupScrollRaf = null;
|
||||
|
||||
let isZenMode = false;
|
||||
let zenHudIdleTimer = null;
|
||||
|
||||
export function getIsZenMode() {
|
||||
return isZenMode;
|
||||
}
|
||||
|
||||
export function getCurrentBook() {
|
||||
return currentBook;
|
||||
}
|
||||
@@ -134,6 +141,7 @@ function createBookCard(b) {
|
||||
}
|
||||
|
||||
export async function loadBookshelf() {
|
||||
if (isZenMode) toggleZenMode(false);
|
||||
const bookshelfContainer = document.getElementById('bookshelf-container');
|
||||
const readerInterface = document.getElementById('reader-interface');
|
||||
const bookshelfGrid = document.getElementById('bookshelf-grid');
|
||||
@@ -1610,6 +1618,26 @@ export function updateReaderProgressBar() {
|
||||
} else if (readerScrollPctBadge) {
|
||||
readerScrollPctBadge.textContent = `${pct}%`;
|
||||
}
|
||||
|
||||
// Update Zen HUD elements if present
|
||||
const zenHudTitle = document.getElementById('zen-hud-title');
|
||||
const zenHudChapter = document.getElementById('zen-hud-chapter');
|
||||
const zenHudProgress = document.getElementById('zen-hud-progress');
|
||||
|
||||
if (zenHudTitle && currentBook) {
|
||||
zenHudTitle.textContent = currentBook.title || 'Книга';
|
||||
zenHudTitle.title = currentBook.title || 'Книга';
|
||||
}
|
||||
if (zenHudChapter && currentBook && currentBook.chapters) {
|
||||
const totalCh = currentBook.chapters.length;
|
||||
const ch = currentBook.chapters[currentChapterIndex];
|
||||
const chTitle = ch ? (ch.title || `Глава ${currentChapterIndex + 1}`) : `Глава ${currentChapterIndex + 1}`;
|
||||
zenHudChapter.textContent = totalCh > 1 ? `Гл. ${currentChapterIndex + 1}/${totalCh}` : chTitle;
|
||||
zenHudChapter.title = chTitle;
|
||||
}
|
||||
if (zenHudProgress) {
|
||||
zenHudProgress.textContent = `${pct}%`;
|
||||
}
|
||||
}
|
||||
|
||||
export function handleReaderScroll() {
|
||||
@@ -1836,6 +1864,62 @@ export function positionSettingsPanel() {
|
||||
readerSettingsPanel.style.maxHeight = `${maxH}px`;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Zen Focus Mode
|
||||
// -----------------------------------------------------------------------
|
||||
export function wakeZenHud() {
|
||||
if (!isZenMode) return;
|
||||
const hud = document.getElementById('reader-zen-hud');
|
||||
if (!hud) return;
|
||||
hud.classList.remove('hud-idle');
|
||||
if (zenHudIdleTimer) {
|
||||
clearTimeout(zenHudIdleTimer);
|
||||
}
|
||||
zenHudIdleTimer = setTimeout(() => {
|
||||
if (isZenMode && hud) {
|
||||
hud.classList.add('hud-idle');
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
export function toggleZenMode(forceState = null) {
|
||||
const newState = (forceState !== null) ? Boolean(forceState) : !isZenMode;
|
||||
if (isZenMode === newState) return;
|
||||
isZenMode = newState;
|
||||
|
||||
const hud = document.getElementById('reader-zen-hud');
|
||||
const zenBtn = document.getElementById('reader-zen-btn');
|
||||
const readerSettingsPanel = document.getElementById('reader-settings-panel');
|
||||
|
||||
if (isZenMode) {
|
||||
document.body.classList.add('zen-mode-active');
|
||||
if (zenBtn) zenBtn.classList.add('active');
|
||||
if (readerSettingsPanel) readerSettingsPanel.classList.add('hidden');
|
||||
if (hud) {
|
||||
hud.classList.remove('hidden');
|
||||
hud.classList.remove('hud-idle');
|
||||
wakeZenHud();
|
||||
}
|
||||
if (!document.fullscreenElement && document.documentElement.requestFullscreen) {
|
||||
document.documentElement.requestFullscreen().catch(() => {});
|
||||
}
|
||||
} else {
|
||||
document.body.classList.remove('zen-mode-active');
|
||||
if (zenBtn) zenBtn.classList.remove('active');
|
||||
if (hud) {
|
||||
hud.classList.add('hidden');
|
||||
hud.classList.remove('hud-idle');
|
||||
}
|
||||
if (zenHudIdleTimer) {
|
||||
clearTimeout(zenHudIdleTimer);
|
||||
zenHudIdleTimer = null;
|
||||
}
|
||||
if (document.fullscreenElement && document.exitFullscreen) {
|
||||
document.exitFullscreen().catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Reader Module Initialization
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -1865,6 +1949,62 @@ export function initReader() {
|
||||
const readerBackBtn = document.getElementById('reader-back-btn');
|
||||
const readerToggleHighlightBtn = document.getElementById('reader-toggle-highlight-btn');
|
||||
const readerTrainBtn = document.getElementById('reader-train-btn');
|
||||
const readerZenBtn = document.getElementById('reader-zen-btn');
|
||||
const zenHudExitBtn = document.getElementById('zen-hud-exit-btn');
|
||||
|
||||
if (readerZenBtn) {
|
||||
readerZenBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleZenMode();
|
||||
});
|
||||
}
|
||||
|
||||
if (zenHudExitBtn) {
|
||||
zenHudExitBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleZenMode(false);
|
||||
});
|
||||
}
|
||||
|
||||
// Wake HUD on user activity during zen mode
|
||||
['mousemove', 'touchstart', 'scroll'].forEach(evt => {
|
||||
window.addEventListener(evt, () => {
|
||||
if (isZenMode) wakeZenHud();
|
||||
}, { passive: true });
|
||||
});
|
||||
|
||||
// Keyboard shortcut for Zen Mode (F / f / а / А) and exit on Escape
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const tag = (e.target && e.target.tagName) ? e.target.tagName.toLowerCase() : '';
|
||||
if (tag === 'input' || tag === 'textarea' || (e.target && e.target.isContentEditable)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const readerView = document.getElementById('reader-view');
|
||||
const readerInterface = document.getElementById('reader-interface');
|
||||
const isReaderActive = readerView && !readerView.classList.contains('hidden') &&
|
||||
readerInterface && !readerInterface.classList.contains('hidden');
|
||||
|
||||
if (!isReaderActive) return;
|
||||
|
||||
if (e.key === 'Escape' && isZenMode) {
|
||||
e.preventDefault();
|
||||
toggleZenMode(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((e.key === 'f' || e.key === 'F' || e.key === 'а' || e.key === 'А') && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
toggleZenMode();
|
||||
}
|
||||
});
|
||||
|
||||
// Native fullscreen sync
|
||||
document.addEventListener('fullscreenchange', () => {
|
||||
if (!document.fullscreenElement && isZenMode) {
|
||||
toggleZenMode(false);
|
||||
}
|
||||
});
|
||||
|
||||
const popupCloseBtn = document.getElementById('popup-close-btn');
|
||||
const popupSaveBtn = document.getElementById('popup-save-btn');
|
||||
@@ -2450,6 +2590,7 @@ export function initReader() {
|
||||
|
||||
if (readerBackBtn) {
|
||||
readerBackBtn.addEventListener('click', () => {
|
||||
if (isZenMode) toggleZenMode(false);
|
||||
window.location.hash = '#reader';
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user