Fixed lagging in downloads screen, but screen position in the epub remains broken. Updated README.md.

This commit is contained in:
sNice/Python
2026-03-31 09:59:05 -05:00
parent 271651ff02
commit 667d4e1a7b
4 changed files with 64 additions and 28 deletions
+14 -6
View File
@@ -1,4 +1,4 @@
# AO3 Client
# AO3 Client (v1.0)
A feature-rich Android client for Archive of Our Own (AO3), designed for a seamless browsing, downloading, and reading experience.
@@ -13,19 +13,23 @@ A feature-rich Android client for Archive of Our Own (AO3), designed for a seaml
- **Built-in EPUB Reader**:
- Custom reader optimized for fanfiction.
- Dark mode by default for comfortable reading.
- Remembers your scroll position for every work.
- **Persistent Progress**: Remembers your scroll position for every work using a deterministic layout-based restoration.
- **Offline Mode**: A dedicated toggle to browse your downloaded library without an internet connection.
- **Library Management**:
- Search through your downloaded works.
- Sort by Name, Date, or File Size.
- Move, delete, and manually manage your files.
- **State Persistence**: The app remembers where you left off, including your last visited URL and the last book you were reading.
- Move, delete, and manually manage your files with safety confirmations.
- **State Persistence**: The app remembers where you left off, including your last visited URL and the last book you were reading, powered by **Jetpack DataStore**.
## Tech Stack
## Tech Stack & Architecture
This project follows **Clean Architecture** principles and modern Android development practices:
- **Language**: [Kotlin](https://kotlinlang.org/)
- **UI Framework**: [Jetpack Compose](https://developer.android.com/jetpack/compose)
- **Navigation**: [Compose Navigation](https://developer.android.com/jetpack/compose/navigation)
- **State Management**: [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) with [StateFlow](https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/)
- **Persistence**: [Jetpack DataStore](https://developer.android.com/topic/libraries/architecture/datastore) (Preferences)
- **Concurrency**: [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html) for non-blocking I/O
- **EPUB Engine**: [epublib](https://github.com/psiegman/epublib)
- **Design System**: Material 3
@@ -56,6 +60,10 @@ A feature-rich Android client for Archive of Our Own (AO3), designed for a seaml
Contributions are welcome! Please feel free to submit a Pull Request.
## Acknowledgements
This project was developed and refactored with the assistance of **AI technology**, which helped in modernizing the architecture, improving performance, and implementing robust state management.
## License
This project is licensed under the MIT License - see the LICENSE file for details.
@@ -213,7 +213,10 @@ fun DownloadsScreen(
}
}
items(items) { item ->
items(
items = items,
key = { it.absolutePath }
) { item ->
ListItem(
headlineContent = { Text(item.name) },
supportingContent = {
@@ -1,5 +1,6 @@
package net.pythonsden.ao3_.reader
import android.view.ViewTreeObserver
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.foundation.layout.Box
@@ -19,6 +20,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -74,12 +78,13 @@ fun ReaderScreen(
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
is ReaderUiState.Success -> {
var hasRestoredScroll by remember(file.absolutePath) { mutableStateOf(false) }
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { ctx ->
WebView(ctx).apply {
setBackgroundColor(android.graphics.Color.parseColor("#121212"))
// Security: Disable JavaScript for EPUB reading unless strictly needed
settings.javaScriptEnabled = false
settings.allowFileAccess = false
settings.allowContentAccess = false
@@ -87,20 +92,35 @@ fun ReaderScreen(
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
if (state.initialScroll > 0) {
// Restore scroll position
if (!hasRestoredScroll && state.initialScroll > 0) {
view?.scrollTo(0, state.initialScroll)
// Multiple attempts to ensure it works after layout
view?.postDelayed({ view.scrollTo(0, state.initialScroll) }, 100)
view?.postDelayed({ view.scrollTo(0, state.initialScroll) }, 300)
}
}
}
val layoutListener = object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (!hasRestoredScroll && state.initialScroll > 0) {
if (contentHeight > 0) {
scrollTo(0, state.initialScroll)
// If we've scrolled to or past the target, or reached bottom
if (scrollY >= state.initialScroll || scrollY > 0) {
hasRestoredScroll = true
}
}
} else if (state.initialScroll == 0) {
hasRestoredScroll = true
}
}
}
viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
setOnScrollChangeListener { _, _, _, scrollY, _ ->
if (hasRestoredScroll) {
viewModel.saveScrollPosition(file.absolutePath, scrollY)
}
}
}
},
update = { webView ->
if (webView.tag != state.content) {
@@ -2,11 +2,13 @@ package net.pythonsden.ao3_.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.pythonsden.ao3_.data.repository.EpubRepository
import net.pythonsden.ao3_.data.repository.FileRepository
import java.io.File
@@ -46,6 +48,7 @@ class DownloadsViewModel(
}
private suspend fun refreshFiles(dir: File, query: String, sort: SortOrder) {
withContext(Dispatchers.Default) {
val allItems = fileRepository.listFiles(dir)
val filtered = if (query.isBlank()) {
allItems
@@ -53,7 +56,7 @@ class DownloadsViewModel(
allItems.filter { it.name.contains(query, ignoreCase = true) }
}
_items.value = filtered.sortedWith { f1, f2 ->
val sorted = filtered.sortedWith { f1, f2 ->
if (f1.isDirectory && !f2.isDirectory) -1
else if (!f1.isDirectory && f2.isDirectory) 1
else {
@@ -64,6 +67,8 @@ class DownloadsViewModel(
}
}
}
_items.value = sorted
}
}
fun navigateTo(dir: File) {