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

View File

@ -30,19 +30,48 @@ import timber.log.Timber
import java.nio.charset.Charset
import nl.siegmann.epublib.domain.MediaType
import nl.siegmann.epublib.domain.Resource
import inhale.rip.epook.data.Settings
// Update the JavaScript for accurate page calculation
private const val PAGE_CALCULATION_JS = """
(function() {
const content = document.body;
const contentWidth = content.scrollWidth;
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
const pageWidth = window.innerWidth;
const totalPages = Math.max(1, Math.ceil(contentWidth / pageWidth));
console.log('Content width:', contentWidth, 'Page width:', pageWidth, 'Total pages:', totalPages);
return totalPages;
const totalWidth = document.documentElement.scrollWidth;
const currentPage = Math.floor(scrollLeft / pageWidth) + 1;
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)
@Composable
fun ReaderScreen(
@ -70,6 +99,7 @@ fun ReaderScreen(
var totalPages by remember { mutableStateOf(1) }
var bookContent by remember { mutableStateOf("") }
var webViewRef by remember { mutableStateOf<WebView?>(null) }
var webViewClient = remember { WebViewScrollClient() }
// Move processChapterHtml outside the Composable
fun processChapterHtml(rawHtml: String, chapterIndex: Int): String {
@ -104,7 +134,8 @@ fun ReaderScreen(
Timber.d("📚 Resource href: ${resource.href}")
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"))
Timber.d("📚 Raw HTML length: ${rawHtml.length}")
@ -166,237 +197,235 @@ fun ReaderScreen(
}
""".trimIndent()
BackHandler(enabled = showSettings || showChapterList) {
when {
showSettings -> showSettings = false
showChapterList -> showChapterList = false
BackHandler {
scope.launch {
settingsStore.saveSettings(Settings(
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(
topBar = {
AnimatedVisibility(
visible = showControls,
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut()
) {
TopAppBar(
title = {
Column {
Text("Chapter ${currentChapter + 1}")
Text(
"Page $currentPage of $totalPages",
style = MaterialTheme.typography.bodyMedium
)
}
},
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
}
},
actions = {
IconButton(onClick = { showChapterList = true }) {
Icon(Icons.Default.List, "Chapters")
}
IconButton(onClick = { showSettings = true }) {
Icon(Icons.Default.Settings, "Settings")
}
IconButton(onClick = { isDarkMode = !isDarkMode }) {
Icon(
if (isDarkMode) Icons.Default.LightMode else Icons.Default.DarkMode,
"Toggle theme"
)
}
}
)
// Update webViewClient callback
webViewClient.onPageLoaded = { webView ->
calculateCurrentPage(webView)
}
// Update the HTML content creation
fun createHtmlContent(): String = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
html {
scroll-snap-type: x mandatory;
overflow-x: scroll;
scroll-behavior: smooth;
}
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"};
display: flex;
flex-direction: row;
width: fit-content;
}
.chapter {
width: calc(100vw - ${padding * 2}px);
height: calc(100vh - ${padding * 2}px);
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() {
override fun onPageStarted(view: WebView?, url: String?, favicon: android.graphics.Bitmap?) {
super.onPageStarted(view, url, favicon)
Timber.d("📚 WebView page load started")
}
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
Timber.d("📚 WebView page load finished")
// Add a slight delay to ensure content is fully laid out
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
}
// Update the WebView setup
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
WebView(context).apply {
settings.apply {
javaScriptEnabled = true
defaultFontSize = fontSize.toInt()
domStorageEnabled = true
allowFileAccess = true
allowContentAccess = true
}
}
// Chapter list
if (showChapterList) {
ModalBottomSheet(
onDismissRequest = { showChapterList = false }
) {
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
items(chapters) { chapter ->
ListItem(
headlineContent = { Text(chapter) },
modifier = Modifier.clickable {
// Navigate to chapter
webViewRef?.evaluateJavascript(
"document.getElementById('chapter_$currentChapter').scrollIntoView()",
null
)
showChapterList = false
webViewClient = webViewClient
setOnScrollChangeListener { _, _, _, _, _ ->
evaluateJavascript(PAGE_CALCULATION_JS) { 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
scope.launch {
currentPage = current.toInt()
totalPages = total.toInt()
}
}
)
} catch (e: Exception) {
Timber.e(e, "Error calculating page")
}
}
}
}
}
// Brightness overlay
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 1f - brightness))
},
update = { webView ->
webViewRef = webView
val htmlContent = """
<!DOCTYPE html>
<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
DisposableEffect(Unit) {
onDispose {
scope.launch {
settingsStore.saveLastPosition(bookId, currentPage)
settingsStore.saveSettings(Settings(
currentPage = currentPage,
fontSize = fontSize,
lineHeight = lineHeight,
padding = padding,
fontFamily = fontFamily,
isDarkMode = isDarkMode
))
}
}
}

View File

@ -2,83 +2,94 @@ package inhale.rip.epook.data
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
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.core.*
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.first
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) {
private object PreferencesKeys {
val CURRENT_PAGE = intPreferencesKey("current_page")
val FONT_SIZE = floatPreferencesKey("font_size")
val LINE_HEIGHT = floatPreferencesKey("line_height")
val PADDING = floatPreferencesKey("padding")
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 {
return context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.FONT_SIZE] ?: 16f
}.first()
return context.dataStore.data.first()[PreferencesKeys.FONT_SIZE] ?: 16f
}
suspend fun getLineHeight(): Float {
return context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.LINE_HEIGHT] ?: 1.5f
}.first()
return context.dataStore.data.first()[PreferencesKeys.LINE_HEIGHT] ?: 1.5f
}
suspend fun getPadding(): Float {
return context.dataStore.data.map { preferences ->
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
}
return context.dataStore.data.first()[PreferencesKeys.PADDING] ?: 16f
}
suspend fun getFontFamily(): String {
return context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.FONT_FAMILY] ?: "Roboto"
}.first()
return context.dataStore.data.first()[PreferencesKeys.FONT_FAMILY] ?: "Roboto"
}
suspend fun updateFontFamily(family: String) {
suspend fun updateFontSize(value: Float) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.FONT_FAMILY] = family
preferences[PreferencesKeys.FONT_SIZE] = value
}
}
suspend fun getLastPosition(bookId: String): Int? {
return context.dataStore.data.map { preferences ->
preferences[PreferencesKeys.lastPositionKey(bookId)]
}.first()
suspend fun updateLineHeight(value: Float) {
context.dataStore.edit { preferences ->
preferences[PreferencesKeys.LINE_HEIGHT] = value
}
}
suspend fun saveLastPosition(bookId: String, position: Int) {
suspend fun updatePadding(value: Float) {
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
}
}
}