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

View File

@ -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")
}

View File

@ -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">
<activity
android:name=".MainActivity"
android:exported="true"

View File

@ -101,6 +101,7 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
App()
}

View File

@ -18,59 +18,27 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import inhale.rip.epook.data.BookStore
import inhale.rip.epook.data.Settings
import inhale.rip.epook.data.SettingsStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.jsoup.Jsoup
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 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)
}
}
}
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<Resource>()) }
var book by remember { mutableStateOf<EpubBook?>(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<String>()) }
var currentPage by remember { mutableStateOf(1) }
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 {
val document = Jsoup.parse(rawHtml)
val body = document.body()
body.select("script, style").remove()
return "<div class='chapter' id='chapter_$chapterIndex'>\n${body.html()}\n</div>"
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<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")
}
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 = """
<!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
}
}
}
// 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 = """
<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) {
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
))
}
}
}
}

View File

@ -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")
}
}

View File

@ -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,

View File

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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>

View File

@ -17,6 +17,7 @@ dependencyResolutionManagement {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
maven { url = uri("https://s01.oss.sonatype.org/content/repositories/releases") }
}
}