[优化] 优化本地书籍界面
This commit is contained in:
3 files changed
+319
-72
No files matched your search
@@ -1,6 +1,7 @@
|
|||||||
package io.legado.app.ui.book.import.local
|
package io.legado.app.ui.book.import.local
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
@@ -21,12 +22,14 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
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.Check
|
||||||
import androidx.compose.material.icons.filled.ChevronRight
|
import androidx.compose.material.icons.filled.ChevronRight
|
||||||
import androidx.compose.material.icons.filled.CloudDownload
|
import androidx.compose.material.icons.filled.CloudDownload
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Folder
|
import androidx.compose.material.icons.filled.Folder
|
||||||
import androidx.compose.material.icons.filled.FolderOpen
|
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.Book
|
||||||
import androidx.compose.material.icons.outlined.Description
|
import androidx.compose.material.icons.outlined.Description
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
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.constant.AppPattern
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.Book
|
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.alert
|
||||||
import io.legado.app.lib.dialogs.selector
|
import io.legado.app.lib.dialogs.selector
|
||||||
import io.legado.app.lib.permission.Permissions
|
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.EmptyMessageView
|
||||||
import io.legado.app.ui.widget.components.SelectionActions
|
import io.legado.app.ui.widget.components.SelectionActions
|
||||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
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.SmallTonalIconButton
|
||||||
import io.legado.app.ui.widget.components.button.TopBarActionButton
|
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.GlassCard
|
||||||
import io.legado.app.ui.widget.components.card.TextCard
|
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.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.list.ListScaffold
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
@@ -102,8 +107,10 @@ private fun ImportBookContent(
|
|||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToLevel: (Int) -> Unit,
|
onNavigateToLevel: (Int) -> Unit,
|
||||||
onSelectAll: () -> Unit,
|
onSelectAll: () -> Unit,
|
||||||
|
onClearSelection: () -> Unit,
|
||||||
onSelectInvert: () -> Unit,
|
onSelectInvert: () -> Unit,
|
||||||
onAddToBookshelf: () -> Unit,
|
onAddToBookshelf: () -> Unit,
|
||||||
|
onItemAddToBookshelf: (ImportBook) -> Unit,
|
||||||
onDeleteSelection: () -> Unit,
|
onDeleteSelection: () -> Unit,
|
||||||
onItemClick: (ImportBook) -> Unit
|
onItemClick: (ImportBook) -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -182,6 +189,7 @@ private fun ImportBookContent(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
selectionActions = SelectionActions(
|
selectionActions = SelectionActions(
|
||||||
|
onClearSelection = onClearSelection,
|
||||||
onSelectAll = onSelectAll,
|
onSelectAll = onSelectAll,
|
||||||
onSelectInvert = onSelectInvert,
|
onSelectInvert = onSelectInvert,
|
||||||
primaryAction = ActionItem(
|
primaryAction = ActionItem(
|
||||||
@@ -227,7 +235,8 @@ private fun ImportBookContent(
|
|||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
item = item,
|
item = item,
|
||||||
isSelected = item.selectionId in state.selectedIds,
|
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()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
var showFolderPicker by remember { mutableStateOf(false) }
|
var showFolderPicker by remember { mutableStateOf(false) }
|
||||||
var showImportFileNameDialog 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) }
|
var pickerTarget by remember { mutableStateOf(ImportFolderPickTarget.IMPORT_FOLDER) }
|
||||||
val selectDocTree = rememberLauncherForActivityResult(
|
val selectDocTree = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.OpenDocumentTree()
|
contract = ActivityResultContracts.OpenDocumentTree()
|
||||||
) { uri ->
|
) { uri ->
|
||||||
viewModel.dispatch(ImportBookIntent.FolderPicked(uri, pickerTarget))
|
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) {
|
LaunchedEffect(viewModel) {
|
||||||
viewModel.dispatch(ImportBookIntent.Initialize)
|
viewModel.dispatch(ImportBookIntent.Initialize)
|
||||||
@@ -320,30 +341,48 @@ fun ImportBookScreen(
|
|||||||
},
|
},
|
||||||
confirmText = stringResource(android.R.string.ok),
|
confirmText = stringResource(android.R.string.ok),
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
AppConfig.bookImportFileName = fileNameJs
|
ImportBookConfig.bookImportFileName = fileNameJs
|
||||||
showImportFileNameDialog = false
|
showImportFileNameDialog = false
|
||||||
},
|
},
|
||||||
dismissText = stringResource(android.R.string.cancel),
|
dismissText = stringResource(android.R.string.cancel),
|
||||||
onDismiss = { showImportFileNameDialog = false }
|
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(
|
ImportBookContent(
|
||||||
state = uiState,
|
state = uiState,
|
||||||
onBackClick = onBackClick,
|
onBackClick = handleBack,
|
||||||
onSearchToggle = { viewModel.dispatch(ImportBookIntent.SearchToggle(it)) },
|
onSearchToggle = { viewModel.dispatch(ImportBookIntent.SearchToggle(it)) },
|
||||||
onSearchQueryChange = { viewModel.dispatch(ImportBookIntent.SearchQueryChange(it)) },
|
onSearchQueryChange = { viewModel.dispatch(ImportBookIntent.SearchQueryChange(it)) },
|
||||||
onSelectFolder = { showFolderPicker = true },
|
onSelectFolder = { showFolderPicker = true },
|
||||||
onScanFolder = { viewModel.dispatch(ImportBookIntent.ScanFolder) },
|
onScanFolder = { viewModel.dispatch(ImportBookIntent.ScanFolder) },
|
||||||
onImportFileName = {
|
onImportFileName = {
|
||||||
fileNameJs = AppConfig.bookImportFileName.orEmpty()
|
fileNameJs = ImportBookConfig.bookImportFileName.orEmpty()
|
||||||
showImportFileNameDialog = true
|
showImportFileNameDialog = true
|
||||||
},
|
},
|
||||||
onSortChange = { viewModel.dispatch(ImportBookIntent.SortChange(it)) },
|
onSortChange = { viewModel.dispatch(ImportBookIntent.SortChange(it)) },
|
||||||
onNavigateBack = { viewModel.dispatch(ImportBookIntent.NavigateBack) },
|
onNavigateBack = { viewModel.dispatch(ImportBookIntent.NavigateBack) },
|
||||||
onNavigateToLevel = { viewModel.dispatch(ImportBookIntent.NavigateToLevel(it)) },
|
onNavigateToLevel = { viewModel.dispatch(ImportBookIntent.NavigateToLevel(it)) },
|
||||||
onSelectAll = { viewModel.dispatch(ImportBookIntent.SelectAll) },
|
onSelectAll = { viewModel.dispatch(ImportBookIntent.SelectAll) },
|
||||||
|
onClearSelection = { viewModel.clearSelection() },
|
||||||
onSelectInvert = { viewModel.dispatch(ImportBookIntent.SelectInvert) },
|
onSelectInvert = { viewModel.dispatch(ImportBookIntent.SelectInvert) },
|
||||||
onAddToBookshelf = { viewModel.dispatch(ImportBookIntent.AddToBookshelf) },
|
onAddToBookshelf = { viewModel.dispatch(ImportBookIntent.AddToBookshelf) },
|
||||||
|
onItemAddToBookshelf = { pendingSingleAddBook = it },
|
||||||
onDeleteSelection = { viewModel.dispatch(ImportBookIntent.DeleteSelection) },
|
onDeleteSelection = { viewModel.dispatch(ImportBookIntent.DeleteSelection) },
|
||||||
onItemClick = { viewModel.dispatch(ImportBookIntent.ItemClick(it)) }
|
onItemClick = { viewModel.dispatch(ImportBookIntent.ItemClick(it)) }
|
||||||
)
|
)
|
||||||
@@ -417,7 +456,8 @@ private fun ImportBookItem(
|
|||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
item: ImportBook,
|
item: ImportBook,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit,
|
||||||
|
onAddToBookshelf: () -> Unit
|
||||||
) {
|
) {
|
||||||
val containerColor = if (isSelected) {
|
val containerColor = if (isSelected) {
|
||||||
LegadoTheme.colorScheme.secondaryContainer
|
LegadoTheme.colorScheme.secondaryContainer
|
||||||
@@ -438,7 +478,7 @@ private fun ImportBookItem(
|
|||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
AppIcon(
|
||||||
imageVector = when {
|
imageVector = when {
|
||||||
item.isDir -> Icons.Default.Folder
|
item.isDir -> Icons.Default.Folder
|
||||||
item.isOnBookShelf -> Icons.Outlined.Book
|
item.isOnBookShelf -> Icons.Outlined.Book
|
||||||
@@ -446,7 +486,7 @@ private fun ImportBookItem(
|
|||||||
},
|
},
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
tint = if (item.isDir) {
|
tint = if (item.isOnBookShelf) {
|
||||||
LegadoTheme.colorScheme.primary
|
LegadoTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
LegadoTheme.colorScheme.onSurfaceVariant
|
LegadoTheme.colorScheme.onSurfaceVariant
|
||||||
@@ -486,10 +526,9 @@ private fun ImportBookItem(
|
|||||||
|
|
||||||
if (!item.isDir && !item.isOnBookShelf) {
|
if (!item.isDir && !item.isOnBookShelf) {
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Icon(
|
SmallIconButton(
|
||||||
imageVector = if (isSelected) Icons.Default.Check else Icons.Default.ChevronRight,
|
onClick = onAddToBookshelf,
|
||||||
contentDescription = null,
|
imageVector = if (isSelected) Icons.Default.Check else Icons.Default.AddCircleOutline
|
||||||
tint = LegadoTheme.colorScheme.primary
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package io.legado.app.ui.book.import.local
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import android.provider.DocumentsContract.getTreeDocumentId
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.compose.runtime.Immutable
|
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
|
||||||
import io.legado.app.constant.AppPattern.archiveFileRegex
|
import io.legado.app.constant.AppPattern.archiveFileRegex
|
||||||
import io.legado.app.constant.AppPattern.bookFileRegex
|
import io.legado.app.constant.AppPattern.bookFileRegex
|
||||||
import io.legado.app.constant.PreferKey
|
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.Book
|
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.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.InteractionState
|
||||||
import io.legado.app.ui.widget.components.list.ListUiState
|
import io.legado.app.ui.widget.components.list.ListUiState
|
||||||
import io.legado.app.utils.AlphanumComparator
|
import io.legado.app.utils.AlphanumComparator
|
||||||
import io.legado.app.utils.ArchiveUtils
|
import io.legado.app.utils.ArchiveUtils
|
||||||
import io.legado.app.utils.FileDoc
|
import io.legado.app.utils.FileDoc
|
||||||
import io.legado.app.utils.delete
|
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.isContentScheme
|
||||||
import io.legado.app.utils.isUri
|
import io.legado.app.utils.isUri
|
||||||
import io.legado.app.utils.list
|
import io.legado.app.utils.list
|
||||||
import io.legado.app.utils.mapParallel
|
import io.legado.app.utils.mapParallel
|
||||||
import io.legado.app.utils.putPrefInt
|
|
||||||
import io.legado.app.utils.takePersistablePermissionSafely
|
import io.legado.app.utils.takePersistablePermissionSafely
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
@@ -49,6 +50,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.takeWhile
|
import kotlinx.coroutines.flow.takeWhile
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -85,6 +87,7 @@ sealed interface ImportBookIntent {
|
|||||||
data object SelectAll : ImportBookIntent
|
data object SelectAll : ImportBookIntent
|
||||||
data object SelectInvert : ImportBookIntent
|
data object SelectInvert : ImportBookIntent
|
||||||
data object AddToBookshelf : ImportBookIntent
|
data object AddToBookshelf : ImportBookIntent
|
||||||
|
data class AddSingleToBookshelf(val item: ImportBook) : ImportBookIntent
|
||||||
data object DeleteSelection : ImportBookIntent
|
data object DeleteSelection : ImportBookIntent
|
||||||
data class ItemClick(val item: ImportBook) : ImportBookIntent
|
data class ItemClick(val item: ImportBook) : ImportBookIntent
|
||||||
data class ArchiveEntrySelected(val fileDoc: FileDoc, val fileName: String) : ImportBookIntent
|
data class ArchiveEntrySelected(val fileDoc: FileDoc, val fileName: String) : ImportBookIntent
|
||||||
@@ -105,6 +108,11 @@ sealed interface ImportBookEffect {
|
|||||||
|
|
||||||
class ImportBookViewModel(application: Application) : BaseViewModel(application) {
|
class ImportBookViewModel(application: Application) : BaseViewModel(application) {
|
||||||
|
|
||||||
|
private enum class SourceMode {
|
||||||
|
CURRENT_DIR,
|
||||||
|
SCAN_RECURSIVE
|
||||||
|
}
|
||||||
|
|
||||||
private data class InternalState(
|
private data class InternalState(
|
||||||
val rootDoc: FileDoc? = null,
|
val rootDoc: FileDoc? = null,
|
||||||
val subDocs: List<FileDoc> = emptyList(),
|
val subDocs: List<FileDoc> = emptyList(),
|
||||||
@@ -112,16 +120,18 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
|
|||||||
val selectedIds: Set<String> = emptySet(),
|
val selectedIds: Set<String> = emptySet(),
|
||||||
val searchKey: String = "",
|
val searchKey: String = "",
|
||||||
val sort: Int,
|
val sort: Int,
|
||||||
|
val sourceMode: SourceMode = SourceMode.CURRENT_DIR,
|
||||||
val interaction: InteractionState = InteractionState()
|
val interaction: InteractionState = InteractionState()
|
||||||
)
|
)
|
||||||
|
|
||||||
private val _state = MutableStateFlow(
|
private val _state = MutableStateFlow(
|
||||||
InternalState(sort = context.getPrefInt(PreferKey.localBookImportSort))
|
InternalState(sort = ImportBookConfig.localBookImportSort)
|
||||||
)
|
)
|
||||||
private val _effects = MutableSharedFlow<ImportBookEffect>(extraBufferCapacity = 1)
|
private val _effects = MutableSharedFlow<ImportBookEffect>(extraBufferCapacity = 1)
|
||||||
val effects = _effects.asSharedFlow()
|
val effects = _effects.asSharedFlow()
|
||||||
|
|
||||||
private var scanDocJob: Job? = null
|
private var scanDocJob: Job? = null
|
||||||
|
private var autoSyncJob: Job? = null
|
||||||
|
|
||||||
fun dispatch(intent: ImportBookIntent) {
|
fun dispatch(intent: ImportBookIntent) {
|
||||||
when (intent) {
|
when (intent) {
|
||||||
@@ -130,7 +140,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
|
|||||||
_effects.tryEmit(
|
_effects.tryEmit(
|
||||||
ImportBookEffect.RequestFolderPicker(
|
ImportBookEffect.RequestFolderPicker(
|
||||||
target = ImportFolderPickTarget.IMPORT_FOLDER,
|
target = ImportFolderPickTarget.IMPORT_FOLDER,
|
||||||
initialUri = AppConfig.importBookPath
|
initialUri = ImportBookConfig.importBookPath
|
||||||
?.takeIf { it.isUri() }
|
?.takeIf { it.isUri() }
|
||||||
?.toUri()
|
?.toUri()
|
||||||
)
|
)
|
||||||
@@ -147,6 +157,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
|
|||||||
ImportBookIntent.SelectAll -> selectAllCheckable()
|
ImportBookIntent.SelectAll -> selectAllCheckable()
|
||||||
ImportBookIntent.SelectInvert -> invertSelection()
|
ImportBookIntent.SelectInvert -> invertSelection()
|
||||||
ImportBookIntent.AddToBookshelf -> addSelectedToBookshelf()
|
ImportBookIntent.AddToBookshelf -> addSelectedToBookshelf()
|
||||||
|
is ImportBookIntent.AddSingleToBookshelf -> addSingleToBookshelf(intent.item)
|
||||||
ImportBookIntent.DeleteSelection -> deleteSelectedDocs()
|
ImportBookIntent.DeleteSelection -> deleteSelectedDocs()
|
||||||
is ImportBookIntent.ItemClick -> onItemClick(intent.item)
|
is ImportBookIntent.ItemClick -> onItemClick(intent.item)
|
||||||
is ImportBookIntent.ArchiveEntrySelected -> onArchiveEntrySelected(
|
is ImportBookIntent.ArchiveEntrySelected -> onArchiveEntrySelected(
|
||||||
@@ -220,34 +231,66 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
|
|||||||
fun hasRootDoc(): Boolean = _state.value.rootDoc != null
|
fun hasRootDoc(): Boolean = _state.value.rootDoc != null
|
||||||
|
|
||||||
private fun initialize() {
|
private fun initialize() {
|
||||||
if (AppConfig.defaultBookTreeUri.isNullOrBlank()) {
|
val defaultPath = ImportBookConfig.defaultBookTreeUri?.takeIf { it.isUri() }
|
||||||
|
val effectiveDefaultPath = defaultPath ?: firstPersistedTreeUri()?.toString()
|
||||||
|
if (effectiveDefaultPath.isNullOrBlank()) {
|
||||||
_effects.tryEmit(
|
_effects.tryEmit(
|
||||||
ImportBookEffect.RequestFolderPicker(target = ImportFolderPickTarget.DEFAULT_BOOK)
|
ImportBookEffect.RequestFolderPicker(target = ImportFolderPickTarget.DEFAULT_BOOK)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (AppConfig.importBookPath.isNullOrBlank()) {
|
if (ImportBookConfig.defaultBookTreeUri != effectiveDefaultPath) {
|
||||||
AppConfig.importBookPath = AppConfig.defaultBookTreeUri
|
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) {
|
private fun onFolderPicked(uri: Uri?, target: ImportFolderPickTarget) {
|
||||||
uri ?: return
|
uri ?: return
|
||||||
uri.takePersistablePermissionSafely(context)
|
val pickedUri = persistFolderPermission(uri)
|
||||||
when (target) {
|
when (target) {
|
||||||
ImportFolderPickTarget.DEFAULT_BOOK -> {
|
ImportFolderPickTarget.DEFAULT_BOOK -> {
|
||||||
AppConfig.defaultBookTreeUri = uri.toString()
|
ImportBookConfig.defaultBookTreeUri = pickedUri.toString()
|
||||||
if (AppConfig.importBookPath.isNullOrBlank()) {
|
if (ImportBookConfig.importBookPath.isNullOrBlank()) {
|
||||||
AppConfig.importBookPath = AppConfig.defaultBookTreeUri
|
ImportBookConfig.importBookPath = ImportBookConfig.defaultBookTreeUri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImportFolderPickTarget.IMPORT_FOLDER -> {
|
ImportFolderPickTarget.IMPORT_FOLDER -> {
|
||||||
AppConfig.importBookPath = uri.toString()
|
ImportBookConfig.importBookPath = pickedUri.toString()
|
||||||
|
if (ImportBookConfig.defaultBookTreeUri.isNullOrBlank()) {
|
||||||
|
ImportBookConfig.defaultBookTreeUri = pickedUri.toString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initRootDoc(changedFolder = true)
|
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) {
|
private fun initRootDoc(changedFolder: Boolean) {
|
||||||
@@ -255,48 +298,111 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
|
|||||||
reloadCurrentDoc()
|
reloadCurrentDoc()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val lastPath = AppConfig.importBookPath
|
val candidates = linkedSetOf<String>().apply {
|
||||||
if (lastPath.isNullOrBlank()) {
|
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(
|
_effects.tryEmit(
|
||||||
ImportBookEffect.RequestFolderPicker(target = ImportFolderPickTarget.IMPORT_FOLDER)
|
ImportBookEffect.RequestFolderPicker(target = ImportFolderPickTarget.DEFAULT_BOOK)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val rootUri = if (lastPath.isUri()) {
|
for (path in candidates) {
|
||||||
lastPath.toUri()
|
if (trySetRootDoc(path)) {
|
||||||
} else {
|
if (ImportBookConfig.importBookPath != path) {
|
||||||
Uri.fromFile(File(lastPath))
|
ImportBookConfig.importBookPath = path
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val persistedUri = path.takeIf { it.isUri() }
|
||||||
|
?.toUri()
|
||||||
|
?.let(::findPersistedTreeUri)
|
||||||
|
if (persistedUri != null) {
|
||||||
|
val persistedPath = persistedUri.toString()
|
||||||
|
if (trySetRootDoc(persistedPath)) {
|
||||||
|
ImportBookConfig.importBookPath = persistedPath
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when {
|
firstPersistedTreeUri()?.toString()?.let { persistedPath ->
|
||||||
rootUri.isContentScheme() -> {
|
if (trySetRootDoc(persistedPath)) {
|
||||||
kotlin.runCatching {
|
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)
|
val doc = DocumentFile.fromTreeUri(context, rootUri)
|
||||||
if (doc == null || doc.name.isNullOrEmpty()) {
|
?: DocumentFile.fromSingleUri(context, rootUri)
|
||||||
_effects.tryEmit(
|
?: return false
|
||||||
ImportBookEffect.RequestFolderPicker(target = ImportFolderPickTarget.IMPORT_FOLDER)
|
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 {
|
} else {
|
||||||
setRootDoc(FileDoc.fromDocumentFile(doc))
|
val file = File(rootUri.path ?: return false)
|
||||||
}
|
if (!file.exists() || !file.isDirectory) return false
|
||||||
}.onFailure {
|
setRootDoc(FileDoc.fromFile(file))
|
||||||
_effects.tryEmit(
|
|
||||||
ImportBookEffect.RequestFolderPicker(target = ImportFolderPickTarget.IMPORT_FOLDER)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
true
|
||||||
|
}.getOrDefault(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
private fun findPersistedTreeUri(targetUri: Uri): Uri? {
|
||||||
kotlin.runCatching {
|
if (!targetUri.isContentScheme()) return null
|
||||||
setRootDoc(FileDoc.fromFile(File(rootUri.path!!)))
|
val permissions = context.contentResolver.persistedUriPermissions
|
||||||
}.onFailure {
|
if (permissions.isEmpty()) return null
|
||||||
_effects.tryEmit(
|
val targetAuthority = targetUri.authority
|
||||||
ImportBookEffect.RequestFolderPicker(target = ImportFolderPickTarget.IMPORT_FOLDER)
|
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() {
|
fun clearRoot() {
|
||||||
@@ -305,7 +411,8 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
|
|||||||
rootDoc = null,
|
rootDoc = null,
|
||||||
subDocs = emptyList(),
|
subDocs = emptyList(),
|
||||||
sourceDocs = emptyList(),
|
sourceDocs = emptyList(),
|
||||||
selectedIds = emptySet()
|
selectedIds = emptySet(),
|
||||||
|
sourceMode = SourceMode.CURRENT_DIR
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,7 +422,8 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
|
|||||||
it.copy(
|
it.copy(
|
||||||
rootDoc = rootDoc,
|
rootDoc = rootDoc,
|
||||||
subDocs = emptyList(),
|
subDocs = emptyList(),
|
||||||
selectedIds = emptySet()
|
selectedIds = emptySet(),
|
||||||
|
sourceMode = SourceMode.CURRENT_DIR
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
loadCurrentDoc()
|
loadCurrentDoc()
|
||||||
@@ -369,7 +477,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
|
|||||||
|
|
||||||
fun setSort(sort: Int) {
|
fun setSort(sort: Int) {
|
||||||
_state.update { it.copy(sort = sort) }
|
_state.update { it.copy(sort = sort) }
|
||||||
context.putPrefInt(PreferKey.localBookImportSort, sort)
|
ImportBookConfig.localBookImportSort = sort
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSearchMode(isSearch: Boolean) {
|
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) {
|
private fun onItemClick(item: ImportBook) {
|
||||||
when {
|
when {
|
||||||
item.isDir -> navigateNext(item.file)
|
item.isDir -> navigateNext(item.file)
|
||||||
@@ -506,12 +630,8 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
|
|||||||
.filter { it.toString() in selectedIds }
|
.filter { it.toString() in selectedIds }
|
||||||
.forEach { it.delete() }
|
.forEach { it.delete() }
|
||||||
}.onFinally {
|
}.onFinally {
|
||||||
_state.update { state ->
|
clearSelection()
|
||||||
state.copy(
|
refreshCurrentSource()
|
||||||
sourceDocs = state.sourceDocs.filterNot { it.toString() in selectedIds },
|
|
||||||
selectedIds = emptySet()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,6 +643,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
|
|||||||
it.copy(
|
it.copy(
|
||||||
sourceDocs = emptyList(),
|
sourceDocs = emptyList(),
|
||||||
selectedIds = emptySet(),
|
selectedIds = emptySet(),
|
||||||
|
sourceMode = SourceMode.SCAN_RECURSIVE,
|
||||||
interaction = it.interaction.copy(isLoading = true)
|
interaction = it.interaction.copy(isLoading = true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -534,6 +655,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
|
|||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
sourceDocs = docs,
|
sourceDocs = docs,
|
||||||
|
selectedIds = emptySet(),
|
||||||
interaction = it.interaction.copy(isLoading = false)
|
interaction = it.interaction.copy(isLoading = false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -562,22 +684,18 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
|
|||||||
it.copy(
|
it.copy(
|
||||||
sourceDocs = emptyList(),
|
sourceDocs = emptyList(),
|
||||||
selectedIds = emptySet(),
|
selectedIds = emptySet(),
|
||||||
|
sourceMode = SourceMode.CURRENT_DIR,
|
||||||
interaction = it.interaction.copy(isLoading = true)
|
interaction = it.interaction.copy(isLoading = true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
fileDoc.list { item ->
|
listCurrentDocDocs(fileDoc)
|
||||||
when {
|
|
||||||
item.name.startsWith(".") -> false
|
|
||||||
item.isDir -> true
|
|
||||||
else -> item.name.matches(bookFileRegex) || item.name.matches(archiveFileRegex)
|
|
||||||
}
|
|
||||||
} ?: emptyList()
|
|
||||||
}.onSuccess { docs ->
|
}.onSuccess { docs ->
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
sourceDocs = docs,
|
sourceDocs = docs,
|
||||||
|
selectedIds = emptySet(),
|
||||||
interaction = it.interaction.copy(isLoading = false)
|
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> {
|
private suspend fun scanDoc(fileDoc: FileDoc): List<FileDoc> {
|
||||||
val channel = Channel<FileDoc>(UNLIMITED)
|
val channel = Channel<FileDoc>(UNLIMITED)
|
||||||
var n = 1
|
var n = 1
|
||||||
@@ -617,6 +797,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
autoSyncJob?.cancel()
|
||||||
scanDocJob?.cancel()
|
scanDocJob?.cancel()
|
||||||
super.onCleared()
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user