good rendering, good ux. I'd say 1.1 Beta !!!
This commit is contained in:
@@ -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")
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Error calculating current page")
|
Timber.e(e, "Error loading book")
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update webViewClient callback
|
Scaffold(
|
||||||
webViewClient.onPageLoaded = { webView ->
|
topBar = {
|
||||||
calculateCurrentPage(webView)
|
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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
// Update the HTML content creation
|
navigationIcon = {
|
||||||
fun createHtmlContent(): String = """
|
IconButton(onClick = onNavigateBack) {
|
||||||
<!DOCTYPE html>
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
<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';
|
actions = {
|
||||||
font-size: ${fontSize}px;
|
IconButton(onClick = { showChapterList = true }) {
|
||||||
line-height: ${lineHeight};
|
Icon(Icons.Default.List, contentDescription = "Chapters")
|
||||||
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 {
|
IconButton(onClick = { /* TODO: Add settings dialog */ }) {
|
||||||
width: calc(100vw - ${padding * 2}px);
|
Icon(Icons.Default.Settings, contentDescription = "Settings")
|
||||||
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
|
) { padding ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
) {
|
||||||
|
if (chapters.isNotEmpty()) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onTap = { showControls = !showControls }
|
||||||
|
)
|
||||||
|
},
|
||||||
factory = { context ->
|
factory = { context ->
|
||||||
WebView(context).apply {
|
WebView(context).apply {
|
||||||
settings.apply {
|
webViewClient = WebViewClient()
|
||||||
|
this.settings.apply {
|
||||||
javaScriptEnabled = true
|
javaScriptEnabled = true
|
||||||
defaultFontSize = fontSize.toInt()
|
defaultFontSize = currentSettings.fontSize.toInt()
|
||||||
domStorageEnabled = true
|
builtInZoomControls = true
|
||||||
allowFileAccess = true
|
displayZoomControls = false
|
||||||
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 ->
|
update = { webView ->
|
||||||
webViewRef = webView
|
val chapter = chapters[currentChapterIndex]
|
||||||
val htmlContent = """
|
val html = """
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: '$fontFamily';
|
font-family: ${currentSettings.fontFamily};
|
||||||
font-size: ${fontSize}px;
|
line-height: ${currentSettings.lineHeight};
|
||||||
line-height: ${lineHeight};
|
padding: ${currentSettings.padding}px;
|
||||||
padding: ${padding}px;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: ${if (isDarkMode) "#1c1c1c" else "#ffffff"};
|
background-color: #${backgroundColor.toString(16)};
|
||||||
color: ${if (isDarkMode) "#ffffff" else "#000000"};
|
color: #${textColor.toString(16)};
|
||||||
}
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
margin: 1em auto;
|
|
||||||
}
|
|
||||||
.chapter {
|
|
||||||
margin-bottom: 2em;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
$bookContent
|
${String(chapter.data, Charset.defaultCharset())}
|
||||||
<script>$SCROLL_TO_CHAPTER_JS</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
""".trimIndent()
|
||||||
webView.loadDataWithBaseURL(
|
webView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null)
|
||||||
"file:///android_asset/",
|
|
||||||
htmlContent,
|
|
||||||
"text/html",
|
|
||||||
"UTF-8",
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Update chapter list dialog
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
Icons.Default.RadioButtonUnchecked,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(16.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user