Compare commits

...

3 Commits
stable ... main

Author SHA1 Message Date
14dbd76477 Update README.md 2024-12-13 15:56:17 +01:00
inhale-dir
1122e06437 fixes & removed settings 2024-12-13 15:43:16 +01:00
inhale-dir
3295816550 bug fixes after changes 2024-12-13 15:30:29 +01:00
2 changed files with 134 additions and 291 deletions

View File

@ -36,7 +36,7 @@ Epook is a sleek, modern EPUB reader built for Android using Jetpack Compose. It
- Reading progress persistence
- Organized book collection view
### 🔧 Technical Features
### <EFBFBD><EFBFBD><EFBFBD><EFBFBD> Technical Features
- CSS stylesheet handling
- HTML content processing
- Efficient file management
@ -76,4 +76,4 @@ This project is licensed under the MIT License - see the LICENSE file for detail
- [epublib](https://github.com/psiegman/epublib) for EPUB processing
- [Jsoup](https://jsoup.org/) for HTML parsing
- [Material Design 3](https://m3.material.io/) for design guidelines
- [Material Design 3](https://m3.material.io/) for design guidelines

View File

@ -31,8 +31,6 @@ 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
@ -50,6 +48,7 @@ import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebResourceError
import android.webkit.WebSettings
import android.view.View
// Move the Chapter data class outside the composable
private data class Chapter(
@ -58,6 +57,9 @@ private data class Chapter(
val resource: Resource
)
// First, add a constant for the swipe area height
private const val SWIPE_AREA_HEIGHT = 80 // in dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReaderScreen(
@ -66,9 +68,7 @@ fun ReaderScreen(
) {
val context = LocalContext.current
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<List<Chapter>>(emptyList()) }
@ -76,21 +76,12 @@ fun ReaderScreen(
var showControls by remember { mutableStateOf(true) }
var showChapterList by remember { mutableStateOf(false) }
var showSettings by remember { mutableStateOf(false) }
var currentX by remember { mutableStateOf(0f) }
var extractedPath by remember { mutableStateOf<String?>(null) }
var webView by remember { mutableStateOf<WebView?>(null) }
LaunchedEffect(Unit) {
try {
currentSettings = settingsStore.getSettings()
} catch (e: Exception) {
Timber.e(e, "Error loading settings")
}
}
LaunchedEffect(bookId) {
try {
currentChapterIndex = bookStore.getReadingPosition(bookId).first()
@ -149,11 +140,133 @@ fun ReaderScreen(
}
}
Box(modifier = Modifier.fillMaxSize()) {
Box(
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
) {
// WebView without swipe gesture
AndroidView(
factory = { context ->
WebView(context).apply {
webViewClient = object : WebViewClient() {
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
Timber.e("WebView error loading ${request?.url}: ${error?.description}")
super.onReceivedError(view, request, error)
}
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest
): WebResourceResponse? {
val url = request.url.toString()
Timber.d("Loading resource: $url")
try {
if (url.endsWith(".css") && extractedPath != null) {
val possiblePaths = listOf(
url.substringAfterLast("/"),
"Styles/${url.substringAfterLast("/")}",
url.substringAfter("/extracted/")
)
for (cssPath in possiblePaths) {
val cssFile = File(extractedPath!!, cssPath)
if (cssFile.exists()) {
Timber.d("Found CSS file at: ${cssFile.absolutePath}")
return WebResourceResponse(
"text/css",
"UTF-8",
cssFile.inputStream()
)
}
}
}
} catch (e: Exception) {
Timber.e(e, "Error intercepting resource request")
}
return super.shouldInterceptRequest(view, request)
}
}
settings.apply {
javaScriptEnabled = true
builtInZoomControls = true
displayZoomControls = false
allowFileAccess = true
@Suppress("DEPRECATION")
allowFileAccessFromFileURLs = true
@Suppress("DEPRECATION")
allowUniversalAccessFromFileURLs = true
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
domStorageEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
// Add these specific scrolling optimizations
@Suppress("DEPRECATION")
setRenderPriority(WebSettings.RenderPriority.HIGH)
// Disable features that might cause stuttering
loadsImagesAutomatically = true
blockNetworkImage = true // Since we're reading locally
blockNetworkLoads = true // Since we're reading locally
// Enable hardware acceleration
setLayerType(View.LAYER_TYPE_HARDWARE, null)
}
// Set better scrolling properties
overScrollMode = View.OVER_SCROLL_NEVER
isVerticalScrollBarEnabled = false // Hide scrollbar for smoother scrolling
// Set scroll sensitivity
@Suppress("DEPRECATION")
setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY)
// Enable smooth scrolling
isScrollContainer = true
// Set better touch handling
isNestedScrollingEnabled = true
}.also { webView = it }
},
update = { view ->
val chapter = chapters.getOrNull(currentChapterIndex)?.resource
chapter?.let { resource ->
scope.launch {
try {
extractedPath = bookStore.getBook(bookId)?.extractedPath
?: throw IllegalStateException("Book not properly extracted")
val baseUrl = "file://$extractedPath/"
val fullUrl = baseUrl + resource.href.replace("../", "")
Timber.d("Loading chapter from: $fullUrl")
Timber.d("Base URL: $baseUrl")
view.loadUrl(fullUrl)
} catch (e: Exception) {
Timber.e(e, "Error loading chapter")
}
}
}
},
modifier = Modifier
.fillMaxSize()
.padding(bottom = if (showControls) 80.dp else 0.dp)
.padding(bottom = if (showControls) SWIPE_AREA_HEIGHT.dp else 0.dp)
)
// Add swipe area at the bottom with tap gesture
Box(
modifier = Modifier
.fillMaxWidth()
.height(SWIPE_AREA_HEIGHT.dp)
.align(Alignment.BottomCenter)
.pointerInput(Unit) {
detectTapGestures(
onTap = { showControls = !showControls }
@ -166,7 +279,6 @@ fun ReaderScreen(
initialX = offset.x
},
onDragEnd = {
// Implement drag threshold for swipe
val dragThreshold = size.width * 0.2f // 20% of screen width
val dragDistance = initialX - currentX
@ -180,99 +292,14 @@ fun ReaderScreen(
currentChapterIndex--
}
}
currentX = 0f
}
) { change, _ ->
) { change, dragAmount ->
currentX = change.position.x
change.consume()
}
}
) {
AndroidView(
factory = { context ->
WebView(context).apply {
webViewClient = object : WebViewClient() {
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
Timber.e("WebView error loading ${request?.url}: ${error?.description}")
super.onReceivedError(view, request, error)
}
override fun shouldInterceptRequest(
view: WebView?,
request: WebResourceRequest
): WebResourceResponse? {
val url = request.url.toString()
Timber.d("Loading resource: $url")
try {
if (url.endsWith(".css") && extractedPath != null) {
val possiblePaths = listOf(
url.substringAfterLast("/"),
"Styles/${url.substringAfterLast("/")}",
url.substringAfter("/extracted/")
)
for (cssPath in possiblePaths) {
val cssFile = File(extractedPath!!, cssPath)
if (cssFile.exists()) {
Timber.d("Found CSS file at: ${cssFile.absolutePath}")
return WebResourceResponse(
"text/css",
"UTF-8",
cssFile.inputStream()
)
}
}
}
} catch (e: Exception) {
Timber.e(e, "Error intercepting resource request")
}
return super.shouldInterceptRequest(view, request)
}
}
settings.apply {
javaScriptEnabled = true
builtInZoomControls = true
displayZoomControls = false
allowFileAccess = true
@Suppress("DEPRECATION")
allowFileAccessFromFileURLs = true
@Suppress("DEPRECATION")
allowUniversalAccessFromFileURLs = true
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
domStorageEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
}
}.also { webView = it }
},
update = { view ->
val chapter = chapters.getOrNull(currentChapterIndex)?.resource
chapter?.let { resource ->
scope.launch {
try {
extractedPath = bookStore.getBook(bookId)?.extractedPath
?: throw IllegalStateException("Book not properly extracted")
val baseUrl = "file://$extractedPath/"
val fullUrl = baseUrl + resource.href.replace("../", "")
Timber.d("Loading chapter from: $fullUrl")
Timber.d("Base URL: $baseUrl")
view.loadUrl(fullUrl)
} catch (e: Exception) {
Timber.e(e, "Error loading chapter")
}
}
}
},
modifier = Modifier.fillMaxSize()
)
}
)
// Controls overlay
AnimatedVisibility(
@ -340,30 +367,6 @@ fun ReaderScreen(
}
}
}
// Back button overlay
AnimatedVisibility(
visible = showControls,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier
.align(Alignment.TopStart)
.statusBarsPadding()
.padding(8.dp)
) {
IconButton(
onClick = onNavigateBack,
colors = IconButtonDefaults.iconButtonColors(
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f),
contentColor = MaterialTheme.colorScheme.onSurface
)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back"
)
}
}
}
// Chapter list dialog
@ -414,164 +417,4 @@ fun ReaderScreen(
}
)
}
// Add Settings Dialog
if (showSettings) {
AlertDialog(
onDismissRequest = { showSettings = false },
title = {
Text(
"Reader Settings",
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Font Size Slider
Column {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Font Size",
style = MaterialTheme.typography.titleMedium
)
Text(
"${currentSettings.fontSize.toInt()}sp",
style = MaterialTheme.typography.bodyMedium
)
}
Slider(
value = currentSettings.fontSize,
onValueChange = { newSize ->
scope.launch {
val newSettings = currentSettings.copy(fontSize = newSize)
settingsStore.saveSettings(newSettings)
currentSettings = newSettings
}
},
valueRange = 12f..24f,
steps = 11
)
}
// Line Height Slider
Column {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Line Height",
style = MaterialTheme.typography.titleMedium
)
Text(
"%.1fx".format(currentSettings.lineHeight),
style = MaterialTheme.typography.bodyMedium
)
}
Slider(
value = currentSettings.lineHeight,
onValueChange = { newHeight ->
scope.launch {
val newSettings = currentSettings.copy(lineHeight = newHeight)
settingsStore.saveSettings(newSettings)
currentSettings = newSettings
}
},
valueRange = 1f..2f,
steps = 9
)
}
// Padding Slider
Column {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Margin",
style = MaterialTheme.typography.titleMedium
)
Text(
"${currentSettings.padding.toInt()}dp",
style = MaterialTheme.typography.bodyMedium
)
}
Slider(
value = currentSettings.padding,
onValueChange = { newPadding ->
scope.launch {
val newSettings = currentSettings.copy(padding = newPadding)
settingsStore.saveSettings(newSettings)
currentSettings = newSettings
}
},
valueRange = 8f..32f,
steps = 11
)
}
// Font Family Dropdown
Column {
Text(
"Font Family",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
val fontFamilies = listOf("Georgia", "Roboto", "Times New Roman", "Arial", "Verdana")
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded }
) {
OutlinedTextField(
value = currentSettings.fontFamily,
onValueChange = {},
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
modifier = Modifier
.menuAnchor()
.fillMaxWidth()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
fontFamilies.forEach { font ->
DropdownMenuItem(
text = { Text(font) },
onClick = {
scope.launch {
val newSettings = currentSettings.copy(fontFamily = font)
settingsStore.saveSettings(newSettings)
currentSettings = newSettings
}
expanded = false
}
)
}
}
}
}
}
},
confirmButton = {
TextButton(onClick = { showSettings = false }) {
Text("Close")
}
}
)
}
}