[优化] 优化本地书籍界面

This commit is contained in:
HapeLee
2026-04-14 00:50:43 +08:00
parent 591d8dd542
commit 6a354acd29
3 changed files with 323 additions and 76 deletions
@@ -1,6 +1,7 @@
package io.legado.app.ui.book.import.local
import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.animateContentSize
@@ -21,12 +22,14 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.AddCircleOutline
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.CloudDownload
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.outlined.AddCircleOutline
import androidx.compose.material.icons.outlined.Book
import androidx.compose.material.icons.outlined.Description
import androidx.compose.material3.CircularProgressIndicator
@@ -58,7 +61,7 @@ import io.legado.app.constant.AppConst
import io.legado.app.constant.AppPattern
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.help.config.AppConfig
import io.legado.app.ui.config.importBookConfig.ImportBookConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.permission.Permissions
@@ -69,11 +72,13 @@ import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.SelectionActions
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
import io.legado.app.ui.widget.components.button.TopBarActionButton
import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.list.ListScaffold
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.text.AppText
@@ -102,8 +107,10 @@ private fun ImportBookContent(
onNavigateBack: () -> Unit,
onNavigateToLevel: (Int) -> Unit,
onSelectAll: () -> Unit,
onClearSelection: () -> Unit,
onSelectInvert: () -> Unit,
onAddToBookshelf: () -> Unit,
onItemAddToBookshelf: (ImportBook) -> Unit,
onDeleteSelection: () -> Unit,
onItemClick: (ImportBook) -> Unit
) {
@@ -182,6 +189,7 @@ private fun ImportBookContent(
)
},
selectionActions = SelectionActions(
onClearSelection = onClearSelection,
onSelectAll = onSelectAll,
onSelectInvert = onSelectInvert,
primaryAction = ActionItem(
@@ -227,7 +235,8 @@ private fun ImportBookContent(
modifier = Modifier.animateItem(),
item = item,
isSelected = item.selectionId in state.selectedIds,
onClick = { onItemClick(item) }
onClick = { onItemClick(item) },
onAddToBookshelf = { onItemAddToBookshelf(item) }
)
}
}
@@ -246,13 +255,25 @@ fun ImportBookScreen(
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var showFolderPicker by remember { mutableStateOf(false) }
var showImportFileNameDialog by remember { mutableStateOf(false) }
var fileNameJs by remember { mutableStateOf(AppConfig.bookImportFileName.orEmpty()) }
var pendingSingleAddBook by remember { mutableStateOf<ImportBook?>(null) }
var fileNameJs by remember { mutableStateOf(ImportBookConfig.bookImportFileName.orEmpty()) }
var pickerTarget by remember { mutableStateOf(ImportFolderPickTarget.IMPORT_FOLDER) }
val selectDocTree = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree()
) { uri ->
viewModel.dispatch(ImportBookIntent.FolderPicked(uri, pickerTarget))
}
val handleBack = {
when {
uiState.selectedIds.isNotEmpty() -> viewModel.clearSelection()
uiState.canGoBack -> viewModel.dispatch(ImportBookIntent.NavigateBack)
else -> onBackClick()
}
}
BackHandler(enabled = !showFolderPicker && !showImportFileNameDialog) {
handleBack()
}
LaunchedEffect(viewModel) {
viewModel.dispatch(ImportBookIntent.Initialize)
@@ -320,30 +341,48 @@ fun ImportBookScreen(
},
confirmText = stringResource(android.R.string.ok),
onConfirm = {
AppConfig.bookImportFileName = fileNameJs
ImportBookConfig.bookImportFileName = fileNameJs
showImportFileNameDialog = false
},
dismissText = stringResource(android.R.string.cancel),
onDismiss = { showImportFileNameDialog = false }
)
pendingSingleAddBook?.let { book ->
AppAlertDialog(
show = true,
onDismissRequest = { pendingSingleAddBook = null },
title = stringResource(R.string.add_to_bookshelf),
text = stringResource(R.string.check_add_bookshelf, book.name),
confirmText = stringResource(android.R.string.ok),
onConfirm = {
viewModel.dispatch(ImportBookIntent.AddSingleToBookshelf(book))
pendingSingleAddBook = null
},
dismissText = stringResource(android.R.string.cancel),
onDismiss = { pendingSingleAddBook = null }
)
}
ImportBookContent(
state = uiState,
onBackClick = onBackClick,
onBackClick = handleBack,
onSearchToggle = { viewModel.dispatch(ImportBookIntent.SearchToggle(it)) },
onSearchQueryChange = { viewModel.dispatch(ImportBookIntent.SearchQueryChange(it)) },
onSelectFolder = { showFolderPicker = true },
onScanFolder = { viewModel.dispatch(ImportBookIntent.ScanFolder) },
onImportFileName = {
fileNameJs = AppConfig.bookImportFileName.orEmpty()
fileNameJs = ImportBookConfig.bookImportFileName.orEmpty()
showImportFileNameDialog = true
},
onSortChange = { viewModel.dispatch(ImportBookIntent.SortChange(it)) },
onNavigateBack = { viewModel.dispatch(ImportBookIntent.NavigateBack) },
onNavigateToLevel = { viewModel.dispatch(ImportBookIntent.NavigateToLevel(it)) },
onSelectAll = { viewModel.dispatch(ImportBookIntent.SelectAll) },
onClearSelection = { viewModel.clearSelection() },
onSelectInvert = { viewModel.dispatch(ImportBookIntent.SelectInvert) },
onAddToBookshelf = { viewModel.dispatch(ImportBookIntent.AddToBookshelf) },
onItemAddToBookshelf = { pendingSingleAddBook = it },
onDeleteSelection = { viewModel.dispatch(ImportBookIntent.DeleteSelection) },
onItemClick = { viewModel.dispatch(ImportBookIntent.ItemClick(it)) }
)
@@ -417,7 +456,8 @@ private fun ImportBookItem(
modifier: Modifier,
item: ImportBook,
isSelected: Boolean,
onClick: () -> Unit
onClick: () -> Unit,
onAddToBookshelf: () -> Unit
) {
val containerColor = if (isSelected) {
LegadoTheme.colorScheme.secondaryContainer
@@ -438,7 +478,7 @@ private fun ImportBookItem(
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
AppIcon(
imageVector = when {
item.isDir -> Icons.Default.Folder
item.isOnBookShelf -> Icons.Outlined.Book
@@ -446,7 +486,7 @@ private fun ImportBookItem(
},
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = if (item.isDir) {
tint = if (item.isOnBookShelf) {
LegadoTheme.colorScheme.primary
} else {
LegadoTheme.colorScheme.onSurfaceVariant
@@ -486,10 +526,9 @@ private fun ImportBookItem(
if (!item.isDir && !item.isOnBookShelf) {
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = if (isSelected) Icons.Default.Check else Icons.Default.ChevronRight,
contentDescription = null,
tint = LegadoTheme.colorScheme.primary
SmallIconButton(
onClick = onAddToBookshelf,
imageVector = if (isSelected) Icons.Default.Check else Icons.Default.AddCircleOutline
)
}
}
@@ -2,6 +2,8 @@ package io.legado.app.ui.book.import.local
import android.app.Application
import android.net.Uri
import android.provider.DocumentsContract
import android.provider.DocumentsContract.getTreeDocumentId
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import androidx.compose.runtime.Immutable
@@ -11,29 +13,28 @@ import io.legado.app.constant.AppLog
import io.legado.app.constant.AppPattern
import io.legado.app.constant.AppPattern.archiveFileRegex
import io.legado.app.constant.AppPattern.bookFileRegex
import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.help.config.AppConfig
import io.legado.app.model.localBook.LocalBook
import io.legado.app.ui.config.importBookConfig.ImportBookConfig
import io.legado.app.ui.widget.components.list.InteractionState
import io.legado.app.ui.widget.components.list.ListUiState
import io.legado.app.utils.AlphanumComparator
import io.legado.app.utils.ArchiveUtils
import io.legado.app.utils.FileDoc
import io.legado.app.utils.delete
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.exists
import io.legado.app.utils.isContentScheme
import io.legado.app.utils.isUri
import io.legado.app.utils.list
import io.legado.app.utils.mapParallel
import io.legado.app.utils.putPrefInt
import io.legado.app.utils.takePersistablePermissionSafely
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -49,6 +50,7 @@ import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
@@ -85,6 +87,7 @@ sealed interface ImportBookIntent {
data object SelectAll : ImportBookIntent
data object SelectInvert : ImportBookIntent
data object AddToBookshelf : ImportBookIntent
data class AddSingleToBookshelf(val item: ImportBook) : ImportBookIntent
data object DeleteSelection : ImportBookIntent
data class ItemClick(val item: ImportBook) : ImportBookIntent
data class ArchiveEntrySelected(val fileDoc: FileDoc, val fileName: String) : ImportBookIntent
@@ -105,6 +108,11 @@ sealed interface ImportBookEffect {
class ImportBookViewModel(application: Application) : BaseViewModel(application) {
private enum class SourceMode {
CURRENT_DIR,
SCAN_RECURSIVE
}
private data class InternalState(
val rootDoc: FileDoc? = null,
val subDocs: List<FileDoc> = emptyList(),
@@ -112,16 +120,18 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
val selectedIds: Set<String> = emptySet(),
val searchKey: String = "",
val sort: Int,
val sourceMode: SourceMode = SourceMode.CURRENT_DIR,
val interaction: InteractionState = InteractionState()
)
private val _state = MutableStateFlow(
InternalState(sort = context.getPrefInt(PreferKey.localBookImportSort))
InternalState(sort = ImportBookConfig.localBookImportSort)
)
private val _effects = MutableSharedFlow<ImportBookEffect>(extraBufferCapacity = 1)
val effects = _effects.asSharedFlow()
private var scanDocJob: Job? = null
private var autoSyncJob: Job? = null
fun dispatch(intent: ImportBookIntent) {
when (intent) {
@@ -130,7 +140,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
_effects.tryEmit(
ImportBookEffect.RequestFolderPicker(
target = ImportFolderPickTarget.IMPORT_FOLDER,
initialUri = AppConfig.importBookPath
initialUri = ImportBookConfig.importBookPath
?.takeIf { it.isUri() }
?.toUri()
)
@@ -147,6 +157,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
ImportBookIntent.SelectAll -> selectAllCheckable()
ImportBookIntent.SelectInvert -> invertSelection()
ImportBookIntent.AddToBookshelf -> addSelectedToBookshelf()
is ImportBookIntent.AddSingleToBookshelf -> addSingleToBookshelf(intent.item)
ImportBookIntent.DeleteSelection -> deleteSelectedDocs()
is ImportBookIntent.ItemClick -> onItemClick(intent.item)
is ImportBookIntent.ArchiveEntrySelected -> onArchiveEntrySelected(
@@ -220,34 +231,66 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
fun hasRootDoc(): Boolean = _state.value.rootDoc != null
private fun initialize() {
if (AppConfig.defaultBookTreeUri.isNullOrBlank()) {
val defaultPath = ImportBookConfig.defaultBookTreeUri?.takeIf { it.isUri() }
val effectiveDefaultPath = defaultPath ?: firstPersistedTreeUri()?.toString()
if (effectiveDefaultPath.isNullOrBlank()) {
_effects.tryEmit(
ImportBookEffect.RequestFolderPicker(target = ImportFolderPickTarget.DEFAULT_BOOK)
)
return
}
if (AppConfig.importBookPath.isNullOrBlank()) {
AppConfig.importBookPath = AppConfig.defaultBookTreeUri
if (ImportBookConfig.defaultBookTreeUri != effectiveDefaultPath) {
ImportBookConfig.defaultBookTreeUri = effectiveDefaultPath
}
initRootDoc(changedFolder = false)
val importPath = ImportBookConfig.importBookPath
if (importPath.isNullOrBlank() || !importPath.isUri()) {
ImportBookConfig.importBookPath = effectiveDefaultPath
}
initRootDoc(changedFolder = true)
startAutoSync()
}
private fun onFolderPicked(uri: Uri?, target: ImportFolderPickTarget) {
uri ?: return
uri.takePersistablePermissionSafely(context)
val pickedUri = persistFolderPermission(uri)
when (target) {
ImportFolderPickTarget.DEFAULT_BOOK -> {
AppConfig.defaultBookTreeUri = uri.toString()
if (AppConfig.importBookPath.isNullOrBlank()) {
AppConfig.importBookPath = AppConfig.defaultBookTreeUri
ImportBookConfig.defaultBookTreeUri = pickedUri.toString()
if (ImportBookConfig.importBookPath.isNullOrBlank()) {
ImportBookConfig.importBookPath = ImportBookConfig.defaultBookTreeUri
}
}
ImportFolderPickTarget.IMPORT_FOLDER -> {
AppConfig.importBookPath = uri.toString()
ImportBookConfig.importBookPath = pickedUri.toString()
if (ImportBookConfig.defaultBookTreeUri.isNullOrBlank()) {
ImportBookConfig.defaultBookTreeUri = pickedUri.toString()
}
}
}
initRootDoc(changedFolder = true)
startAutoSync()
}
private fun persistFolderPermission(uri: Uri): Uri {
val normalizedUri = normalizeFolderUri(uri)
uri.takePersistablePermissionSafely(context)
if (normalizedUri != uri) {
normalizedUri.takePersistablePermissionSafely(context)
}
return normalizedUri
}
private fun normalizeFolderUri(uri: Uri): Uri {
if (!uri.isContentScheme()) return uri
return runCatching {
DocumentsContract.buildTreeDocumentUri(uri.authority, getTreeDocumentId(uri))
}.recoverCatching {
DocumentsContract.buildTreeDocumentUri(
uri.authority,
DocumentsContract.getDocumentId(uri)
)
}.getOrDefault(uri)
}
private fun initRootDoc(changedFolder: Boolean) {
@@ -255,48 +298,111 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
reloadCurrentDoc()
return
}
val lastPath = AppConfig.importBookPath
if (lastPath.isNullOrBlank()) {
val candidates = linkedSetOf<String>().apply {
ImportBookConfig.importBookPath
?.takeIf { it.isNotBlank() && it.isUri() }
?.let(::add)
ImportBookConfig.defaultBookTreeUri
?.takeIf { it.isNotBlank() && it.isUri() }
?.let(::add)
}
if (candidates.isEmpty()) {
firstPersistedTreeUri()?.toString()?.let { persistedPath ->
if (trySetRootDoc(persistedPath)) {
ImportBookConfig.defaultBookTreeUri = persistedPath
ImportBookConfig.importBookPath = persistedPath
return
}
}
_effects.tryEmit(
ImportBookEffect.RequestFolderPicker(target = ImportFolderPickTarget.IMPORT_FOLDER)
ImportBookEffect.RequestFolderPicker(target = ImportFolderPickTarget.DEFAULT_BOOK)
)
return
}
val rootUri = if (lastPath.isUri()) {
lastPath.toUri()
} else {
Uri.fromFile(File(lastPath))
}
when {
rootUri.isContentScheme() -> {
kotlin.runCatching {
val doc = DocumentFile.fromTreeUri(context, rootUri)
if (doc == null || doc.name.isNullOrEmpty()) {
_effects.tryEmit(
ImportBookEffect.RequestFolderPicker(target = ImportFolderPickTarget.IMPORT_FOLDER)
)
} else {
setRootDoc(FileDoc.fromDocumentFile(doc))
}
}.onFailure {
_effects.tryEmit(
ImportBookEffect.RequestFolderPicker(target = ImportFolderPickTarget.IMPORT_FOLDER)
)
for (path in candidates) {
if (trySetRootDoc(path)) {
if (ImportBookConfig.importBookPath != path) {
ImportBookConfig.importBookPath = path
}
return
}
else -> {
kotlin.runCatching {
setRootDoc(FileDoc.fromFile(File(rootUri.path!!)))
}.onFailure {
_effects.tryEmit(
ImportBookEffect.RequestFolderPicker(target = ImportFolderPickTarget.IMPORT_FOLDER)
)
val persistedUri = path.takeIf { it.isUri() }
?.toUri()
?.let(::findPersistedTreeUri)
if (persistedUri != null) {
val persistedPath = persistedUri.toString()
if (trySetRootDoc(persistedPath)) {
ImportBookConfig.importBookPath = persistedPath
return
}
}
}
firstPersistedTreeUri()?.toString()?.let { persistedPath ->
if (trySetRootDoc(persistedPath)) {
ImportBookConfig.defaultBookTreeUri = persistedPath
ImportBookConfig.importBookPath = persistedPath
return
}
}
_effects.tryEmit(
ImportBookEffect.RequestFolderPicker(target = ImportFolderPickTarget.DEFAULT_BOOK)
)
}
private fun trySetRootDoc(path: String): Boolean {
val rootUri = if (path.isUri()) path.toUri() else Uri.fromFile(File(path))
return kotlin.runCatching {
if (rootUri.isContentScheme()) {
val doc = DocumentFile.fromTreeUri(context, rootUri)
?: DocumentFile.fromSingleUri(context, rootUri)
?: return false
val rootName = doc.name
?.takeIf { it.isNotBlank() }
?: runCatching {
getTreeDocumentId(rootUri).substringAfter(':').ifBlank { "root" }
}.getOrElse { "root" }
setRootDoc(
FileDoc(
name = rootName,
isDir = true,
size = doc.length(),
lastModified = doc.lastModified(),
uri = doc.uri
)
)
} else {
val file = File(rootUri.path ?: return false)
if (!file.exists() || !file.isDirectory) return false
setRootDoc(FileDoc.fromFile(file))
}
true
}.getOrDefault(false)
}
private fun findPersistedTreeUri(targetUri: Uri): Uri? {
if (!targetUri.isContentScheme()) return null
val permissions = context.contentResolver.persistedUriPermissions
if (permissions.isEmpty()) return null
val targetAuthority = targetUri.authority
val targetTreeId = runCatching { getTreeDocumentId(targetUri) }.getOrNull()
return permissions.firstOrNull { permission ->
permission.isReadPermission
&& permission.uri.authority == targetAuthority
&& runCatching { getTreeDocumentId(permission.uri) }.getOrNull() == targetTreeId
}?.uri ?: permissions.firstOrNull { permission ->
permission.isReadPermission && permission.uri.authority == targetAuthority
}?.uri
}
private fun firstPersistedTreeUri(): Uri? {
return context.contentResolver.persistedUriPermissions
.firstOrNull { it.isReadPermission && it.uri.isContentScheme() }
?.uri
?.let(::normalizeFolderUri)
}
fun clearRoot() {
@@ -305,7 +411,8 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
rootDoc = null,
subDocs = emptyList(),
sourceDocs = emptyList(),
selectedIds = emptySet()
selectedIds = emptySet(),
sourceMode = SourceMode.CURRENT_DIR
)
}
}
@@ -315,7 +422,8 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
it.copy(
rootDoc = rootDoc,
subDocs = emptyList(),
selectedIds = emptySet()
selectedIds = emptySet(),
sourceMode = SourceMode.CURRENT_DIR
)
}
loadCurrentDoc()
@@ -369,7 +477,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
fun setSort(sort: Int) {
_state.update { it.copy(sort = sort) }
context.putPrefInt(PreferKey.localBookImportSort, sort)
ImportBookConfig.localBookImportSort = sort
}
fun setSearchMode(isSearch: Boolean) {
@@ -439,6 +547,22 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
}
}
private fun addSingleToBookshelf(item: ImportBook) {
if (item.isDir || item.isOnBookShelf) return
execute {
LocalBook.importFiles(listOf(item.file.uri))
}.onError {
context.toastOnUi("添加书架失败,请尝试重新选择文件夹")
AppLog.put("添加书架失败\n${it.localizedMessage}", it)
}.onSuccess {
context.toastOnUi("添加书架成功")
}.onFinally {
_state.update { state ->
state.copy(selectedIds = state.selectedIds - item.selectionId)
}
}
}
private fun onItemClick(item: ImportBook) {
when {
item.isDir -> navigateNext(item.file)
@@ -506,12 +630,8 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
.filter { it.toString() in selectedIds }
.forEach { it.delete() }
}.onFinally {
_state.update { state ->
state.copy(
sourceDocs = state.sourceDocs.filterNot { it.toString() in selectedIds },
selectedIds = emptySet()
)
}
clearSelection()
refreshCurrentSource()
}
}
@@ -523,6 +643,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
it.copy(
sourceDocs = emptyList(),
selectedIds = emptySet(),
sourceMode = SourceMode.SCAN_RECURSIVE,
interaction = it.interaction.copy(isLoading = true)
)
}
@@ -534,6 +655,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
_state.update {
it.copy(
sourceDocs = docs,
selectedIds = emptySet(),
interaction = it.interaction.copy(isLoading = false)
)
}
@@ -562,22 +684,18 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
it.copy(
sourceDocs = emptyList(),
selectedIds = emptySet(),
sourceMode = SourceMode.CURRENT_DIR,
interaction = it.interaction.copy(isLoading = true)
)
}
execute {
fileDoc.list { item ->
when {
item.name.startsWith(".") -> false
item.isDir -> true
else -> item.name.matches(bookFileRegex) || item.name.matches(archiveFileRegex)
}
} ?: emptyList()
listCurrentDocDocs(fileDoc)
}.onSuccess { docs ->
_state.update {
it.copy(
sourceDocs = docs,
selectedIds = emptySet(),
interaction = it.interaction.copy(isLoading = false)
)
}
@@ -589,6 +707,68 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
}
}
private fun listCurrentDocDocs(fileDoc: FileDoc): List<FileDoc> {
return fileDoc.list { item ->
when {
item.name.startsWith(".") -> false
item.isDir -> true
else -> item.name.matches(bookFileRegex) || item.name.matches(archiveFileRegex)
}
} ?: emptyList()
}
private fun refreshCurrentSource() {
when (_state.value.sourceMode) {
SourceMode.CURRENT_DIR -> loadCurrentDoc()
SourceMode.SCAN_RECURSIVE -> scanCurrentDoc()
}
}
private fun startAutoSync() {
if (autoSyncJob?.isActive == true) return
autoSyncJob = viewModelScope.launch(IO) {
while (isActive) {
delay(1500)
syncSourceDocs()
}
}
}
private suspend fun syncSourceDocs() {
if (scanDocJob?.isActive == true) return
val snapshot = _state.value
if (snapshot.interaction.isLoading || snapshot.sourceDocs.isEmpty()) return
val current = currentDoc(snapshot) ?: return
when (snapshot.sourceMode) {
SourceMode.CURRENT_DIR -> {
val latestDocs = kotlin.runCatching { listCurrentDocDocs(current) }.getOrNull() ?: return
val latestIds = latestDocs.asSequence().map { it.toString() }.toSet()
val sourceIds = snapshot.sourceDocs.asSequence().map { it.toString() }.toSet()
if (latestIds == sourceIds) return
_state.update { state ->
state.copy(
sourceDocs = latestDocs,
selectedIds = state.selectedIds.filterTo(hashSetOf()) { it in latestIds }
)
}
}
SourceMode.SCAN_RECURSIVE -> {
if (snapshot.sourceDocs.none { !it.exists() }) return
val latestDocs = kotlin.runCatching { scanDoc(current) }.getOrNull() ?: return
val latestIds = latestDocs.asSequence().map { it.toString() }.toSet()
_state.update { state ->
state.copy(
sourceDocs = latestDocs,
selectedIds = state.selectedIds.filterTo(hashSetOf()) { it in latestIds }
)
}
}
}
}
private suspend fun scanDoc(fileDoc: FileDoc): List<FileDoc> {
val channel = Channel<FileDoc>(UNLIMITED)
var n = 1
@@ -617,6 +797,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
}
override fun onCleared() {
autoSyncJob?.cancel()
scanDocJob?.cancel()
super.onCleared()
}
@@ -0,0 +1,27 @@
package io.legado.app.ui.config.importBookConfig
import io.legado.app.constant.PreferKey
import io.legado.app.ui.config.prefDelegate
object ImportBookConfig {
var defaultBookTreeUri by prefDelegate(
PreferKey.defaultBookTreeUri,
null as String?
)
var importBookPath by prefDelegate(
PreferKey.importBookPath,
null as String?
)
var bookImportFileName by prefDelegate(
PreferKey.bookImportFileName,
null as String?
)
var localBookImportSort by prefDelegate(
PreferKey.localBookImportSort,
0
)
}