working and rendering, but pretty bad

This commit is contained in:
inhale-dir
2024-12-13 12:08:29 +01:00
parent 04820240dc
commit 5b587d4c63
2 changed files with 301 additions and 261 deletions
+244 -215
View File
@@ -30,19 +30,48 @@ import timber.log.Timber
import java.nio.charset.Charset import java.nio.charset.Charset
import nl.siegmann.epublib.domain.MediaType import nl.siegmann.epublib.domain.MediaType
import nl.siegmann.epublib.domain.Resource import nl.siegmann.epublib.domain.Resource
import inhale.rip.epook.data.Settings
// Update the JavaScript for accurate page calculation // Update the JavaScript for accurate page calculation
private const val PAGE_CALCULATION_JS = """ private const val PAGE_CALCULATION_JS = """
(function() { (function() {
const content = document.body; const content = document.body;
const contentWidth = content.scrollWidth; const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
const pageWidth = window.innerWidth; const pageWidth = window.innerWidth;
const totalPages = Math.max(1, Math.ceil(contentWidth / pageWidth)); const totalWidth = document.documentElement.scrollWidth;
console.log('Content width:', contentWidth, 'Page width:', pageWidth, 'Total pages:', totalPages); const currentPage = Math.floor(scrollLeft / pageWidth) + 1;
return totalPages; const totalPages = Math.max(1, Math.ceil(totalWidth / pageWidth));
return JSON.stringify({
currentPage: currentPage,
totalPages: totalPages
});
})(); })();
""" """
// Add this function to handle chapter navigation
private const val SCROLL_TO_CHAPTER_JS = """
function scrollToChapter(chapterIndex) {
const chapter = document.getElementById('chapter_' + chapterIndex);
if (chapter) {
chapter.scrollIntoView({ behavior: 'smooth', block: 'start' });
return true;
}
return false;
}
"""
// Add this to track WebView scroll position
class WebViewScrollClient : WebViewClient() {
var onPageLoaded: ((WebView) -> Unit)? = null
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
view?.let { webView ->
onPageLoaded?.invoke(webView)
}
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ReaderScreen( fun ReaderScreen(
@@ -70,6 +99,7 @@ fun ReaderScreen(
var totalPages by remember { mutableStateOf(1) } var totalPages by remember { mutableStateOf(1) }
var bookContent by remember { mutableStateOf("") } var bookContent by remember { mutableStateOf("") }
var webViewRef by remember { mutableStateOf<WebView?>(null) } var webViewRef by remember { mutableStateOf<WebView?>(null) }
var webViewClient = remember { WebViewScrollClient() }
// Move processChapterHtml outside the Composable // Move processChapterHtml outside the Composable
fun processChapterHtml(rawHtml: String, chapterIndex: Int): String { fun processChapterHtml(rawHtml: String, chapterIndex: Int): String {
@@ -104,7 +134,8 @@ fun ReaderScreen(
Timber.d("📚 Resource href: ${resource.href}") Timber.d("📚 Resource href: ${resource.href}")
Timber.d("📚 Resource media type: ${resource.mediaType}") Timber.d("📚 Resource media type: ${resource.mediaType}")
if (resource.mediaType == MediaType.XHTML) { if (resource.mediaType.toString().contains("application/xhtml+xml") ||
resource.mediaType.toString().contains("text/html")) {
val rawHtml = String(resource.data, Charset.forName("UTF-8")) val rawHtml = String(resource.data, Charset.forName("UTF-8"))
Timber.d("📚 Raw HTML length: ${rawHtml.length}") Timber.d("📚 Raw HTML length: ${rawHtml.length}")
@@ -166,237 +197,235 @@ fun ReaderScreen(
} }
""".trimIndent() """.trimIndent()
BackHandler(enabled = showSettings || showChapterList) { BackHandler {
when { scope.launch {
showSettings -> showSettings = false settingsStore.saveSettings(Settings(
showChapterList -> showChapterList = false currentPage = currentPage,
fontSize = fontSize,
lineHeight = lineHeight,
padding = padding,
fontFamily = fontFamily,
isDarkMode = isDarkMode
))
}
onNavigateBack()
}
// Update the calculateCurrentPage function to use a callback
fun calculateCurrentPage(webView: WebView) {
webView.evaluateJavascript("""
(function() {
const height = document.documentElement.scrollHeight;
const scrollTop = document.documentElement.scrollTop;
const clientHeight = document.documentElement.clientHeight;
const totalPages = Math.ceil(height / clientHeight);
const currentPage = Math.ceil((scrollTop + clientHeight) / clientHeight);
return JSON.stringify({currentPage: currentPage, totalPages: totalPages});
})()
""".trimIndent()) { result ->
result?.let {
try {
val jsonStr = result.removeSurrounding("\"")
val regex = """.*"currentPage":(\d+).*"totalPages":(\d+).*""".toRegex()
regex.find(jsonStr)?.let { matchResult ->
val (current, total) = matchResult.destructured
// Update state values
scope.launch {
currentPage = current.toInt()
totalPages = total.toInt()
Timber.d("📚 Current page: $currentPage of $totalPages")
}
}
} catch (e: Exception) {
Timber.e(e, "Error calculating current page")
}
}
} }
} }
Scaffold( // Update webViewClient callback
topBar = { webViewClient.onPageLoaded = { webView ->
AnimatedVisibility( calculateCurrentPage(webView)
visible = showControls, }
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut() // Update the HTML content creation
) { fun createHtmlContent(): String = """
TopAppBar( <!DOCTYPE html>
title = { <html>
Column { <head>
Text("Chapter ${currentChapter + 1}") <meta name="viewport" content="width=device-width, initial-scale=1.0">
Text( <style>
"Page $currentPage of $totalPages", html {
style = MaterialTheme.typography.bodyMedium scroll-snap-type: x mandatory;
) overflow-x: scroll;
} scroll-behavior: smooth;
}, }
navigationIcon = { body {
IconButton(onClick = onNavigateBack) { font-family: '$fontFamily';
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") font-size: ${fontSize}px;
} line-height: ${lineHeight};
}, padding: ${padding}px;
actions = { margin: 0;
IconButton(onClick = { showChapterList = true }) { background-color: ${if (isDarkMode) "#1c1c1c" else "#ffffff"};
Icon(Icons.Default.List, "Chapters") color: ${if (isDarkMode) "#ffffff" else "#000000"};
} display: flex;
IconButton(onClick = { showSettings = true }) { flex-direction: row;
Icon(Icons.Default.Settings, "Settings") width: fit-content;
} }
IconButton(onClick = { isDarkMode = !isDarkMode }) { .chapter {
Icon( width: calc(100vw - ${padding * 2}px);
if (isDarkMode) Icons.Default.LightMode else Icons.Default.DarkMode, height: calc(100vh - ${padding * 2}px);
"Toggle theme" overflow: hidden;
) scroll-snap-align: start;
} break-inside: avoid;
} flex-shrink: 0;
) }
img {
max-width: 100%;
height: auto;
display: block;
margin: 1em auto;
}
</style>
</head>
<body>
$bookContent
<script>$SCROLL_TO_CHAPTER_JS</script>
</body>
</html>
""".trimIndent()
// Update chapter navigation
fun navigateToChapter(index: Int) {
webViewRef?.evaluateJavascript("scrollToChapter($index);") { result ->
if (result == "true") {
currentChapter = index
showChapterList = false
} }
} }
) { padding -> }
Box(modifier = Modifier.padding(padding)) {
AndroidView(
factory = { context ->
WebView(context).apply {
settings.apply {
javaScriptEnabled = true
useWideViewPort = true
loadWithOverviewMode = true
defaultFontSize = fontSize.toInt()
Timber.d("📚 WebView settings configured")
}
webViewClient = object : WebViewClient() { // Update the WebView setup
override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) { AndroidView(
super.onPageStarted(view, url, favicon) modifier = Modifier.fillMaxSize(),
Timber.d("📚 WebView page load started") factory = { context ->
} WebView(context).apply {
settings.apply {
override fun onPageFinished(view: WebView?, url: String?) { javaScriptEnabled = true
super.onPageFinished(view, url) defaultFontSize = fontSize.toInt()
Timber.d("📚 WebView page load finished") domStorageEnabled = true
allowFileAccess = true
// Add a slight delay to ensure content is fully laid out allowContentAccess = true
view?.postDelayed({
Timber.d("📚 Starting page calculation")
view.evaluateJavascript(PAGE_CALCULATION_JS) { result ->
try {
val pages = result.toInt()
Timber.d("📚 Page calculation complete - Total pages: $pages")
totalPages = pages
} catch (e: Exception) {
Timber.e(e, "📚 Error calculating pages")
}
}
// Log the current HTML content for debugging
view.evaluateJavascript(
"(function() { return document.documentElement.outerHTML; })()",
) { result ->
Timber.d("📚 Current HTML content length: ${result.length}")
Timber.d("📚 First 100 chars of HTML: ${result.take(100)}")
}
// Scroll to last position
view.evaluateJavascript(
"window.scrollTo({left: (${currentPage - 1}) * window.innerWidth, behavior: 'auto'})",
null
)
}, 500)
}
override fun onReceivedError(
view: WebView?,
errorCode: Int,
description: String?,
failingUrl: String?
) {
super.onReceivedError(view, errorCode, description, failingUrl)
Timber.e("📚 WebView error: $errorCode - $description")
}
}
Timber.d("📚 Loading content into WebView")
loadDataWithBaseURL(
null,
"""
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<style>$css</style>
</head>
<body>$bookContent</body>
</html>
""".trimIndent(),
"text/html",
"UTF-8",
null
)
webViewRef = this
}
},
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures { offset ->
val screenWidth = this.size.width.toFloat()
when {
offset.x < screenWidth * 0.3f && currentPage > 1 -> {
currentPage--
webViewRef?.evaluateJavascript(
"window.scrollTo({left: (${currentPage - 1}) * window.innerWidth, behavior: 'smooth'})",
null
)
}
offset.x > screenWidth * 0.7f && currentPage < totalPages -> {
currentPage++
webViewRef?.evaluateJavascript(
"window.scrollTo({left: (${currentPage - 1}) * window.innerWidth, behavior: 'smooth'})",
null
)
}
else -> {
showControls = !showControls
}
}
}
}
)
// Settings sheet
if (showSettings) {
ModalBottomSheet(
onDismissRequest = { showSettings = false }
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text("Reading Settings", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(16.dp))
Text("Font Size: $fontSize", style = MaterialTheme.typography.bodyMedium)
Slider(
value = fontSize,
onValueChange = {
fontSize = it
// Update WebView font size
webViewRef?.evaluateJavascript(
"document.body.style.fontSize = '${fontSize}px'",
null
)
},
valueRange = 12f..24f,
steps = 11
)
// Similar sliders for lineHeight, padding, brightness
// Font family selector
// Color theme selector
}
} }
}
// Chapter list webViewClient = webViewClient
if (showChapterList) {
ModalBottomSheet( setOnScrollChangeListener { _, _, _, _, _ ->
onDismissRequest = { showChapterList = false } evaluateJavascript(PAGE_CALCULATION_JS) { result ->
) { result?.let {
LazyColumn( try {
modifier = Modifier.fillMaxWidth() val jsonStr = result.removeSurrounding("\"")
) { val regex = """.*"currentPage":(\d+).*"totalPages":(\d+).*""".toRegex()
items(chapters) { chapter -> regex.find(jsonStr)?.let { matchResult ->
ListItem( val (current, total) = matchResult.destructured
headlineContent = { Text(chapter) }, scope.launch {
modifier = Modifier.clickable { currentPage = current.toInt()
// Navigate to chapter totalPages = total.toInt()
webViewRef?.evaluateJavascript( }
"document.getElementById('chapter_$currentChapter').scrollIntoView()",
null
)
showChapterList = false
} }
) } catch (e: Exception) {
Timber.e(e, "Error calculating page")
}
} }
} }
} }
} }
},
// Brightness overlay update = { webView ->
Box( webViewRef = webView
modifier = Modifier val htmlContent = """
.fillMaxSize() <!DOCTYPE html>
.background(Color.Black.copy(alpha = 1f - brightness)) <html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: '$fontFamily';
font-size: ${fontSize}px;
line-height: ${lineHeight};
padding: ${padding}px;
margin: 0;
background-color: ${if (isDarkMode) "#1c1c1c" else "#ffffff"};
color: ${if (isDarkMode) "#ffffff" else "#000000"};
}
img {
max-width: 100%;
height: auto;
display: block;
margin: 1em auto;
}
.chapter {
margin-bottom: 2em;
}
</style>
</head>
<body>
$bookContent
<script>$SCROLL_TO_CHAPTER_JS</script>
</body>
</html>
"""
webView.loadDataWithBaseURL(
"file:///android_asset/",
htmlContent,
"text/html",
"UTF-8",
null
) )
} }
)
// Update chapter list dialog
if (showChapterList) {
AlertDialog(
onDismissRequest = { showChapterList = false },
title = { Text("Chapters") },
text = {
LazyColumn {
items(chapters) { chapter ->
Text(
text = chapter,
modifier = Modifier
.fillMaxWidth()
.clickable {
val index = chapters.indexOf(chapter)
navigateToChapter(index)
}
.padding(16.dp)
)
}
}
},
confirmButton = {
TextButton(onClick = { showChapterList = false }) {
Text("Close")
}
}
)
} }
// Save reading progress when leaving // Save reading progress when leaving
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
scope.launch { scope.launch {
settingsStore.saveLastPosition(bookId, currentPage) settingsStore.saveSettings(Settings(
currentPage = currentPage,
fontSize = fontSize,
lineHeight = lineHeight,
padding = padding,
fontFamily = fontFamily,
isDarkMode = isDarkMode
))
} }
} }
} }
@@ -2,83 +2,94 @@ package inhale.rip.epook.data
import android.content.Context import android.content.Context
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.first
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings") private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
data class Settings(
val currentPage: Int = 1,
val fontSize: Float = 16f,
val lineHeight: Float = 1.5f,
val padding: Float = 16f,
val fontFamily: String = "Roboto",
val isDarkMode: Boolean = false
)
class SettingsStore(private val context: Context) { class SettingsStore(private val context: Context) {
private object PreferencesKeys { private object PreferencesKeys {
val CURRENT_PAGE = intPreferencesKey("current_page")
val FONT_SIZE = floatPreferencesKey("font_size") val FONT_SIZE = floatPreferencesKey("font_size")
val LINE_HEIGHT = floatPreferencesKey("line_height") val LINE_HEIGHT = floatPreferencesKey("line_height")
val PADDING = floatPreferencesKey("padding") val PADDING = floatPreferencesKey("padding")
val FONT_FAMILY = stringPreferencesKey("font_family") val FONT_FAMILY = stringPreferencesKey("font_family")
fun lastPositionKey(bookId: String) = intPreferencesKey("last_position_$bookId") val IS_DARK_MODE = booleanPreferencesKey("is_dark_mode")
} }
suspend fun getFontSize(): Float { suspend fun getFontSize(): Float {
return context.dataStore.data.map { preferences -> return context.dataStore.data.first()[PreferencesKeys.FONT_SIZE] ?: 16f
preferences[PreferencesKeys.FONT_SIZE] ?: 16f
}.first()
} }
suspend fun getLineHeight(): Float { suspend fun getLineHeight(): Float {
return context.dataStore.data.map { preferences -> return context.dataStore.data.first()[PreferencesKeys.LINE_HEIGHT] ?: 1.5f
preferences[PreferencesKeys.LINE_HEIGHT] ?: 1.5f
}.first()
} }
suspend fun getPadding(): Float { suspend fun getPadding(): Float {
return context.dataStore.data.map { preferences -> return context.dataStore.data.first()[PreferencesKeys.PADDING] ?: 16f
preferences[PreferencesKeys.PADDING] ?: 16f
}.first()
}
suspend fun updateFontSize(size: Float) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.FONT_SIZE] = size
}
}
suspend fun updateLineHeight(height: Float) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.LINE_HEIGHT] = height
}
}
suspend fun updatePadding(padding: Float) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.PADDING] = padding
}
} }
suspend fun getFontFamily(): String { suspend fun getFontFamily(): String {
return context.dataStore.data.map { preferences -> return context.dataStore.data.first()[PreferencesKeys.FONT_FAMILY] ?: "Roboto"
preferences[PreferencesKeys.FONT_FAMILY] ?: "Roboto"
}.first()
} }
suspend fun updateFontFamily(family: String) { suspend fun updateFontSize(value: Float) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.FONT_FAMILY] = family preferences[PreferencesKeys.FONT_SIZE] = value
} }
} }
suspend fun getLastPosition(bookId: String): Int? { suspend fun updateLineHeight(value: Float) {
return context.dataStore.data.map { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.lastPositionKey(bookId)] preferences[PreferencesKeys.LINE_HEIGHT] = value
}.first() }
} }
suspend fun saveLastPosition(bookId: String, position: Int) { suspend fun updatePadding(value: Float) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[PreferencesKeys.lastPositionKey(bookId)] = position preferences[PreferencesKeys.PADDING] = value
}
}
suspend fun updateFontFamily(value: String) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.FONT_FAMILY] = value
}
}
suspend fun loadSettings(): Settings {
return context.dataStore.data.first().let { preferences ->
Settings(
currentPage = preferences[PreferencesKeys.CURRENT_PAGE] ?: 1,
fontSize = preferences[PreferencesKeys.FONT_SIZE] ?: 16f,
lineHeight = preferences[PreferencesKeys.LINE_HEIGHT] ?: 1.5f,
padding = preferences[PreferencesKeys.PADDING] ?: 16f,
fontFamily = preferences[PreferencesKeys.FONT_FAMILY] ?: "Roboto",
isDarkMode = preferences[PreferencesKeys.IS_DARK_MODE] ?: false
)
}
}
suspend fun saveSettings(settings: Settings) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.CURRENT_PAGE] = settings.currentPage
preferences[PreferencesKeys.FONT_SIZE] = settings.fontSize
preferences[PreferencesKeys.LINE_HEIGHT] = settings.lineHeight
preferences[PreferencesKeys.PADDING] = settings.padding
preferences[PreferencesKeys.FONT_FAMILY] = settings.fontFamily
preferences[PreferencesKeys.IS_DARK_MODE] = settings.isDarkMode
} }
} }
} }