From d632e7ac0d48c877e4822f51b805ebf08051b8ba Mon Sep 17 00:00:00 2001 From: inhale-dir Date: Fri, 13 Dec 2024 13:05:23 +0100 Subject: [PATCH] good rendering, good ux. I'd say 1.1 Beta !!! --- app/build.gradle.kts | 17 + app/src/main/AndroidManifest.xml | 3 +- .../java/inhale/rip/epook/MainActivity.kt | 1 + .../java/inhale/rip/epook/ReaderScreen.kt | 531 ++++++------------ .../java/inhale/rip/epook/data/BookStore.kt | 5 + .../inhale/rip/epook/data/SettingsStore.kt | 2 +- app/src/main/res/values/themes.xml | 4 +- settings.gradle.kts | 1 + 8 files changed, 213 insertions(+), 351 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f8e5c34..7c3a0c9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -44,6 +44,13 @@ android { composeOptions { kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() } + + configurations.all { + resolutionStrategy { + force("androidx.core:core:1.12.0") + force("androidx.versionedparcelable:versionedparcelable:1.1.1") + } + } } dependencies { @@ -131,4 +138,14 @@ dependencies { exclude(group = "xmlpull", module = "xmlpull") } } + + // Add Fuel dependencies explicitly + implementation("com.github.kittinunf.fuel:fuel:2.3.1") + implementation("com.github.kittinunf.fuel:fuel-android:2.3.1") + + // Add explicit AndroidX dependencies + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.11.0") + implementation("androidx.recyclerview:recyclerview:1.3.2") + implementation("androidx.legacy:legacy-support-v4:1.0.0") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ac87928..8ba6190 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,8 +16,7 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.Epook" - tools:targetApi="31"> + android:theme="@style/Theme.Epook"> Unit)? = null - - override fun onPageFinished(view: WebView?, url: String?) { - super.onPageFinished(view, url) - view?.let { webView -> - onPageLoaded?.invoke(webView) - } - } -} +import nl.siegmann.epublib.epub.EpubReader +import java.io.FileInputStream +import kotlinx.coroutines.flow.Flow +import inhale.rip.epook.data.Book +import nl.siegmann.epublib.domain.Book as EpubBook @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -82,326 +50,211 @@ fun ReaderScreen( val bookStore = remember { BookStore(context) } val settingsStore = remember { SettingsStore(context) } val scope = rememberCoroutineScope() + var currentSettings by remember { mutableStateOf(Settings()) } + + var currentChapterIndex by remember { mutableStateOf(0) } + var chapters by remember { mutableStateOf(listOf()) } + var book by remember { mutableStateOf(null) } var showControls by remember { mutableStateOf(true) } - var showSettings by remember { mutableStateOf(false) } - var brightness by remember { mutableStateOf(1f) } - var isDarkMode by remember { mutableStateOf(false) } - var fontSize by remember { mutableStateOf(16f) } - var lineHeight by remember { mutableStateOf(1.5f) } - var padding by remember { mutableStateOf(16f) } - var fontFamily by remember { mutableStateOf("Roboto") } - var currentChapter by remember { mutableStateOf(0) } var showChapterList by remember { mutableStateOf(false) } - var chapters by remember { mutableStateOf(listOf()) } - var currentPage by remember { mutableStateOf(1) } - var totalPages by remember { mutableStateOf(1) } - var bookContent by remember { mutableStateOf("") } - var webViewRef by remember { mutableStateOf(null) } - var webViewClient = remember { WebViewScrollClient() } - - // Move processChapterHtml outside the Composable - fun processChapterHtml(rawHtml: String, chapterIndex: Int): String { - val document = Jsoup.parse(rawHtml) - val body = document.body() - body.select("script, style").remove() - return "
\n${body.html()}\n
" + val backgroundColor = MaterialTheme.colorScheme.background.toArgb() + val textColor = MaterialTheme.colorScheme.onBackground.toArgb() + + LaunchedEffect(Unit) { + try { + currentSettings = settingsStore.getSettings() + } catch (e: Exception) { + Timber.e(e, "Error loading settings") + } } - - // Load book content + LaunchedEffect(bookId) { try { - Timber.d("📚 Starting to load book with ID: $bookId") - val book = bookStore.getBook(bookId) - Timber.d("📚 Book loaded successfully: ${book.title}") - - // Load settings first - val savedSettings = settingsStore.loadSettings() - currentPage = savedSettings.currentPage - fontSize = savedSettings.fontSize - lineHeight = savedSettings.lineHeight - padding = savedSettings.padding - fontFamily = savedSettings.fontFamily - isDarkMode = savedSettings.isDarkMode - Timber.d("📚 Settings loaded - Page: $currentPage, Font: $fontSize, Line: $lineHeight, Padding: $padding") - - // Process chapters - val processedChapters = mutableListOf() - book.spine.spineReferences.forEachIndexed { index, spineReference -> - Timber.d("📚 Processing chapter $index") - val resource = book.resources.getByHref(spineReference.resource.href) - Timber.d("📚 Resource href: ${resource.href}") - Timber.d("📚 Resource media type: ${resource.mediaType}") - - 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}") - - val processedHtml = processChapterHtml(rawHtml, index) - processedChapters.add(processedHtml) - Timber.d("📚 Chapter $index processed successfully") - } + val bookPath = bookStore.getBookPath(bookId) + val epubBook = EpubReader().readEpub(FileInputStream(bookPath)) + book = epubBook + chapters = epubBook.spine.spineReferences.mapNotNull { spineRef: nl.siegmann.epublib.domain.SpineReference -> + spineRef.resource } - - // Combine all chapters - bookContent = processedChapters.joinToString("\n") - Timber.d("📚 Final book content processed:") - Timber.d("📚 - Total length: ${bookContent.length}") - Timber.d("📚 - First 100 chars: ${bookContent.take(100)}") - Timber.d("📚 - Last 100 chars: ${bookContent.takeLast(100)}") - - // Update chapter list - chapters = book.tableOfContents.tocReferences.map { it.title } - } catch (e: Exception) { - Timber.e(e, "📚 Error loading book") + Timber.e(e, "Error loading book") } } - val css = """ - :root { - --page-width: calc(100vw - ${padding * 2}px); - } - body { - margin: 0; - padding: ${padding}px; - width: var(--page-width); - max-width: var(--page-width); - height: calc(100vh - ${padding * 2}px); - column-width: var(--page-width); - column-gap: ${padding * 2}px; - column-fill: auto; - overflow-x: hidden; - overflow-y: hidden; - font-family: $fontFamily; - font-size: ${fontSize}px; - line-height: ${lineHeight}em; - background-color: ${if (isDarkMode) "#1C1B1F" else "#FFFFFF"}; - color: ${if (isDarkMode) "#E6E1E5" else "#1C1B1F"}; - } - .chapter { - break-inside: avoid; - margin-bottom: 2em; - display: inline-block; - width: 100%; - } - img { - max-width: 100%; - height: auto; - } - p { - margin: 0.5em 0; - text-align: justify; - } - """.trimIndent() - - 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") + Scaffold( + topBar = { + AnimatedVisibility( + visible = showControls, + enter = fadeIn() + slideInVertically(), + exit = fadeOut() + slideOutVertically() + ) { + TopAppBar( + title = { + Column { + Text( + text = book?.title ?: "Reader", + style = MaterialTheme.typography.titleLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = "Chapter ${currentChapterIndex + 1} of ${chapters.size}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } + }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + }, + actions = { + IconButton(onClick = { showChapterList = true }) { + Icon(Icons.Default.List, contentDescription = "Chapters") + } + IconButton(onClick = { /* TODO: Add settings dialog */ }) { + Icon(Icons.Default.Settings, contentDescription = "Settings") } } - } catch (e: Exception) { - Timber.e(e, "Error calculating current page") - } + ) } } - } - - // Update webViewClient callback - webViewClient.onPageLoaded = { webView -> - calculateCurrentPage(webView) - } - - // Update the HTML content creation - fun createHtmlContent(): String = """ - - - - - - - - $bookContent - - - - """.trimIndent() - - // Update chapter navigation - fun navigateToChapter(index: Int) { - webViewRef?.evaluateJavascript("scrollToChapter($index);") { result -> - if (result == "true") { - currentChapter = index - showChapterList = false - } - } - } - - // 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 - } - - 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") + ) { padding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + if (chapters.isNotEmpty()) { + AndroidView( + modifier = Modifier + .fillMaxSize() + .pointerInput(Unit) { + detectTapGestures( + onTap = { showControls = !showControls } + ) + }, + factory = { context -> + WebView(context).apply { + webViewClient = WebViewClient() + this.settings.apply { + javaScriptEnabled = true + defaultFontSize = currentSettings.fontSize.toInt() + builtInZoomControls = true + displayZoomControls = false } } + }, + update = { webView -> + val chapter = chapters[currentChapterIndex] + val html = """ + + + + + + + ${String(chapter.data, Charset.defaultCharset())} + + + """.trimIndent() + webView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null) + } + ) + } + + // Navigation controls + AnimatedVisibility( + visible = showControls, + modifier = Modifier.align(Alignment.BottomCenter), + enter = fadeIn() + slideInVertically { it }, + exit = fadeOut() + slideOutVertically { it } + ) { + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f), + tonalElevation = 3.dp + ) { + Row( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = { if (currentChapterIndex > 0) currentChapterIndex-- }, + enabled = currentChapterIndex > 0 + ) { + Icon(Icons.Default.NavigateBefore, "Previous chapter") + } + + Text( + text = "${currentChapterIndex + 1}/${chapters.size}", + style = MaterialTheme.typography.titleMedium + ) + + IconButton( + onClick = { if (currentChapterIndex < chapters.size - 1) currentChapterIndex++ }, + enabled = currentChapterIndex < chapters.size - 1 + ) { + Icon(Icons.Default.NavigateNext, "Next chapter") + } } } } - }, - update = { webView -> - webViewRef = webView - val htmlContent = """ - - - - - - - - $bookContent - - - - """ - webView.loadDataWithBaseURL( - "file:///android_asset/", - htmlContent, - "text/html", - "UTF-8", - null - ) } - ) + } - // Update chapter list dialog + // 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) + items( + items = List(chapters.size) { it }, + key = { it } + ) { index -> + ListItem( + headlineContent = { + Text("Chapter ${index + 1}") + }, + modifier = Modifier.clickable { + currentChapterIndex = index + showChapterList = false + }, + leadingContent = if (currentChapterIndex == index) { + { + Icon( + Icons.Default.RadioButtonChecked, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) } - .padding(16.dp) + } else { + { + Icon( + Icons.Default.RadioButtonUnchecked, + contentDescription = null + ) + } + } ) } } @@ -413,20 +266,4 @@ fun ReaderScreen( } ) } - - // Save reading progress when leaving - DisposableEffect(Unit) { - onDispose { - scope.launch { - settingsStore.saveSettings(Settings( - currentPage = currentPage, - fontSize = fontSize, - lineHeight = lineHeight, - padding = padding, - fontFamily = fontFamily, - isDarkMode = isDarkMode - )) - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/inhale/rip/epook/data/BookStore.kt b/app/src/main/java/inhale/rip/epook/data/BookStore.kt index 5bed9af..7e0767f 100644 --- a/app/src/main/java/inhale/rip/epook/data/BookStore.kt +++ b/app/src/main/java/inhale/rip/epook/data/BookStore.kt @@ -89,4 +89,9 @@ class BookStore(private val context: Context) { val bookEntity = bookDao.getBook(bookId) ?: throw IllegalArgumentException("Book not found") return EpubReader().readEpub(FileInputStream(bookEntity.filePath)) } + + suspend fun getBookPath(bookId: String): String { + return bookDao.getBook(bookId)?.filePath + ?: throw IllegalArgumentException("Book not found") + } } \ No newline at end of file diff --git a/app/src/main/java/inhale/rip/epook/data/SettingsStore.kt b/app/src/main/java/inhale/rip/epook/data/SettingsStore.kt index 09fb10f..1a587e9 100644 --- a/app/src/main/java/inhale/rip/epook/data/SettingsStore.kt +++ b/app/src/main/java/inhale/rip/epook/data/SettingsStore.kt @@ -69,7 +69,7 @@ class SettingsStore(private val context: Context) { } } - suspend fun loadSettings(): Settings { + suspend fun getSettings(): Settings { return context.dataStore.data.first().let { preferences -> Settings( currentPage = preferences[PreferencesKeys.CURRENT_PAGE] ?: 1, diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 07ffee5..127cf39 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,7 @@ - \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index baed02a..8fc0b3b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,7 @@ dependencyResolutionManagement { google() mavenCentral() maven { url = uri("https://jitpack.io") } + maven { url = uri("https://s01.oss.sonatype.org/content/repositories/releases") } } }