@@ -29,7 +29,7 @@
overflow : hidden ;
}
# book-content {
# book-content , # fb2-content {
width : 100 % ;
height : 100 % ;
position : absolute ;
@@ -38,8 +38,6 @@
}
# fb2-content {
width : 100 % ;
height : 100 % ;
overflow : hidden ;
padding : 20 px ;
font-size : 18 px ;
@@ -58,11 +56,11 @@
gap : 15 px ;
}
# loading . spinner {
. spinner {
width : 40 px ;
height : 40 px ;
border : 4 px solid #ddd ;
border-top : 4 px solid #5D4037 ;
border-top-color : #5D4037 ;
border-radius : 50 % ;
animation : spin 1 s linear infinite ;
}
@@ -86,23 +84,8 @@
gap : 10 px ;
}
# debug-log {
position : fixed ;
bottom : 0 ;
left : 0 ;
right : 0 ;
max-height : 200 px ;
overflow-y : auto ;
background : rgba ( 0 , 0 , 0 , 0.9 ) ;
color : #0f0 ;
font-size : 11 px ;
font-family : monospace ;
padding : 5 px ;
z-index : 9999 ;
display : none ;
}
/* Прозрачные зоны для тачей поверх всего */
# touch-left , # touch-right , # touch-center {
/* Прозрачные зоны для тачей */
. touch-zone {
position : fixed ;
top : 0 ;
height : 100 % ;
@@ -138,166 +121,154 @@
< span id = "error-text" > < / span >
< / div >
< / div >
<!-- Невидимые зоны тачей поверх iframe epub.js -- >
< div id = "touch-left " > < / div >
< div id = "touch-center " > < / div >
< div id = "touch-right" > < / div >
< div id = "touch-left" class = "touch-zone" > < / div >
< div id = "touch-center" class = "touch-zone " > < / div >
< div id = "touch-right" class = "touch-zone " > < / div >
< script src = "_framework/hybridwebview.js" > < / script >
< script src = "js/jszip.min.js" > < / script >
< script src = "js/epub.min.js" > < / script >
< script >
// ========== DEBUG LOGGING ==========
( function ( ) {
'use strict' ;
// ========== КЭШИРОВАННЫЕ DOM-ЭЛЕМЕНТЫ ==========
const $ = id => document . getElementById ( id ) ;
const els = {
loading : $ ( 'loading' ) ,
loadingText : $ ( 'loading-text' ) ,
errorDisplay : $ ( 'error-display' ) ,
errorText : $ ( 'error-text' ) ,
bookContent : $ ( 'book-content' ) ,
fb2Content : $ ( 'fb2-content' ) ,
} ;
// ========== СОСТОЯНИЕ ==========
const state = {
book : null ,
rendition : null ,
currentCfi : null ,
totalPages : 0 ,
currentPage : 0 ,
bookFormat : '' ,
isBookLoaded : false ,
fb2CurrentPage : 0 ,
fb2TotalPages : 1 ,
} ;
// ========== УТИЛИТЫ ==========
function debugLog ( msg ) {
console . log ( '[Reader] ' + msg ) ;
}
function showError ( msg ) {
document . getElementById ( ' loading' ) . style . display = 'none' ;
document . getElementById ( ' error-d isplay' ) . style . display = 'flex' ;
document . getElementById ( ' error-t ext' ) . textContent = msg ;
els . loading . style . display = 'none' ;
els . errorD isplay . style . display = 'flex' ;
els . errorT ext . textContent = msg ;
debugLog ( 'ERROR: ' + msg ) ;
}
function setLoadingText ( msg ) {
var el = document . getElementById ( 'loading-text' ) ;
if ( el ) el . textContent = msg ;
if ( els . loadingText ) els . loadingText . textContent = msg ;
debugLog ( msg ) ;
}
// Один div для escapeHtml — переиспользуем
const _escDiv = document . createElement ( 'div' ) ;
function escapeHtml ( text ) {
_escDiv . textContent = text ;
return _escDiv . innerHTML ;
}
function base64ToArrayBuffer ( base64 ) {
const bin = atob ( base64 ) ;
const len = bin . length ;
const buf = new ArrayBuffer ( len ) ;
const view = new Uint8Array ( buf ) ;
// Обработка блоками для больших файлов — снижает давление на GC
for ( let i = 0 ; i < len ; i ++ ) {
view [ i ] = bin . charCodeAt ( i ) ;
}
return buf ;
}
// ========== MESSAGE BRIDGE ==========
function sendMessage ( action , data ) {
var message = JSON . stringify ( { action : action , data : data || { } } ) ;
const message = JSON . stringify ( { action , data : data || { } } ) ;
try {
// .NET MAUI HybridWebView uses hybridWebViewHost
if ( window . HybridWebView && typeof window . HybridWebView . SendRawMessage === 'function' ) {
window . HybridWebView . SendRawMessage ( message ) ;
return ;
}
debugLog ( 'No bridge method found on hybridWebViewHost' ) ;
} catch ( e ) {
debugLog ( 'Bridge error: ' + e . message ) ;
}
}
// ========== TOUCH ZONES ==========
document . getElementById ( 'touch-left' ) . addEventListener ( 'click' , function ( e ) {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
prevPage ( ) ;
} ) ;
// ========== TOUCH / SWIPE ==========
let swipeStartX = 0 , swipeStartY = 0 , swipeStartTime = 0 ;
document . getElementById ( 'touch-right' ) . addEventListener ( 'click' , fun ction ( e ) {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
nextPage ( ) ;
} ) ;
document . getElementById ( 'touch-center' ) . addEventListener ( 'click' , function ( e ) {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
sendMessage ( 'toggleMenu' , { } ) ;
} ) ;
// Свайпы на зонах
var swipeStartX = 0 , swipeStartY = 0 , swipeStartTime = 0 ;
function onSwipeStart ( e ) {
var touch = e . touches ? e . touches [ 0 ] : e ;
swipeStartX = touch . clientX ;
swipeStartY = touch . clientY ;
swipeStartTime = Date . now ( ) ;
}
function onSwipeEnd ( e ) {
var touch = e . changedTouches ? e . changedTouches [ 0 ] : e ;
var dx = touch . clientX - swipeStartX ;
var dy = touch . clientY - swipeStartY ;
var dt = Date . now ( ) - swipeStartTime ;
if ( dt > 500 ) return ;
if ( Math . abs ( dx ) > Math . abs ( dy ) && Math . abs ( dx ) > 50 ) {
if ( dx < 0 ) nextPage ( ) ;
else prevPage ( ) ;
}
}
[ 'touch-left' , 'touch-center' , 'touch-right' ] . forEach ( function ( id ) {
var el = document . getElementById ( id ) ;
el . addEventListener ( 'touchstart' , onSwipeStart , { passive : true } ) ;
el . addEventListener ( 'touchend' , onSwipeEnd , { passive : true } ) ;
} ) ;
// ========== EPUB READER ==========
var book = null ;
var rendition = null ;
var currentCfi = null ;
var totalPages = 0 ;
var currentPage = 0 ;
var bookFormat = '' ;
var isBookLoaded = false ;
window . loadBookFromBase64 = function ( base64Data , format , lastPosition ) {
debugLog ( 'loadBookFromBase64: format=' + format + ', len=' + ( base64Data ? base64Data . length : 0 ) ) ;
bookFormat = format ;
if ( format === 'epub' ) {
loadEpubFromBase64 ( base64Data , lastPosition ) ;
} else if ( format === 'fb2' ) {
loadFb2FromBase64 ( base64Data , lastPosition ) ;
} else {
showError ( 'Unsupported format: ' + format ) ;
}
const touchA ctions = {
'touch-left' : ( ) => window . prevPage ( ) ,
'touch-right' : ( ) => window . nextPage ( ) ,
'touch-center' : ( ) => sendMessage ( 'toggleMenu' , { } ) ,
} ;
function base64ToArrayBuffer ( base64 ) {
var binaryString = atob ( base64 ) ;
var len = binaryString . length ;
var bytes = new Uint8Array ( len ) ;
for ( var i = 0 ; i < len ; i ++ ) {
bytes [ i ] = binaryString . charCodeAt ( i ) ;
function initTouchZones ( ) {
Object . keys ( touchActions ) . forEach ( id => {
const el = $ ( id ) ;
if ( ! el ) return ;
el . addEventListener ( 'click' , e => {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
touchActions [ id ] ( ) ;
} ) ;
el . addEventListener ( 'touchstart' , e => {
const t = e . touches [ 0 ] ;
swipeStartX = t . clientX ;
swipeStartY = t . clientY ;
swipeStartTime = Date . now ( ) ;
} , { passive : true } ) ;
el . addEventListener ( 'touchend' , e => {
const t = e . changedTouches [ 0 ] ;
const dx = t . clientX - swipeStartX ;
const dt = Date . now ( ) - swipeStartTime ;
if ( dt < 500 && Math . abs ( dx ) > Math . abs ( t . clientY - swipeStartY ) && Math . abs ( dx ) > 50 ) {
dx < 0 ? window . nextPage ( ) : window . prevPage ( ) ;
}
return bytes . buffer ;
} , { passive : true } ) ;
} ) ;
}
// ========== EPUB ==========
function loadEpubFromBase64 ( base64Data , lastCfi ) {
setLoadingText ( 'Decoding EPUB...' ) ;
try {
var arrayBuffer = base64ToArrayBuffer ( base64Data ) ;
const arrayBuffer = base64ToArrayBuffer ( base64Data ) ;
debugLog ( 'EPUB size: ' + arrayBuffer . byteLength + ' bytes' ) ;
// Check JSZip
if ( typeof JSZip === 'undefined' ) {
showError ( 'JSZip not loaded! Cannot open EPUB.' ) ;
return ;
}
debugLog ( 'JSZip version: ' + ( JSZip . version || 'unknown' ) ) ;
// Check epub.js
if ( typeof ePub === 'undefined' ) {
showError ( 'epub.js not loaded!' ) ;
return ;
}
debugLog ( 'ePub function available' ) ;
if ( typeof JSZip === 'undefined' ) { showError ( 'JSZip not loaded!' ) ; return ; }
if ( typeof ePub === 'undefined' ) { showError ( 'epub.js not loaded!' ) ; return ; }
setLoadingText ( 'Opening EPUB...' ) ;
// First unzip manually to verify the file is valid
JSZip . loadAsync ( arrayBuffer ) . then ( function ( zip ) {
debugLog ( 'ZIP opened, files: ' + Object . keys ( zip . files ) . length ) ;
// epub.js сам умеет работать с ArrayBuffer — двойная распаковка не нужна
state . book = ePub ( arrayBuffer ) ;
// Now use epub.js
book = ePub ( arrayBuffer ) ;
els . fb2Content . style . display = 'none' ;
els . bookContent . style . display = 'block' ;
document . getElementById ( 'fb2-content' ) . style . display = 'none' ;
document . getElementById ( 'book-content' ) . style . display = 'block' ;
rendition = book . renderTo ( 'book-content' , {
state . rendition = state . book . renderTo ( 'book-content' , {
width : '100%' ,
height : '100%' ,
spread : 'none' ,
flow : 'paginated'
} ) ;
rendition . themes . default ( {
state . rendition . themes . default ( {
'body' : {
'font-family' : 'serif !important' ,
'font-size' : '18px !important' ,
@@ -306,129 +277,110 @@
'background-color' : '#faf8ef !important' ,
'color' : '#333 !important'
} ,
'p' : {
'text-indent' : '1.5em' ,
'margin-bottom' : '0.5em'
}
'p' : { 'text-indent' : '1.5em' , 'margin-bottom' : '0.5em' }
} ) ;
book . ready . then ( function ( ) {
state . book. ready
. then ( ( ) => {
debugLog ( 'EPUB ready' ) ;
document . getElementById ( ' loading' ) . style . display = 'none' ;
els . loading . style . display = 'none' ;
try {
var toc = book . navigation . toc || [ ] ;
var chapters = toc . map ( function ( ch ) {
return { label : ( ch . label || '' ) . trim ( ) , href : ch . href || '' } ;
} ) ;
sendMessage ( 'chaptersLoaded' , { chapters : chapters } ) ;
debugLog ( 'TOC chapters: ' + chapters . length ) ;
const toc = state . book . navigation . toc || [ ] ;
const chapters = toc . map ( ch => ( {
label : ( ch . label || '' ) . trim ( ) ,
href : ch . href || ''
} ) ) ;
sendMessage ( 'chaptersLoaded' , { chapters } ) ;
} catch ( e ) {
debugLog ( 'TOC error: ' + e . message ) ;
}
return book . locations . generate ( 1600 ) ;
} ) . then ( function ( ) {
totalPages = book . locations . length ( ) ;
debugLog ( ' Pages: ' + totalPages ) ;
sendMessage ( 'bookReady' , { totalPages : totalPages } ) ;
isBookLoaded = true ;
} ) . catch ( function ( e ) {
debugLog ( 'Book ready error: ' + e . message ) ;
} ) ;
return state . book . locations . generate ( 1600 ) ;
} )
. then ( ( ) => {
state . total Pages = state . book . locations . length ( ) ;
debugLog ( 'Pages: ' + state . totalPages ) ;
sendMessage ( 'bookReady' , { totalPages : state . totalPages } ) ;
state . isBookLoaded = true ;
} )
. catch ( e => debugLog ( 'Book ready error: ' + e . message ) ) ;
rendition . on ( 'relocated' , function ( location ) {
state . rendition. on ( 'relocated' , location => {
if ( ! location || ! location . start ) return ;
currentCfi = location . start . cfi ;
state . currentCfi = location . start . cfi ;
try {
var progress = book . locations . percentageFromCfi ( currentCfi ) || 0 ;
currentPage = location . start . location || 0 ;
sendMessage ( 'progressUpdate' , {
progress : progress ,
cfi : currentCfi ,
currentPage : currentPage ,
totalPages : totalPages ,
progress : state . book . locations . percentageFromCfi ( state . currentCfi ) || 0 ,
cfi : state . currentCfi,
currentPage : location . start . location || 0 ,
totalPages : state . totalPages,
chapter : location . start . href || ''
} ) ;
} catch ( e ) { }
} catch ( e ) {
debugLog ( 'Relocated error: ' + e . message ) ;
}
} ) ;
setLoadingText ( 'Rendering...' ) ;
if ( lastCfi && lastCfi . length > 0 && lastCfi !== 'null' && lastCfi !== ' undefined' ) {
rendition . display ( lastCfi ) ;
} else {
rendition . display ( ) ;
}
} ) . catch ( function ( e ) {
showError ( 'ZIP error: ' + e . message ) ;
} ) ;
const displayTarget = ( lastCfi && lastCfi !== 'null' && lastCfi !== 'undefined' )
? lastCfi : undefined ;
state . rendition . display ( displayTarget ) ;
} catch ( e ) {
showError ( 'EPUB load error: ' + e . message ) ;
}
}
// ========== FB2 ==========
function loadFb2FromBase64 ( base64Data , lastPosition ) {
setLoadingText ( 'Parsing FB2...' ) ;
try {
var arrayBuffer = base64ToArrayBuffer ( base64Data ) ;
var bytes = new Uint8Array ( arrayBuffer ) ;
const arrayBuffer = base64ToArrayBuffer ( base64Data ) ;
const bytes = new Uint8Array ( arrayBuffer ) ;
var xmlText ;
try {
xmlText = new TextDecoder ( 'utf-8' ) . decode ( bytes ) ;
} catch ( e ) {
xmlText = new TextDecoder ( 'windows-1251' ) . decode ( bytes ) ;
}
let xmlText = new TextDecoder ( 'utf-8' ) . decode ( bytes ) ;
// Check encoding declaration and re-decode if needed
var encoding Match = xmlText . match ( /encoding=["\']([^"\']+)["\']/i ) ;
if ( encoding Match ) {
var declaredEncoding = encoding Match [ 1 ] . toLowerCase ( ) ;
debugLog ( 'FB2 encoding: ' + declaredEncoding ) ;
if ( declaredEncoding !== 'utf-8' ) {
try {
xmlText = new TextDecoder ( declaredEncoding ) . decod e( bytes ) ;
} catch ( e ) {
debugLog ( 'Re-decode error: ' + e . message ) ;
}
// Перекодируем если нужно
const encMatch = xmlText . match ( /encoding=["\']([^"\']+)["\']/i ) ;
if ( encMatch ) {
const enc = encMatch [ 1 ] . toLowerCase ( ) ;
debugLog ( 'FB2 encoding: ' + enc ) ;
if ( enc !== 'utf-8' ) {
try { xmlText = new TextDecoder ( enc ) . decode ( bytes ) ; }
catch ( e ) { debugLog ( 'Re-decode error: ' + e. message ) ; }
}
}
document . getElementById ( ' book-c ontent' ) . style . display = 'none' ;
document . getElementById ( ' fb2-c ontent' ) . style . display = 'block' ;
els . bookC ontent . style . display = 'none' ;
els . fb2C ontent . style . display = 'block' ;
var parser = new DOMParser ( ) ;
var doc = parser . parseFromString ( xmlText , 'text/xml' ) ;
var parseError = doc . querySelector ( 'parsererror' ) ;
if ( parseError ) {
const doc = new DOMParser ( ) . parseFromString ( xmlText , 'text/xml' ) ;
if ( doc . querySelector ( 'parsererror' ) ) {
showError ( 'FB2 XML parse error' ) ;
debugLog ( parseError . textContent . substring ( 0 , 200 ) ) ;
return ;
}
var fb2Html = parseFb2Document ( doc ) ;
var fb2Conta iner = document . getElementById ( 'fb2-content' ) ;
fb2Container . innerHTML = fb2Html . html ;
const fb2Html = parseFb2Document ( doc ) ;
els . fb2Content . inn erHTML = fb2Html . html ;
els . loading . style . display = 'none' ;
document . getElementById ( 'loading' ) . style . display = 'none' ;
setTimeout ( function ( ) {
// Используем requestAnimationFrame вместо setTimeout для точного тайминга
requestAnimationFrame ( ( ) => {
requestAnimationFrame ( ( ) => {
setupFb2Pagination ( ) ;
sendMessage ( 'chaptersLoaded' , { chapters : fb2Html . chapters } ) ;
sendMessage ( 'bookReady' , { totalPages : totalPages } ) ;
isBookLoaded = true ;
bookFormat = 'fb2' ;
sendMessage ( 'bookReady' , { totalPages : state . totalPages } ) ;
state . isBookLoaded = true ;
state . bookFormat = 'fb2' ;
if ( lastPosition && parseFloat ( lastPosition ) > 0 ) {
goToFb2Position ( parseFloat ( lastPosition ) ) ;
}
updateFb2Progress ( ) ;
}, 500 ) ;
} ) ;
} ) ;
} catch ( e ) {
showError ( 'FB2 error: ' + e . message ) ;
@@ -436,299 +388,289 @@
}
// ========== FB2 PARSER ==========
function parseFb2Document ( doc ) {
var chapters = [ ] ;
var html = '<div id="fb2-inner" style="font-size:18px; font-family:serif; line-height:1.6;"> ';
// Вспомогательная: получить локальное имя тега без namespace
function localTag ( el ) {
return el . tagName ? el . tagName . toLowerCase ( ) . replace ( /.*:/ , '' ) : ' ';
}
var bodies = doc . querySelectorAll ( 'body' ) ;
if ( bodies . length === 0 ) {
function parseFb2Document ( doc ) {
const chapters = [ ] ;
const parts = [ '<div id="fb2-inner" style="font-size:18px;font-family:serif;line-height:1.6;">' ] ;
let bodies = doc . querySelectorAll ( 'body' ) ;
if ( ! bodies . length ) {
bodies = doc . getElementsByTagNameNS ( 'http://www.gribuser.ru/xml/fictionbook/2.0' , 'body' ) ;
}
var chapterIndex = 0 ;
for ( var b = 0 ; b < bodies . length ; b ++ ) {
var children = bodies [ b ] . children ;
for ( var s = 0 ; s < children . length ; s ++ ) {
var child = children [ s ] ;
var tagName = child . tagName ? child . tagName . toLowerCase ( ) .replace ( /.*:/ , '' ) : '' ;
if ( tagName === 'section' ) {
var result = parseFb2Section ( child , chapterIndex ) ;
html += result . html ;
for ( var c = 0 ; c < result . chapters . length ; c ++ ) {
chapters . push ( result . chapters [ c ] ) ;
}
let chapterIndex = 0 ;
for ( const body of bodies ) {
for ( const child of body . children ) {
if ( localTag ( child) === 'section' ) {
const result = parseFb2Section ( child , chapterIndex ) ;
parts . push ( result . html ) ;
chapters . push ( .. .result . chapters ) ;
chapterIndex += Math . max ( result . chapters . length , 1 ) ;
}
}
}
html += '</div>' ;
if ( chapters . length === 0 ) chapters . push ( { label : 'Start' , href : '0' } ) ;
return { html : html , chapters : chapters } ;
parts . push ( '</div>' ) ;
if ( ! chapters . length ) chapters . push ( { label : 'Start' , href : '0' } ) ;
return { html : parts . join ( '' ) , chapters } ;
}
// Маппинг тегов FB2 → генераторы HTML
const sectionTagHandlers = {
title ( child , idx , chapters ) {
const text = ( child . textContent || '' ) . trim ( ) ;
chapters . push ( { label : text , href : idx . toString ( ) } ) ;
return ` <h2 class="fb2-title" data-chapter=" ${ idx } " style="text-align:center;margin:1em 0 .5em"> ${ escapeHtml ( text ) } </h2> ` ;
} ,
p ( child ) {
return ` <p style="text-indent:1.5em;margin-bottom:.3em"> ${ getInlineHtml ( child ) } </p> ` ;
} ,
'empty-line' ( ) { return '<br/>' ; } ,
subtitle ( child ) {
return ` <h3 style="text-align:center;margin:.8em 0"> ${ escapeHtml ( child . textContent || '' ) } </h3> ` ;
} ,
epigraph ( child ) {
return ` <blockquote style="font-style:italic;margin:1em 2em"> ${ parseInnerParagraphs ( child ) } </blockquote> ` ;
} ,
poem ( child ) {
return ` <div style="margin:1em 2em"> ${ parsePoem ( child ) } </div> ` ;
} ,
cite ( child ) {
return ` <blockquote style="margin:1em 2em;padding-left:1em;border-left:3px solid #ccc"> ${ parseInnerParagraphs ( child ) } </blockquote> ` ;
} ,
} ;
function parseFb2Section ( section , startIndex ) {
var chapters = [ ] ;
var html = ' <div class="fb2-section" data-section="' + startIndex + '">' ;
const chapters = [ ] ;
const parts = [ ` <div class="fb2-section" data-section="${ startIndex } "> ` ] ;
var children = section . children ;
for ( var i = 0 ; i < children . length ; i ++ ) {
var child = children [ i ] ;
var tag = child . tagName ? child . tagName . toLowerCase ( ) . replace ( /.*:/ , '' ) : '' ;
for ( const child of section . children ) {
const tag = localTag ( child ) ;
switch ( tag ) {
case 'title' :
var titleText = ( child . textContent || '' ) . trim ( ) ;
html += '<h2 class="fb2-title" data-chapter="' + startIndex + '" style="text-align:center; margin:1em 0 0.5em;">' + escapeHtml ( titleText ) + '</h2>' ;
chapters . push ( { label : titleText , href : startIndex . toStrin g ( ) } ) ;
break ;
case 'p' :
html += '<p style="text-indent:1.5em; margin-bottom:0.3em;">' + getInlineHtml ( child ) + '</p>' ;
break ;
case 'empty-line' :
html += '<br/>' ;
break ;
case 'subtitle' :
html += '<h3 style="text-align:center; margin:0.8em 0;">' + escapeHtml ( child . textContent || '' ) + '</h3>' ;
break ;
case 'epigraph' :
html += '<blockquote style="font-style:italic; margin:1em 2em;">' + parseInnerParagraphs ( child ) + '</blockquote>' ;
break ;
case 'poem' :
html += '<div style="margin:1em 2em;">' + parsePoem ( child ) + '</div>' ;
break ;
case 'cite' :
html += '<blockquote style="margin:1em 2em; padding-left:1em; border-left:3px solid #ccc;">' + parseInnerParagraphs ( child ) + '</blockquote>' ;
break ;
case 'section' :
var sub = parseFb2Section ( child , startIndex + chapters . length ) ;
html += sub . html ;
for ( var j = 0 ; j < sub . chapters . length ; j ++ ) chapters . push ( sub . chapters [ j ] ) ;
break ;
if ( tag === 'section' ) {
const sub = parseFb2Section ( child , startIndex + chapters . length ) ;
parts . push ( sub . html ) ;
chapters . push ( ... sub . chapters ) ;
} else if ( sectionTagHandlers [ ta g ] ) {
parts . push ( sectionTagHandlers [ tag ] ( child , startIndex + chapters . length , chapters ) ) ;
}
}
html += '</div>' ;
return { html : html , chapters : chapters } ;
parts . push ( '</div>' ) ;
return { html : parts . join ( '' ) , chapters } ;
}
function getInlineHtml ( el ) {
var result = '' ;
for ( var i = 0 ; i < el . childNodes . length ; i ++ ) {
var node = el . childNodes [ i ] ;
const parts = [ ] ;
for ( const node of el . childNodes ) {
if ( node . nodeType === 3 ) {
result += escapeHtml ( node . textContent ) ;
parts . push ( escapeHtml ( node . textContent ) ) ;
} else if ( node . nodeType === 1 ) {
var tag = node . tagName . toLowerCase ( ) . replace ( /.*:/ , '' ) ;
if ( tag === 'strong' || tag === 'bold' ) result += '<strong>' + getInlineHtml ( node ) + '</strong>' ;
else if ( tag === 'emphasis ' || tag === 'em ' ) result += '<em>' + getInlineHtml ( node ) + '</em>' ;
else if ( tag === 'strikethrough' ) result + = '<s>' + getInlineHtml ( node ) + '</s>' ;
else result + = getInlineHtml ( node ) ;
const tag = localTag ( node ) ;
const inner = getInlineHtml ( node ) ;
if ( tag === 'strong ' || tag === 'bold ' ) parts . push ( ` <strong> ${ inner } </strong> ` ) ;
else if ( tag === 'emphasis' || tag == = 'em' ) parts . push ( ` <em> ${ inner } </em> ` ) ;
else if ( tag == = 'strikethrough' ) parts . push ( ` <s> ${ inner } </s> ` ) ;
else parts . push ( inner ) ;
}
}
return result ;
return parts . join ( '' ) ;
}
function parseInnerParagraphs ( el ) {
var html = '' ;
for ( var i = 0 ; i < el . children . length ; i ++ ) {
var child = el . children [ i ] ;
var tag = child . tagName ? child . tagName . toLowerCase ( ) . replace ( /.*:/ , '' ) : '' ;
if ( tag === 'p' ) html += '<p>' + getInlin eHtml ( child ) + '</p> ' ;
else if ( tag === 'text-author' ) html += '<p style="text-align:right; font-style:italic;">— ' + escapeHtml ( child . textContent || '' ) + '</p>' ;
const parts = [ ] ;
for ( const child of el . children ) {
const tag = localTag ( child) ;
if ( tag === 'p' ) parts . push ( ` <p> ${ getInlineHtml ( child ) } </p> ` ) ;
else if ( tag === 'text-author' ) parts . push ( ` <p style="text-align:right;font-style:italic">— ${ escap eHtml ( child . textContent || '') } </p> ` ) ;
}
return html ;
return parts . join ( '' ) ;
}
function parsePoem ( el ) {
var html = '' ;
for ( var i = 0 ; i < el . children . length ; i ++ ) {
var child = el . children [ i ] ;
var tag = child . tagName ? child . tagName . toLowerCase ( ) . replace ( /.*:/ , '' ) : '' ;
const parts = [ ] ;
for ( const child of el . children ) {
const tag = localTag ( child) ;
if ( tag === 'stanza' ) {
html += '<div style="margin-bottom:1em; ">' ;
for ( var j = 0 ; j < child . children . length ; j ++ ) {
var v = child . children [ j ] ;
if ( v . tagName && v . tagName . toLowerCase ( ) . replace ( /.*:/ , '' ) === 'v' )
html += '<p style="text-indent:0;">' + escapeHtml ( v . textContent || '' ) + '</p>' ;
parts . push ( '<div style="margin-bottom:1em">' ) ;
for ( const v of child . children ) {
if ( localTag ( v ) === 'v' ) parts . push ( ` <p style="text-indent:0"> ${ escapeHtml ( v . textContent || '' ) } </p> ` ) ;
}
html += '</div>' ;
parts . push ( '</div>' ) ;
} else if ( tag === 'title' ) {
html += '<h4>' + escapeHtml ( child . textContent || '' ) + '</h4>' ;
parts . push ( ` <h4> ${ escapeHtml ( child . textContent || '' ) } </h4> ` ) ;
}
}
return html ;
}
function escapeHtml ( text ) {
var div = document . createElement ( 'div' ) ;
div . textContent = text ;
return div . innerHTML ;
return parts . join ( '' ) ;
}
// ========== FB2 PAGINATION ==========
var fb2CurrentPage = 0 ;
var fb2TotalPages = 1 ;
function setupFb2Pagination ( ) {
var container = document . getElementById ( ' fb2-c ontent' ) ;
var inner = document . getElementById ( 'fb2-inner' ) ;
const container = els . fb2C ontent;
const inner = $ ( 'fb2-inner' ) ;
if ( ! container || ! inner ) return ;
var w = container . clientWidth ;
var h = container . clientHeight ;
const w = container . clientWidth ;
const h = container . clientHeight ;
inner . style . columnWidth = w + 'px' ;
inner . style . columnGap = '40 px' ;
inner . style . columnFill = 'auto ';
inner . style . height = h + 'px ';
inner . style . overflow = 'hidden ' ;
Object . assign ( inner . style , {
columnWidth : w + 'px' ,
columnGap : '40px ' ,
columnFill : 'auto ' ,
height : h + 'px ' ,
overflow : 'hidden' ,
} ) ;
setTimeout ( function ( ) {
fb2TotalPages = Math . max ( 1 , Math . ceil ( inner . scrollWidth / w ) ) ;
totalPages = fb2TotalPages ;
fb2Current Page = 0 ;
// Даём браузеру отрисовать колонки
requestAnimationFrame ( ( ) => {
state . fb2TotalPages = Math . max ( 1 , Math . ceil ( inner . scrollWidth / w ) ) ;
state . total Pages = state . fb2TotalPages ;
state . fb2CurrentPage = 0 ;
showFb2Page ( 0 ) ;
debugLog ( 'FB2 pages: ' + fb2TotalPages ) ;
}, 300 ) ;
debugLog ( 'FB2 pages: ' + state . fb2TotalPages ) ;
} ) ;
}
function showFb2Page ( idx ) {
if ( idx < 0 ) idx = 0 ;
if ( idx >= fb2TotalPages ) idx = fb2Total Pages - 1 ;
fb2CurrentPage = idx ;
var inner = document . getElementById ( 'fb2- inner' ) ;
var container = document . getElementById ( ' fb2-c ontent' ) ;
if ( inner && container ) {
inner . style . transform = 'translateX(-' + ( idx * container . clientWidth ) + 'px)' ;
idx = Math . max ( 0 , Math . min ( idx , state . fb2TotalPages - 1 ) ) ;
state . fb2Current Page = idx ;
const inner = $ ( 'fb2-inner' ) ;
if ( inner ) {
inner . style . transform = ` translateX(- ${ idx * els . fb2C ontent . clientWidth } px) ` ;
}
}
function updateFb2Progress ( ) {
var progress = fb2TotalPages > 1 ? fb2CurrentPage / ( fb2TotalPages - 1 ) : 0 ;
const total = state . fb2TotalPages ;
const progress = total > 1 ? state . fb2CurrentPage / ( total - 1 ) : 0 ;
sendMessage ( 'progressUpdate' , {
progress : progress ,
progress ,
cfi : progress . toString ( ) ,
currentPage : fb2CurrentPage + 1 ,
totalPages : fb2TotalPages ,
currentPage : state . fb2CurrentPage + 1 ,
totalPages : total ,
chapter : getCurrentFb2Chapter ( )
} ) ;
}
function getCurrentFb2Chapter ( ) {
var inner = document . getElementById ( 'fb2-inner' ) ;
var container = document . getElementById ( ' fb2-c ontent' ) ;
const inner = $ ( 'fb2-inner' ) ;
const container = els . fb2C ontent;
if ( ! inner || ! container ) return '' ;
var offset = fb2CurrentPage * container . clientWidth ;
var c h = '' ;
var titles = inner . querySelectorAll ( '.fb2-title' ) ;
for ( var i = 0 ; i < titles . length ; i ++ ) {
i f ( titles [ i ] . offsetLeft < = offset + container . clientWid th) ch = titles [ i ] . textContent ;
const maxOffset = state . fb2CurrentPage * container . clientWidt h + container . clientWidth ;
let chapter = '' ;
const titles = inner . querySelectorAll ( '.fb2-title' ) ;
for ( let i = 0 ; i < titles . leng th; i ++ ) {
if ( titles [ i ] . offsetLeft <= maxOffset ) chapter = titles [ i ] . textContent ;
else break ; // Заголовки идут по порядку — можно прервать
}
return ch ;
return chapter ;
}
function goToFb2Position ( progress ) {
showFb2Page ( Math . floor ( progress * ( fb2TotalPages - 1 ) ) ) ;
showFb2Page ( Math . round ( progress * ( state . fb2TotalPages - 1 ) ) ) ;
}
// ========== PUBLIC API ==========
window . loadBookFromBase64 = function ( base64Data , format , lastPosition ) {
debugLog ( ` loadBookFromBase64: format= ${ format } , len= ${ base64Data ? base64Data . length : 0 } ` ) ;
state . bookFormat = format ;
if ( format === 'epub' ) loadEpubFromBase64 ( base64Data , lastPosition ) ;
else if ( format === 'fb2' ) loadFb2FromBase64 ( base64Data , lastPosition ) ;
else showError ( 'Unsupported format: ' + format ) ;
} ;
window . nextPage = function ( ) {
if ( bookFormat === 'epub' && rendition ) rendition . next ( ) ;
else if ( bookFormat === 'fb2' && fb2CurrentPage < fb2TotalPages - 1 ) {
showFb2Pag e( fb2CurrentPage + 1 ) ;
if ( state . bookFormat === 'epub' && state . rendition ) {
state . rendition . next ( ) ;
} else if ( state . bookFormat === 'fb2' && stat e. fb2CurrentPage < state . fb2TotalPages - 1 ) {
showFb2Page ( state . fb2CurrentPage + 1 ) ;
updateFb2Progress ( ) ;
}
} ;
window . prevPage = function ( ) {
if ( bookFormat === 'epub' && rendition ) rendition . prev ( ) ;
else if ( bookFormat === 'fb2' && fb2CurrentPage > 0 ) {
showFb2Pag e( fb2CurrentPage - 1 ) ;
if ( state . bookFormat === 'epub' && state . rendition ) {
state . rendition . prev ( ) ;
} else if ( state . bookFormat === 'fb2' && stat e. fb2CurrentPage > 0 ) {
showFb2Page ( state . fb2CurrentPage - 1 ) ;
updateFb2Progress ( ) ;
}
} ;
window . setFontSize = function ( size ) {
if ( bookFormat === 'epub' && rendition ) rendition . themes . fontSize ( size + 'px' ) ;
else if ( bookFormat === 'fb2 ' ) {
var inner = document . getElementById ( 'fb2-inner ') ;
if ( inner ) { inner . style . fontSize = size + 'px' ; setTimeout ( setupFb2Pagination , 100 ) ; }
if ( state . bookFormat === 'epub' && state . rendition ) {
state . rendition . themes . fontSize ( size + 'px ' ) ;
} else if ( state . bookFormat === 'fb2 ') {
const inner = $ ( 'fb2-inner' ) ;
if ( inner ) {
inner . style . fontSize = size + 'px' ;
requestAnimationFrame ( setupFb2Pagination ) ;
}
}
} ;
window . setFontFamily = function ( family ) {
if ( bookFormat === 'epub' && rendition ) rendition . themes . font ( family ) ;
else if ( bookFormat === 'fb2' ) {
var inner = document . getElementById ( 'fb2-inner ') ;
if ( inner ) { inner . style . fontFamily = family ; setTimeout ( setupFb2Pagination , 100 ) ; }
if ( state . bookFormat === 'epub' && state . rendition ) {
state . rendition . themes . font ( family ) ;
} else if ( state . bookFormat === 'fb2 ') {
const inner = $ ( 'fb2-inner' ) ;
if ( inner ) {
inner . style . fontFamily = family ;
requestAnimationFrame ( setupFb2Pagination ) ;
}
}
} ;
window . goToChapter = function ( href ) {
if ( bookFormat === 'epub' && rendition ) rendition . display ( href ) ;
else if ( bookFormat === 'fb2' ) {
var inner = document . getElementById ( 'fb2-inner ') ;
var container = document . getElementById ( 'fb2-content ' ) ;
if ( state . bookFormat === 'epub' && state . rendition ) {
state . rendition . display ( href ) ;
} else if ( state . bookFormat === 'fb2 ') {
const inner = $ ( 'fb2-inner ' ) ;
const container = els . fb2Content ;
if ( inner && container ) {
var el = inner . querySelector ( ' [data-chapter="' + href + '"]' ) ;
if ( el ) { showFb2Page ( Math . floor ( el . offsetLeft / container . clientWidth ) ) ; updateFb2Progress ( ) ; }
const el = inner . querySelector ( ` [data-chapter="${ href } "] ` ) ;
if ( el ) {
showFb2Page ( Math . floor ( el . offsetLeft / container . clientWidth ) ) ;
updateFb2Progress ( ) ;
}
}
}
} ;
window . getProgress = function ( ) {
if ( bookFormat === 'epub' && book && currentCfi ) {
if ( state . bookFormat === 'epub' && state . book && state . currentCfi ) {
try {
return JSON . stringify ( { progress : book . locations . percentageFromCfi ( currentCfi ) || 0 , cfi : currentCfi , currentPage : currentPage , totalPages : totalPages } ) ;
return JSON . stringify ( {
progress : state . book . locations . percentageFromCfi ( state . currentCfi ) || 0 ,
cfi : state . currentCfi ,
currentPage : state . currentPage ,
totalPages : state . totalPages
} ) ;
} catch ( e ) { return '{}' ; }
} else if ( bookFormat === 'fb2' ) {
var p = fb2TotalPages > 1 ? fb2CurrentPage / ( fb2TotalPages - 1 ) : 0 ;
return JSON . stringify ( { progress : p , cfi : p . toString ( ) , currentPage : fb2CurrentPage + 1 , totalPages : fb2TotalPages } ) ;
} else if ( state . bookFormat === 'fb2' ) {
const p = state . fb2TotalPages > 1 ? state . fb2CurrentPage / ( state . fb2TotalPages - 1 ) : 0 ;
return JSON . stringify ( {
progress : p ,
cfi : p . toString ( ) ,
currentPage : state . fb2CurrentPage + 1 ,
totalPages : state . fb2TotalPages
} ) ;
}
return '{}' ;
} ;
// ========== INIT ==========
debugLog ( 'Page loaded' ) ;
// Log hybridWebViewHost details
if ( window . HybridWebView ) {
debugLog ( 'hybridWebView found!' ) ;
var methods = [ ] ;
for ( var k in window . HybridWebView ) {
methods . push ( k + ':' + typeof window . HybridWebView [ k ] ) ;
}
debugLog ( 'Methods: ' + methods . join ( ', ' ) ) ;
}
initTouchZones ( ) ;
setLoadingText ( 'Waiting for book...' ) ;
sendMessage ( 'readerReady' , { } ) ;
< / script >
<!-- IMPORTANT: Load JSZip BEFORE epub.js -->
<!-- JSZip 3.x is required by epub.js -->
< script >
// Verify after script load
debugLog ( 'Checking libraries after inline script...' ) ;
< / script >
< script src = "js/jszip.min.js" onload = "debugLog('JSZip loaded: ' + typeof JSZip)" onerror = "debugLog('ERROR: jszip.min.js failed to load')" > < / script >
< script >
// After JSZip loads, verify it
if ( typeof JSZip !== 'undefined' ) {
debugLog ( 'JSZip OK, version: ' + ( JSZip . version || 'unknown' ) ) ;
} else {
debugLog ( 'JSZip NOT available after script tag' ) ;
}
< / script >
< script src = "js/epub.min.js" onload = "debugLog('epub.js loaded: ' + typeof ePub)" onerror = "debugLog('ERROR: epub.min.js failed to load')" > < / script >
< script >
if ( typeof ePub !== 'undefined' ) {
debugLog ( 'ePub OK' ) ;
} else {
debugLog ( 'ePub NOT available after script tag' ) ;
}
} ) ( ) ;
< / script >
< / body >
< / html >