Fixed lagging in downloads screen, but screen position in the epub remains broken. Updated README.md.
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user