|
|
@@ -1,8 +1,10 @@
|
|
|
package io.legado.app.ui.main.bookshelf
|
|
|
|
|
|
+import android.content.ClipData
|
|
|
import android.content.res.Configuration
|
|
|
import androidx.activity.compose.BackHandler
|
|
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
|
+import androidx.activity.result.contract.ActivityResultContracts
|
|
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
|
import androidx.compose.foundation.layout.Box
|
|
|
@@ -12,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|
|
import androidx.compose.foundation.layout.padding
|
|
|
import androidx.compose.foundation.lazy.grid.GridCells
|
|
|
import androidx.compose.foundation.lazy.grid.items
|
|
|
+import androidx.compose.foundation.lazy.grid.itemsIndexed
|
|
|
import androidx.compose.foundation.pager.HorizontalPager
|
|
|
import androidx.compose.foundation.pager.rememberPagerState
|
|
|
import androidx.compose.material.icons.Icons
|
|
|
@@ -32,6 +35,8 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
|
|
import androidx.compose.material3.Icon
|
|
|
import androidx.compose.material3.MaterialTheme
|
|
|
import androidx.compose.material3.PrimaryScrollableTabRow
|
|
|
+import androidx.compose.material3.SnackbarHostState
|
|
|
+import androidx.compose.material3.SnackbarResult
|
|
|
import androidx.compose.material3.Surface
|
|
|
import androidx.compose.material3.Tab
|
|
|
import androidx.compose.material3.Text
|
|
|
@@ -49,6 +54,8 @@ import androidx.compose.runtime.setValue
|
|
|
import androidx.compose.runtime.snapshotFlow
|
|
|
import androidx.compose.ui.Alignment
|
|
|
import androidx.compose.ui.Modifier
|
|
|
+import androidx.compose.ui.platform.ClipEntry
|
|
|
+import androidx.compose.ui.platform.LocalClipboard
|
|
|
import androidx.compose.ui.platform.LocalConfiguration
|
|
|
import androidx.compose.ui.platform.LocalContext
|
|
|
import androidx.compose.ui.res.stringResource
|
|
|
@@ -56,6 +63,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|
|
import androidx.compose.ui.unit.dp
|
|
|
import androidx.compose.ui.window.Dialog
|
|
|
import io.legado.app.R
|
|
|
+import io.legado.app.base.BaseRuleEvent
|
|
|
import io.legado.app.data.entities.Book
|
|
|
import io.legado.app.ui.about.AppLogSheet
|
|
|
import io.legado.app.ui.book.cache.CacheActivity
|
|
|
@@ -64,7 +72,6 @@ import io.legado.app.ui.book.import.remote.RemoteBookActivity
|
|
|
import io.legado.app.ui.book.manage.BookshelfManageActivity
|
|
|
import io.legado.app.ui.book.search.SearchActivity
|
|
|
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
|
|
-import io.legado.app.ui.file.HandleFileContract
|
|
|
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
|
|
|
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
|
|
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
|
|
|
@@ -75,6 +82,7 @@ import io.legado.app.utils.readText
|
|
|
import io.legado.app.utils.startActivity
|
|
|
import io.legado.app.utils.toastOnUi
|
|
|
import kotlinx.coroutines.delay
|
|
|
+import kotlinx.coroutines.flow.distinctUntilChanged
|
|
|
import kotlinx.coroutines.launch
|
|
|
import org.koin.androidx.compose.koinViewModel
|
|
|
|
|
|
@@ -94,38 +102,79 @@ fun BookshelfScreen(
|
|
|
|
|
|
var showAddUrlDialog by remember { mutableStateOf(false) }
|
|
|
var showImportSheet by remember { mutableStateOf(false) }
|
|
|
+ var showExportSheet by remember { mutableStateOf(false) }
|
|
|
var showConfigSheet by remember { mutableStateOf(false) }
|
|
|
var showGroupManageSheet by remember { mutableStateOf(false) }
|
|
|
var showLogSheet by remember { mutableStateOf(false) }
|
|
|
|
|
|
- val importLauncher = rememberLauncherForActivityResult(HandleFileContract()) {
|
|
|
- runCatching {
|
|
|
- it.uri?.readText(context)?.let { text ->
|
|
|
- val groupId = uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
|
|
|
- viewModel.importBookshelf(text, groupId)
|
|
|
+ val clipboardManager = LocalClipboard.current
|
|
|
+ val snackbarHostState = remember { SnackbarHostState() }
|
|
|
+
|
|
|
+ LaunchedEffect(Unit) {
|
|
|
+ viewModel.events.collect { event ->
|
|
|
+ when (event) {
|
|
|
+ is BaseRuleEvent.ShowSnackbar -> {
|
|
|
+ val result = snackbarHostState.showSnackbar(
|
|
|
+ message = event.message,
|
|
|
+ actionLabel = event.actionLabel,
|
|
|
+ withDismissAction = true
|
|
|
+ )
|
|
|
+ if (result == SnackbarResult.ActionPerformed && event.url != null) {
|
|
|
+ clipboardManager.setClipEntry(
|
|
|
+ ClipEntry(
|
|
|
+ ClipData.newPlainText(
|
|
|
+ "url",
|
|
|
+ event.url
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
- }.onFailure {
|
|
|
- context.toastOnUi(it.localizedMessage ?: "ERROR")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ val importLauncher = rememberLauncherForActivityResult(
|
|
|
+ contract = ActivityResultContracts.OpenDocument(),
|
|
|
+ onResult = { uri ->
|
|
|
+ uri?.let {
|
|
|
+ runCatching {
|
|
|
+ val text = it.readText(context)
|
|
|
+ val groupId =
|
|
|
+ uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
|
|
|
+ viewModel.importBookshelf(text, groupId)
|
|
|
+ }.onFailure {
|
|
|
+ context.toastOnUi(it.localizedMessage ?: "ERROR")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ val exportLauncher = rememberLauncherForActivityResult(
|
|
|
+ contract = ActivityResultContracts.CreateDocument("application/json"),
|
|
|
+ onResult = { uri ->
|
|
|
+ uri?.let { viewModel.exportToUri(it, uiState.items) }
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
val pagerState = rememberPagerState(
|
|
|
initialPage = uiState.selectedGroupIndex,
|
|
|
pageCount = { uiState.groups.size }
|
|
|
)
|
|
|
|
|
|
LaunchedEffect(pagerState) {
|
|
|
- snapshotFlow { pagerState.currentPage }.collect { page ->
|
|
|
- if (uiState.groups.isNotEmpty() && page < uiState.groups.size) {
|
|
|
- viewModel.changeGroup(uiState.groups[page].groupId)
|
|
|
+ snapshotFlow { pagerState.currentPage }
|
|
|
+ .distinctUntilChanged()
|
|
|
+ .collect { page ->
|
|
|
+ if (uiState.groups.isNotEmpty() && page in uiState.groups.indices) {
|
|
|
+ val targetGroupId = uiState.groups[page].groupId
|
|
|
+ val currentGroupId =
|
|
|
+ uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId
|
|
|
+ if (currentGroupId != targetGroupId) {
|
|
|
+ viewModel.changeGroup(targetGroupId)
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- LaunchedEffect(uiState.selectedGroupIndex) {
|
|
|
- if (uiState.selectedGroupIndex != pagerState.currentPage && uiState.selectedGroupIndex < pagerState.pageCount) {
|
|
|
- pagerState.scrollToPage(uiState.selectedGroupIndex)
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
val bookGroupStyle = BookshelfConfig.bookGroupStyle
|
|
|
@@ -223,6 +272,7 @@ fun BookshelfScreen(
|
|
|
RoundDropdownMenuItem(
|
|
|
text = { Text(stringResource(R.string.export_bookshelf)) },
|
|
|
onClick = {
|
|
|
+ showExportSheet = true
|
|
|
dismiss()
|
|
|
},
|
|
|
leadingIcon = { Icon(Icons.Default.ImportExport, null) }
|
|
|
@@ -241,12 +291,12 @@ fun BookshelfScreen(
|
|
|
leadingIcon = { Icon(Icons.Default.History, null) }
|
|
|
)
|
|
|
},
|
|
|
+ snackbarHostState = snackbarHostState,
|
|
|
bottomContent = if (bookGroupStyle == 0) {
|
|
|
{
|
|
|
if (uiState.groups.isNotEmpty()) {
|
|
|
- val selectedTabIndex = remember(pagerState.currentPage, uiState.groups.size) {
|
|
|
+ val selectedTabIndex =
|
|
|
pagerState.currentPage.coerceIn(0, uiState.groups.size - 1)
|
|
|
- }
|
|
|
PrimaryScrollableTabRow(
|
|
|
selectedTabIndex = selectedTabIndex,
|
|
|
edgePadding = 0.dp,
|
|
|
@@ -312,7 +362,7 @@ fun BookshelfScreen(
|
|
|
horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
|
|
|
showFastScroll = BookshelfConfig.showBookshelfFastScroller
|
|
|
) {
|
|
|
- items(uiState.groups) { group ->
|
|
|
+ itemsIndexed(uiState.groups, key = { _, it -> it.groupId }) { index, group ->
|
|
|
if (bookshelfLayoutMode == 0) {
|
|
|
BookGroupItemList(
|
|
|
group = group,
|
|
|
@@ -322,11 +372,8 @@ fun BookshelfScreen(
|
|
|
titleCenter = BookshelfConfig.bookshelfTitleCenter,
|
|
|
titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
|
|
|
onClick = {
|
|
|
- val index = uiState.groups.indexOf(group)
|
|
|
- if (index != -1) {
|
|
|
- scope.launch { pagerState.scrollToPage(index) }
|
|
|
- isInFolderRoot = false
|
|
|
- }
|
|
|
+ scope.launch { pagerState.scrollToPage(index) }
|
|
|
+ isInFolderRoot = false
|
|
|
},
|
|
|
onLongClick = { showGroupManageSheet = true }
|
|
|
)
|
|
|
@@ -340,11 +387,8 @@ fun BookshelfScreen(
|
|
|
titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
|
|
|
coverShadow = BookshelfConfig.bookshelfCoverShadow,
|
|
|
onClick = {
|
|
|
- val index = uiState.groups.indexOf(group)
|
|
|
- if (index != -1) {
|
|
|
- scope.launch { pagerState.scrollToPage(index) }
|
|
|
- isInFolderRoot = false
|
|
|
- }
|
|
|
+ scope.launch { pagerState.scrollToPage(index) }
|
|
|
+ isInFolderRoot = false
|
|
|
},
|
|
|
onLongClick = { showGroupManageSheet = true }
|
|
|
)
|
|
|
@@ -360,8 +404,10 @@ fun BookshelfScreen(
|
|
|
) { pageIndex ->
|
|
|
val group = uiState.groups.getOrNull(pageIndex)
|
|
|
if (group != null) {
|
|
|
- val books by viewModel.getBooksFlow(group.groupId)
|
|
|
- .collectAsState(emptyList())
|
|
|
+ val booksFlow = remember(group.groupId) {
|
|
|
+ viewModel.getBooksFlow(group.groupId)
|
|
|
+ }
|
|
|
+ val books by booksFlow.collectAsState(emptyList())
|
|
|
BookshelfPage(
|
|
|
paddingValues = paddingValues,
|
|
|
books = books,
|
|
|
@@ -409,15 +455,28 @@ fun BookshelfScreen(
|
|
|
onDismissRequest = { showImportSheet = false },
|
|
|
title = stringResource(R.string.import_bookshelf),
|
|
|
onSelectSysFile = { types ->
|
|
|
- importLauncher.launch {
|
|
|
- mode = HandleFileContract.FILE
|
|
|
- allowExtensions = types
|
|
|
- }
|
|
|
+ importLauncher.launch(types)
|
|
|
showImportSheet = false
|
|
|
},
|
|
|
onManualInput = {
|
|
|
showAddUrlDialog = true
|
|
|
showImportSheet = false
|
|
|
+ },
|
|
|
+ allowExtensions = arrayOf("json", "txt")
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ if (showExportSheet) {
|
|
|
+ FilePickerSheet(
|
|
|
+ onDismissRequest = { showExportSheet = false },
|
|
|
+ title = stringResource(R.string.export_bookshelf),
|
|
|
+ onSelectSysDir = {
|
|
|
+ showExportSheet = false
|
|
|
+ exportLauncher.launch("bookshelf.json")
|
|
|
+ },
|
|
|
+ onUpload = {
|
|
|
+ showExportSheet = false
|
|
|
+ viewModel.uploadBookshelf(uiState.items)
|
|
|
}
|
|
|
)
|
|
|
}
|