good rendering, good ux. I'd say 1.1 Beta !!!

This commit is contained in:
inhale-dir
2024-12-13 13:05:23 +01:00
parent 5b587d4c63
commit d632e7ac0d
8 changed files with 213 additions and 351 deletions
+17
View File
@@ -44,6 +44,13 @@ android {
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
} }
configurations.all {
resolutionStrategy {
force("androidx.core:core:1.12.0")
force("androidx.versionedparcelable:versionedparcelable:1.1.1")
}
}
} }
dependencies { dependencies {
@@ -131,4 +138,14 @@ dependencies {
exclude(group = "xmlpull", module = "xmlpull") 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")
} }
+1 -2
View File
@@ -16,8 +16,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Epook" android:theme="@style/Theme.Epook">
tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@@ -101,6 +101,7 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
App() App()
} }
+182 -345
View File
@@ -18,59 +18,27 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import inhale.rip.epook.data.BookStore import inhale.rip.epook.data.BookStore
import inhale.rip.epook.data.Settings
import inhale.rip.epook.data.SettingsStore import inhale.rip.epook.data.SettingsStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jsoup.Jsoup import org.jsoup.Jsoup
import timber.log.Timber 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 import nl.siegmann.epublib.epub.EpubReader
import java.io.FileInputStream
// Update the JavaScript for accurate page calculation import kotlinx.coroutines.flow.Flow
private const val PAGE_CALCULATION_JS = """ import inhale.rip.epook.data.Book
(function() { import nl.siegmann.epublib.domain.Book as EpubBook
const content = document.body;
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
const pageWidth = window.innerWidth;
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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -82,326 +50,211 @@ fun ReaderScreen(
val bookStore = remember { BookStore(context) } val bookStore = remember { BookStore(context) }
val settingsStore = remember { SettingsStore(context) } val settingsStore = remember { SettingsStore(context) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var currentSettings by remember { mutableStateOf(Settings()) }
var currentChapterIndex by remember { mutableStateOf(0) }
var chapters by remember { mutableStateOf(listOf<Resource>()) }
var book by remember { mutableStateOf<EpubBook?>(null) }
var showControls by remember { mutableStateOf(true) } 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 showChapterList by remember { mutableStateOf(false) }
var chapters by remember { mutableStateOf(listOf<String>()) }
var currentPage by remember { mutableStateOf(1) } val backgroundColor = MaterialTheme.colorScheme.background.toArgb()
var totalPages by remember { mutableStateOf(1) } val textColor = MaterialTheme.colorScheme.onBackground.toArgb()
var bookContent by remember { mutableStateOf("") }
var webViewRef by remember { mutableStateOf<WebView?>(null) }
var webViewClient = remember { WebViewScrollClient() }
// Move processChapterHtml outside the Composable LaunchedEffect(Unit) {
fun processChapterHtml(rawHtml: String, chapterIndex: Int): String { try {
val document = Jsoup.parse(rawHtml) currentSettings = settingsStore.getSettings()
val body = document.body() } catch (e: Exception) {
body.select("script, style").remove() Timber.e(e, "Error loading settings")
return "<div class='chapter' id='chapter_$chapterIndex'>\n${body.html()}\n</div>" }
} }
// Load book content
LaunchedEffect(bookId) { LaunchedEffect(bookId) {
try { try {
Timber.d("📚 Starting to load book with ID: $bookId") val bookPath = bookStore.getBookPath(bookId)
val book = bookStore.getBook(bookId) val epubBook = EpubReader().readEpub(FileInputStream(bookPath))
Timber.d("📚 Book loaded successfully: ${book.title}") book = epubBook
chapters = epubBook.spine.spineReferences.mapNotNull { spineRef: nl.siegmann.epublib.domain.SpineReference ->
// Load settings first spineRef.resource
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<String>()
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")
}
} }
// 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) { } catch (e: Exception) {
Timber.e(e, "📚 Error loading book") Timber.e(e, "Error loading book")
} }
} }
val css = """ Scaffold(
:root { topBar = {
--page-width: calc(100vw - ${padding * 2}px); AnimatedVisibility(
} visible = showControls,
body { enter = fadeIn() + slideInVertically(),
margin: 0; exit = fadeOut() + slideOutVertically()
padding: ${padding}px; ) {
width: var(--page-width); TopAppBar(
max-width: var(--page-width); title = {
height: calc(100vh - ${padding * 2}px); Column {
column-width: var(--page-width); Text(
column-gap: ${padding * 2}px; text = book?.title ?: "Reader",
column-fill: auto; style = MaterialTheme.typography.titleLarge,
overflow-x: hidden; maxLines = 1,
overflow-y: hidden; overflow = TextOverflow.Ellipsis
font-family: $fontFamily; )
font-size: ${fontSize}px; Text(
line-height: ${lineHeight}em; text = "Chapter ${currentChapterIndex + 1} of ${chapters.size}",
background-color: ${if (isDarkMode) "#1C1B1F" else "#FFFFFF"}; style = MaterialTheme.typography.bodyMedium,
color: ${if (isDarkMode) "#E6E1E5" else "#1C1B1F"}; color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
} )
.chapter { }
break-inside: avoid; },
margin-bottom: 2em; navigationIcon = {
display: inline-block; IconButton(onClick = onNavigateBack) {
width: 100%; Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
} }
img { },
max-width: 100%; actions = {
height: auto; IconButton(onClick = { showChapterList = true }) {
} Icon(Icons.Default.List, contentDescription = "Chapters")
p { }
margin: 0.5em 0; IconButton(onClick = { /* TODO: Add settings dialog */ }) {
text-align: justify; Icon(Icons.Default.Settings, contentDescription = "Settings")
}
""".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")
} }
} }
} catch (e: Exception) { )
Timber.e(e, "Error calculating current page")
}
} }
} }
} ) { padding ->
Box(
// Update webViewClient callback modifier = Modifier
webViewClient.onPageLoaded = { webView -> .fillMaxSize()
calculateCurrentPage(webView) .padding(padding)
} ) {
if (chapters.isNotEmpty()) {
// Update the HTML content creation AndroidView(
fun createHtmlContent(): String = """ modifier = Modifier
<!DOCTYPE html> .fillMaxSize()
<html> .pointerInput(Unit) {
<head> detectTapGestures(
<meta name="viewport" content="width=device-width, initial-scale=1.0"> onTap = { showControls = !showControls }
<style> )
html { },
scroll-snap-type: x mandatory; factory = { context ->
overflow-x: scroll; WebView(context).apply {
scroll-behavior: smooth; webViewClient = WebViewClient()
} this.settings.apply {
body { javaScriptEnabled = true
font-family: '$fontFamily'; defaultFontSize = currentSettings.fontSize.toInt()
font-size: ${fontSize}px; builtInZoomControls = true
line-height: ${lineHeight}; displayZoomControls = false
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
}
}
}
// 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")
} }
} }
},
update = { webView ->
val chapter = chapters[currentChapterIndex]
val html = """
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: ${currentSettings.fontFamily};
line-height: ${currentSettings.lineHeight};
padding: ${currentSettings.padding}px;
margin: 0;
background-color: #${backgroundColor.toString(16)};
color: #${textColor.toString(16)};
}
</style>
</head>
<body>
${String(chapter.data, Charset.defaultCharset())}
</body>
</html>
""".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 = """
<!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 // Chapter list dialog
if (showChapterList) { if (showChapterList) {
AlertDialog( AlertDialog(
onDismissRequest = { showChapterList = false }, onDismissRequest = { showChapterList = false },
title = { Text("Chapters") }, title = { Text("Chapters") },
text = { text = {
LazyColumn { LazyColumn {
items(chapters) { chapter -> items(
Text( items = List(chapters.size) { it },
text = chapter, key = { it }
modifier = Modifier ) { index ->
.fillMaxWidth() ListItem(
.clickable { headlineContent = {
val index = chapters.indexOf(chapter) Text("Chapter ${index + 1}")
navigateToChapter(index) },
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
))
}
}
}
} }
@@ -89,4 +89,9 @@ class BookStore(private val context: Context) {
val bookEntity = bookDao.getBook(bookId) ?: throw IllegalArgumentException("Book not found") val bookEntity = bookDao.getBook(bookId) ?: throw IllegalArgumentException("Book not found")
return EpubReader().readEpub(FileInputStream(bookEntity.filePath)) return EpubReader().readEpub(FileInputStream(bookEntity.filePath))
} }
suspend fun getBookPath(bookId: String): String {
return bookDao.getBook(bookId)?.filePath
?: throw IllegalArgumentException("Book not found")
}
} }
@@ -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 -> return context.dataStore.data.first().let { preferences ->
Settings( Settings(
currentPage = preferences[PreferencesKeys.CURRENT_PAGE] ?: 1, currentPage = preferences[PreferencesKeys.CURRENT_PAGE] ?: 1,
+3 -1
View File
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.Epook" parent="android:Theme.Material.Light.NoActionBar" /> <style name="Theme.Epook" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:progressBarStyle">@style/ProgressBarStyle</item>
</style>
</resources> </resources>
+1
View File
@@ -17,6 +17,7 @@ dependencyResolutionManagement {
google() google()
mavenCentral() mavenCentral()
maven { url = uri("https://jitpack.io") } maven { url = uri("https://jitpack.io") }
maven { url = uri("https://s01.oss.sonatype.org/content/repositories/releases") }
} }
} }