feat(reader): implement FEAT-01 Focus Zen Mode (100% vh/vw, auto-hiding HUD, hotkeys F/Esc, native fullscreen)

This commit is contained in:
Antigravity committed 2026-10-08 10:45:09 +03:00
1 parent 9bbc9473de
commit f10965eff5
13 files changed
+3157 -792

No files matched your search

+141
View File
@@ -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';
});
}