|
|
@@ -27,6 +27,7 @@ import androidx.compose.animation.scaleOut
|
|
|
import androidx.compose.animation.shrinkVertically
|
|
|
import androidx.compose.animation.togetherWith
|
|
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
|
+import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
|
import androidx.compose.foundation.layout.Box
|
|
|
import androidx.compose.foundation.layout.Column
|
|
|
@@ -39,16 +40,19 @@ import androidx.compose.foundation.layout.WindowInsets
|
|
|
import androidx.compose.foundation.layout.asPaddingValues
|
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
|
+import androidx.compose.foundation.layout.height
|
|
|
import androidx.compose.foundation.layout.navigationBars
|
|
|
import androidx.compose.foundation.layout.padding
|
|
|
import androidx.compose.foundation.layout.size
|
|
|
import androidx.compose.foundation.layout.width
|
|
|
import androidx.compose.foundation.lazy.grid.GridCells
|
|
|
+import androidx.compose.foundation.lazy.grid.LazyGridState
|
|
|
import androidx.compose.foundation.lazy.grid.items
|
|
|
-import androidx.compose.foundation.lazy.grid.itemsIndexed
|
|
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
|
|
import androidx.compose.foundation.pager.HorizontalPager
|
|
|
import androidx.compose.foundation.pager.rememberPagerState
|
|
|
+import androidx.compose.foundation.rememberScrollState
|
|
|
+import androidx.compose.foundation.verticalScroll
|
|
|
import androidx.compose.material.icons.Icons
|
|
|
import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
|
|
|
import androidx.compose.material.icons.filled.Bookmarks
|
|
|
@@ -84,8 +88,10 @@ import androidx.compose.runtime.setValue
|
|
|
import androidx.compose.runtime.snapshotFlow
|
|
|
import androidx.compose.ui.Alignment
|
|
|
import androidx.compose.ui.Modifier
|
|
|
+import androidx.compose.ui.draw.alpha
|
|
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
|
+import androidx.compose.ui.input.pointer.pointerInput
|
|
|
import androidx.compose.ui.platform.ClipEntry
|
|
|
import androidx.compose.ui.platform.LocalClipboard
|
|
|
import androidx.compose.ui.platform.LocalConfiguration
|
|
|
@@ -144,1271 +150,4 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|
|
import kotlinx.coroutines.launch
|
|
|
import org.koin.androidx.compose.koinViewModel
|
|
|
import sh.calvin.reorderable.ReorderableItem
|
|
|
-import sh.calvin.reorderable.rememberReorderableLazyGridState
|
|
|
-
|
|
|
-@OptIn(
|
|
|
- ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
|
|
|
- ExperimentalMaterial3ExpressiveApi::class, ExperimentalSharedTransitionApi::class,
|
|
|
- ExperimentalAnimationApi::class
|
|
|
-)
|
|
|
-@Composable
|
|
|
-fun BookshelfScreen(
|
|
|
- viewModel: BookshelfViewModel = koinViewModel(),
|
|
|
- onBookClick: (BookShelfItem) -> Unit,
|
|
|
- onBookLongClick: (book: BookShelfItem, sharedCoverKey: String?) -> Unit,
|
|
|
- onNavigateToSearch: (String) -> Unit,
|
|
|
- onNavigateToRemoteImport: () -> Unit,
|
|
|
- onNavigateToLocalImport: () -> Unit,
|
|
|
- onNavigateToCache: (Long) -> Unit,
|
|
|
- sharedTransitionScope: SharedTransitionScope? = null,
|
|
|
- animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
|
|
-) {
|
|
|
- val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
|
|
- val scope = rememberCoroutineScope()
|
|
|
-
|
|
|
- ReportDrawnWhen { !uiState.isInitialLoading }
|
|
|
-
|
|
|
- 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
|
|
|
- )
|
|
|
- )
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- val importLauncher = rememberLauncherForActivityResult(
|
|
|
- contract = ActivityResultContracts.OpenDocument(),
|
|
|
- onResult = { uri ->
|
|
|
- uri?.let {
|
|
|
- val groupId = uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
|
|
|
- viewModel.importBookshelf(it, groupId)
|
|
|
- }
|
|
|
- }
|
|
|
- )
|
|
|
-
|
|
|
- val exportLauncher = rememberLauncherForActivityResult(
|
|
|
- contract = ActivityResultContracts.CreateDocument("application/json"),
|
|
|
- onResult = { uri ->
|
|
|
- uri?.let { viewModel.exportToUri(it, uiState.items) }
|
|
|
- }
|
|
|
- )
|
|
|
-
|
|
|
- val activeOverlay = uiState.activeOverlay
|
|
|
- val showGroupMenu = activeOverlay == BookshelfOverlay.GroupMenu
|
|
|
- val isEditMode = uiState.isEditMode
|
|
|
- val selectedBookUrls = uiState.selectedBookUrls
|
|
|
- val isInFolderRoot = uiState.isInFolderRoot
|
|
|
- val bookGroupStyle = uiState.bookGroupStyle
|
|
|
-
|
|
|
- val transitionState = remember { SeekableTransitionState(isInFolderRoot) }
|
|
|
- val folderTransition = rememberTransition(transitionState, label = "FolderTransition")
|
|
|
- LaunchedEffect(isInFolderRoot) {
|
|
|
- if (transitionState.targetState != isInFolderRoot) {
|
|
|
- transitionState.animateTo(isInFolderRoot)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- val pagerState = rememberPagerState(
|
|
|
- initialPage = uiState.selectedGroupIndex.coerceAtLeast(0),
|
|
|
- pageCount = { uiState.groups.size }
|
|
|
- )
|
|
|
- val latestGroups by rememberUpdatedState(uiState.groups)
|
|
|
- val latestSelectedGroupId by rememberUpdatedState(uiState.selectedGroupId)
|
|
|
-
|
|
|
- LaunchedEffect(uiState.selectedGroupIndex, uiState.groups.size) {
|
|
|
- if (uiState.groups.isNotEmpty()
|
|
|
- && uiState.selectedGroupIndex in uiState.groups.indices
|
|
|
- && pagerState.currentPage != uiState.selectedGroupIndex
|
|
|
- ) {
|
|
|
- pagerState.scrollToPage(uiState.selectedGroupIndex)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- LaunchedEffect(pagerState) {
|
|
|
- snapshotFlow { pagerState.settledPage }
|
|
|
- .distinctUntilChanged()
|
|
|
- .collect { page ->
|
|
|
- val groups = latestGroups
|
|
|
- if (groups.isNotEmpty() && page in groups.indices) {
|
|
|
- val targetGroupId = groups[page].groupId
|
|
|
- if (latestSelectedGroupId != targetGroupId) {
|
|
|
- viewModel.changeGroup(targetGroupId)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- val currentTabGroupId by remember {
|
|
|
- derivedStateOf {
|
|
|
- uiState.groups.getOrNull(pagerState.settledPage)?.groupId ?: BookGroup.IdAll
|
|
|
- }
|
|
|
- }
|
|
|
- val searchGroupExists by remember {
|
|
|
- derivedStateOf { uiState.allGroups.any { it.groupId == uiState.selectedGroupId } }
|
|
|
- }
|
|
|
- val currentGroupId by remember {
|
|
|
- derivedStateOf {
|
|
|
- if (uiState.isSearch && searchGroupExists) {
|
|
|
- uiState.selectedGroupId
|
|
|
- } else {
|
|
|
- currentTabGroupId
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- val isUsingStandaloneSearchGroup by remember {
|
|
|
- derivedStateOf {
|
|
|
- uiState.isSearch && uiState.groups.none { it.groupId == currentGroupId }
|
|
|
- }
|
|
|
- }
|
|
|
- val currentGroupBookCount by remember { derivedStateOf { uiState.currentGroupBookCount } }
|
|
|
-
|
|
|
- val clearSelection = {
|
|
|
- viewModel.clearSelection()
|
|
|
- }
|
|
|
- val exitEditMode = {
|
|
|
- viewModel.exitEditMode()
|
|
|
- }
|
|
|
- val toggleEditMode = {
|
|
|
- viewModel.toggleEditMode()
|
|
|
- }
|
|
|
- val toggleBookSelection: (String) -> Unit = { bookUrl ->
|
|
|
- viewModel.toggleBookSelection(bookUrl)
|
|
|
- }
|
|
|
-
|
|
|
- LaunchedEffect(pagerState.currentPage, isInFolderRoot) {
|
|
|
- clearSelection()
|
|
|
- }
|
|
|
-
|
|
|
- BackHandler(enabled = isEditMode) {
|
|
|
- if (selectedBookUrls.isNotEmpty()) {
|
|
|
- clearSelection()
|
|
|
- } else {
|
|
|
- exitEditMode()
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- val currentGroupName by remember { derivedStateOf { uiState.currentGroupName } }
|
|
|
-
|
|
|
- PredictiveBackHandler(enabled = bookGroupStyle == 2 && !isInFolderRoot && !isEditMode) { progress ->
|
|
|
- try {
|
|
|
- progress.collect { backEvent ->
|
|
|
- transitionState.seekTo(backEvent.progress, targetState = true)
|
|
|
- }
|
|
|
- viewModel.setInFolderRoot(true)
|
|
|
- transitionState.animateTo(true)
|
|
|
- } catch (e: CancellationException) {
|
|
|
- transitionState.animateTo(false)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- val configuration = LocalConfiguration.current
|
|
|
- val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
|
|
- val bookshelfFolderLayoutMode by remember(isLandscape) {
|
|
|
- derivedStateOf {
|
|
|
- if (isLandscape) BookshelfConfig.bookshelfFolderLayoutModeLandscapeState.value
|
|
|
- else BookshelfConfig.bookshelfFolderLayoutModePortraitState.value
|
|
|
- }
|
|
|
- }
|
|
|
- val bookshelfFolderLayoutGrid by remember(isLandscape) {
|
|
|
- derivedStateOf {
|
|
|
- if (isLandscape) BookshelfConfig.bookshelfFolderLayoutGridLandscapeState.value
|
|
|
- else BookshelfConfig.bookshelfFolderLayoutGridPortraitState.value
|
|
|
- }
|
|
|
- }
|
|
|
- val bookshelfFolderLayoutList by remember(isLandscape) {
|
|
|
- derivedStateOf {
|
|
|
- if (isLandscape) BookshelfConfig.bookshelfFolderLayoutListLandscapeState.value
|
|
|
- else BookshelfConfig.bookshelfFolderLayoutListPortraitState.value
|
|
|
- }
|
|
|
- }
|
|
|
- val currentMenuGroupId by remember {
|
|
|
- derivedStateOf { if (uiState.isSearch) uiState.selectedGroupId else currentTabGroupId }
|
|
|
- }
|
|
|
- val editStickySummary by remember {
|
|
|
- derivedStateOf {
|
|
|
- if (uiState.isEditMode) {
|
|
|
- BookshelfEditStickySummary(
|
|
|
- selectedCount = uiState.selectedBookUrls.size,
|
|
|
- currentGroupTotalCount = currentGroupBookCount,
|
|
|
- groupName = currentGroupName,
|
|
|
- showGroupName = uiState.bookGroupStyle != 0
|
|
|
- )
|
|
|
- } else {
|
|
|
- null
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
|
|
- var showTopBarMenu by remember { mutableStateOf(false) }
|
|
|
- val onSearchClick = {
|
|
|
- if (BookshelfConfig.bookshelfSearchActionDirectToSearchState.value) {
|
|
|
- onNavigateToSearch(uiState.searchKey.trim())
|
|
|
- } else {
|
|
|
- val active = !uiState.isSearch
|
|
|
- viewModel.setSearchMode(active)
|
|
|
- if (!active && uiState.selectedGroupId != currentTabGroupId) {
|
|
|
- viewModel.changeGroup(currentTabGroupId)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- AppScaffold(
|
|
|
- modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
|
|
- contentWindowInsets = WindowInsets(0),
|
|
|
- snackbarHost = {
|
|
|
- SnackbarHost(
|
|
|
- hostState = snackbarHostState,
|
|
|
- modifier = Modifier
|
|
|
- .padding(
|
|
|
- bottom = 72.dp + WindowInsets.navigationBars
|
|
|
- .asPaddingValues()
|
|
|
- .calculateBottomPadding()
|
|
|
- )
|
|
|
- )
|
|
|
- },
|
|
|
- topBar = {
|
|
|
- BookshelfTopBar(
|
|
|
- uiState = uiState,
|
|
|
- scrollBehavior = scrollBehavior,
|
|
|
- onSearchClick = onSearchClick,
|
|
|
- onSearchQueryChange = { viewModel.setSearchKey(it) },
|
|
|
- onSearchSubmit = { rawQuery ->
|
|
|
- rawQuery.trim()
|
|
|
- .takeIf { it.isNotEmpty() }
|
|
|
- ?.let(onNavigateToSearch)
|
|
|
- },
|
|
|
- onClearSearch = { viewModel.setSearchKey("") },
|
|
|
- actions = {
|
|
|
- AnimatedVisibility(visible = isEditMode) {
|
|
|
- TopBarActionButton(
|
|
|
- onClick = { viewModel.selectAllVisible() },
|
|
|
- imageVector = Icons.Default.SelectAll,
|
|
|
- contentDescription = stringResource(R.string.select_all)
|
|
|
- )
|
|
|
- }
|
|
|
- AnimatedVisibility(visible = isEditMode) {
|
|
|
- TopBarActionButton(
|
|
|
- onClick = { viewModel.invertVisibleSelection() },
|
|
|
- imageVector = Icons.Default.Refresh,
|
|
|
- contentDescription = stringResource(R.string.revert_selection)
|
|
|
- )
|
|
|
- }
|
|
|
- AnimatedVisibility(visible = isEditMode) {
|
|
|
- TopBarActionButton(
|
|
|
- onClick = {
|
|
|
- if (selectedBookUrls.isNotEmpty()) {
|
|
|
- viewModel.showOverlay(BookshelfOverlay.BatchDownloadConfirmDialog)
|
|
|
- }
|
|
|
- },
|
|
|
- imageVector = Icons.Default.Download,
|
|
|
- contentDescription = stringResource(R.string.action_download)
|
|
|
- )
|
|
|
- }
|
|
|
- AnimatedVisibility(visible = isEditMode) {
|
|
|
- TopBarActionButton(
|
|
|
- onClick = {
|
|
|
- if (selectedBookUrls.isNotEmpty()) {
|
|
|
- viewModel.showOverlay(BookshelfOverlay.GroupSelectSheet)
|
|
|
- }
|
|
|
- },
|
|
|
- imageVector = Icons.Default.Bookmarks,
|
|
|
- contentDescription = stringResource(R.string.move_to_group)
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
- if (!isEditMode) {
|
|
|
- Box {
|
|
|
- TopBarActionButton(
|
|
|
- onClick = { showTopBarMenu = true },
|
|
|
- imageVector = Icons.Default.MoreVert,
|
|
|
- contentDescription = stringResource(R.string.more_menu)
|
|
|
- )
|
|
|
- RoundDropdownMenu(
|
|
|
- expanded = showTopBarMenu,
|
|
|
- onDismissRequest = { showTopBarMenu = false }
|
|
|
- ) { dismiss ->
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.add_remote_book),
|
|
|
- onClick = { onNavigateToRemoteImport(); dismiss() },
|
|
|
- leadingIcon = { Icon(Icons.Default.Wifi, null) }
|
|
|
- )
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.book_local),
|
|
|
- onClick = { onNavigateToLocalImport(); dismiss() },
|
|
|
- leadingIcon = { Icon(Icons.Default.Save, null) }
|
|
|
- )
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.update_toc),
|
|
|
- onClick = { viewModel.upToc(uiState.items); dismiss() },
|
|
|
- leadingIcon = { Icon(Icons.Default.Refresh, null) }
|
|
|
- )
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.layout_setting),
|
|
|
- onClick = {
|
|
|
- viewModel.showOverlay(BookshelfOverlay.ConfigSheet)
|
|
|
- dismiss()
|
|
|
- },
|
|
|
- leadingIcon = { Icon(Icons.Default.GridView, null) }
|
|
|
- )
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.group_manage),
|
|
|
- onClick = {
|
|
|
- viewModel.showOverlay(BookshelfOverlay.GroupManageSheet)
|
|
|
- dismiss()
|
|
|
- },
|
|
|
- leadingIcon = { Icon(Icons.Outlined.ViewCarousel, null) }
|
|
|
- )
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.add_url),
|
|
|
- onClick = {
|
|
|
- viewModel.showOverlay(BookshelfOverlay.AddUrlDialog)
|
|
|
- dismiss()
|
|
|
- },
|
|
|
- leadingIcon = { Icon(Icons.Default.Link, null) }
|
|
|
- )
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = "选择模式",
|
|
|
- onClick = {
|
|
|
- toggleEditMode()
|
|
|
- dismiss()
|
|
|
- },
|
|
|
- leadingIcon = { Icon(Icons.Default.Edit, null) }
|
|
|
- )
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.bookshelf_management),
|
|
|
- onClick = {
|
|
|
- val groupId =
|
|
|
- uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId
|
|
|
- ?: -1L
|
|
|
- onNavigateToCache(groupId)
|
|
|
- dismiss()
|
|
|
- },
|
|
|
- leadingIcon = { Icon(Icons.Default.Bookmarks, null) }
|
|
|
- )
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.export_bookshelf),
|
|
|
- onClick = {
|
|
|
- viewModel.showOverlay(BookshelfOverlay.ExportSheet)
|
|
|
- dismiss()
|
|
|
- },
|
|
|
- leadingIcon = { Icon(Icons.Default.UploadFile, null) }
|
|
|
- )
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.import_bookshelf),
|
|
|
- onClick = {
|
|
|
- viewModel.showOverlay(BookshelfOverlay.ImportSheet)
|
|
|
- dismiss()
|
|
|
- },
|
|
|
- leadingIcon = { Icon(Icons.Default.CloudDownload, null) }
|
|
|
- )
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.log),
|
|
|
- onClick = {
|
|
|
- viewModel.showOverlay(BookshelfOverlay.LogSheet)
|
|
|
- dismiss()
|
|
|
- },
|
|
|
- leadingIcon = { Icon(Icons.Default.History, null) }
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- bottomContent = {
|
|
|
- if (bookGroupStyle == 0 && uiState.groups.isNotEmpty()) {
|
|
|
- Row(
|
|
|
- modifier = Modifier
|
|
|
- .fillMaxWidth()
|
|
|
- .adaptiveHorizontalPaddingTab(),
|
|
|
- verticalAlignment = Alignment.CenterVertically
|
|
|
- ) {
|
|
|
- val selectedTabIndex =
|
|
|
- pagerState.currentPage.coerceIn(0, uiState.groups.size - 1)
|
|
|
- val tabTitles = remember(uiState.groups) {
|
|
|
- uiState.groups.map { it.groupName }
|
|
|
- }
|
|
|
-
|
|
|
- AppTabRow(
|
|
|
- tabTitles = tabTitles,
|
|
|
- selectedTabIndex = selectedTabIndex,
|
|
|
- onTabSelected = { index ->
|
|
|
- scope.launch { pagerState.animateScrollToPage(index) }
|
|
|
- },
|
|
|
- modifier = Modifier.weight(1f)
|
|
|
- )
|
|
|
-
|
|
|
- val showExpandButton by BookshelfConfig.shouldShowExpandButtonState
|
|
|
- if (showExpandButton) {
|
|
|
- Box(modifier = Modifier) {
|
|
|
- SmallToggleButton(
|
|
|
- checked = showGroupMenu,
|
|
|
- onCheckedChange = {
|
|
|
- if (it) {
|
|
|
- viewModel.showOverlay(BookshelfOverlay.GroupMenu)
|
|
|
- } else {
|
|
|
- viewModel.dismissOverlay()
|
|
|
- }
|
|
|
- },
|
|
|
- style = ToggleStyle.Outlined,
|
|
|
- icon = Icons.AutoMirrored.Filled.FormatListBulleted,
|
|
|
- contentDescription = stringResource(R.string.group_manage)
|
|
|
- )
|
|
|
- RoundDropdownMenu(
|
|
|
- expanded = showGroupMenu,
|
|
|
- onDismissRequest = { viewModel.dismissOverlay() }
|
|
|
- ) { dismiss ->
|
|
|
- uiState.groups.forEachIndexed { index, group ->
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = group.groupName,
|
|
|
- onClick = {
|
|
|
- if (uiState.isSearch) {
|
|
|
- viewModel.changeGroup(group.groupId)
|
|
|
- }
|
|
|
- scope.launch {
|
|
|
- pagerState.animateScrollToPage(
|
|
|
- index
|
|
|
- )
|
|
|
- }
|
|
|
- dismiss()
|
|
|
- },
|
|
|
- trailingIcon = {
|
|
|
- val isSelected = if (uiState.isSearch) {
|
|
|
- uiState.selectedGroupId == group.groupId
|
|
|
- } else {
|
|
|
- selectedTabIndex == index
|
|
|
- }
|
|
|
- if (isSelected) {
|
|
|
- Icon(
|
|
|
- Icons.Default.Check,
|
|
|
- null,
|
|
|
- modifier = Modifier.size(18.dp)
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
- if (uiState.isSearch) {
|
|
|
- val allGroup = uiState.allGroups.firstOrNull {
|
|
|
- it.groupId == BookGroup.IdAll
|
|
|
- }
|
|
|
- val hiddenGroups = uiState.allGroups.filter {
|
|
|
- !it.show && it.groupId != BookGroup.IdAll
|
|
|
- }
|
|
|
-
|
|
|
- if (allGroup != null || hiddenGroups.isNotEmpty()) {
|
|
|
- PillHeaderDivider(
|
|
|
- title = "${stringResource(R.string.all)} / ${
|
|
|
- stringResource(
|
|
|
- R.string.hide
|
|
|
- )
|
|
|
- }"
|
|
|
- )
|
|
|
-
|
|
|
- allGroup?.let { group ->
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = group.groupName,
|
|
|
- onClick = {
|
|
|
- viewModel.changeGroup(group.groupId)
|
|
|
- dismiss()
|
|
|
- },
|
|
|
- trailingIcon = {
|
|
|
- if (uiState.selectedGroupId == group.groupId) {
|
|
|
- Icon(
|
|
|
- Icons.Default.Check,
|
|
|
- null,
|
|
|
- modifier = Modifier.size(18.dp)
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
- hiddenGroups.forEach { group ->
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = group.groupName,
|
|
|
- onClick = {
|
|
|
- viewModel.changeGroup(group.groupId)
|
|
|
- dismiss()
|
|
|
- },
|
|
|
- trailingIcon = {
|
|
|
- if (uiState.selectedGroupId == group.groupId) {
|
|
|
- Icon(
|
|
|
- Icons.Default.Check,
|
|
|
- null,
|
|
|
- modifier = Modifier.size(18.dp)
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- )
|
|
|
- }
|
|
|
- ) { paddingValues ->
|
|
|
- val currentGroup by remember {
|
|
|
- derivedStateOf {
|
|
|
- if (uiState.isSearch) {
|
|
|
- uiState.allGroups.firstOrNull { it.groupId == currentGroupId }
|
|
|
- } else {
|
|
|
- uiState.groups.getOrNull(pagerState.settledPage)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- val pullToRefreshEnabled by remember {
|
|
|
- derivedStateOf { (currentGroup?.enableRefresh ?: true) && !isEditMode }
|
|
|
- }
|
|
|
-
|
|
|
- Box(Modifier.fillMaxSize()) {
|
|
|
- AppPullToRefresh(
|
|
|
- isRefreshing = uiState.isRefreshing,
|
|
|
- onRefresh = { viewModel.refreshBooks(uiState.items) },
|
|
|
- enabled = pullToRefreshEnabled,
|
|
|
- topPadding = paddingValues.calculateTopPadding()
|
|
|
- ) {
|
|
|
- folderTransition.AnimatedContent(
|
|
|
- transitionSpec = {
|
|
|
- val easing = FastOutSlowInEasing
|
|
|
- val duration = 480
|
|
|
- if (targetState) {
|
|
|
- (fadeIn(animationSpec = tween(duration, easing = easing)) +
|
|
|
- scaleIn(
|
|
|
- initialScale = 1.2f,
|
|
|
- animationSpec = tween(duration, easing = easing)
|
|
|
- ))
|
|
|
- .togetherWith(fadeOut(animationSpec = tween(duration, easing = easing)) +
|
|
|
- scaleOut(
|
|
|
- targetScale = 0.8f,
|
|
|
- animationSpec = tween(duration, easing = easing)
|
|
|
- )
|
|
|
- )
|
|
|
- } else {
|
|
|
- (fadeIn(animationSpec = tween(duration, easing = easing)) +
|
|
|
- scaleIn(
|
|
|
- initialScale = 0.8f,
|
|
|
- animationSpec = tween(duration, easing = easing)
|
|
|
- ))
|
|
|
- .togetherWith(fadeOut(animationSpec = tween(duration, easing = easing)) +
|
|
|
- scaleOut(
|
|
|
- targetScale = 1.2f,
|
|
|
- animationSpec = tween(duration, easing = easing)
|
|
|
- )
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- ) { isRoot ->
|
|
|
- if (uiState.groups.isEmpty() && !uiState.isSearch) {
|
|
|
- if (!uiState.isInitialLoading) {
|
|
|
- EmptyMessage(
|
|
|
- modifier = Modifier
|
|
|
- .fillMaxSize()
|
|
|
- .padding(
|
|
|
- top = paddingValues.calculateTopPadding(),
|
|
|
- bottom = 120.dp
|
|
|
- ),
|
|
|
- messageResId = R.string.bookshelf_empty
|
|
|
- )
|
|
|
- }
|
|
|
- } else if (bookGroupStyle == 2 && isRoot && !isUsingStandaloneSearchGroup) {
|
|
|
- val folderColumns =
|
|
|
- if (bookshelfFolderLayoutMode == 0) bookshelfFolderLayoutList else bookshelfFolderLayoutGrid
|
|
|
- val isGridMode = bookshelfFolderLayoutMode != 0
|
|
|
- FastScrollLazyVerticalGrid(
|
|
|
- columns = GridCells.Fixed(folderColumns.coerceAtLeast(1)),
|
|
|
- modifier = Modifier
|
|
|
- .fillMaxSize()
|
|
|
- .then(
|
|
|
- with(sharedTransitionScope) {
|
|
|
- if (this != null) Modifier.skipToLookaheadSize() else Modifier
|
|
|
- }
|
|
|
- ),
|
|
|
- contentPadding = adaptiveContentPaddingBookshelf(
|
|
|
- top = paddingValues.calculateTopPadding(),
|
|
|
- bottom = if (ThemeConfig.useFloatingBottomBar || ThemeConfig.enableBlur) 120.dp else 8.dp,
|
|
|
- horizontal = 4.dp
|
|
|
- ),
|
|
|
- verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
|
|
|
- horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
|
|
|
- showFastScroll = BookshelfConfig.showBookshelfFastScrollerState.value
|
|
|
- ) {
|
|
|
- itemsIndexed(
|
|
|
- uiState.groups,
|
|
|
- key = { _, it -> it.groupId }) { index, group ->
|
|
|
- val countText = if (BookshelfConfig.showBookCountState.value) {
|
|
|
- uiState.groupBookCounts[group.groupId]?.let {
|
|
|
- stringResource(R.string.book_count, it)
|
|
|
- }
|
|
|
- } else {
|
|
|
- null
|
|
|
- }
|
|
|
- if (bookshelfFolderLayoutMode == 0) {
|
|
|
- BookGroupItemList(
|
|
|
- group = group,
|
|
|
- previewBooks = uiState.groupPreviews[group.groupId]
|
|
|
- ?: emptyList(),
|
|
|
- countText = countText,
|
|
|
- isCompact = BookshelfConfig.bookshelfLayoutCompactState.value,
|
|
|
- titleSmallFont = BookshelfConfig.bookshelfTitleSmallFontState.value,
|
|
|
- titleCenter = BookshelfConfig.bookshelfTitleCenterState.value,
|
|
|
- titleMaxLines = BookshelfConfig.bookshelfTitleMaxLinesState.value,
|
|
|
- coverShadow = BookshelfConfig.bookshelfCoverShadowState.value,
|
|
|
- onClick = {
|
|
|
- scope.launch { pagerState.scrollToPage(index) }
|
|
|
- viewModel.setInFolderRoot(false)
|
|
|
- },
|
|
|
- onLongClick = {
|
|
|
- viewModel.showOverlay(BookshelfOverlay.GroupEditSheet(group.groupId))
|
|
|
- },
|
|
|
- onBookClick = onBookClick
|
|
|
- )
|
|
|
- } else {
|
|
|
- BookGroupItemGrid(
|
|
|
- group = group,
|
|
|
- previewBooks = uiState.groupPreviews[group.groupId]
|
|
|
- ?: emptyList(),
|
|
|
- countText = countText,
|
|
|
- gridStyle = BookshelfConfig.bookshelfGridLayoutState.value,
|
|
|
- titleSmallFont = BookshelfConfig.bookshelfTitleSmallFontState.value,
|
|
|
- titleCenter = BookshelfConfig.bookshelfTitleCenterState.value,
|
|
|
- titleMaxLines = BookshelfConfig.bookshelfTitleMaxLinesState.value,
|
|
|
- coverShadow = BookshelfConfig.bookshelfCoverShadowState.value,
|
|
|
- onClick = {
|
|
|
- scope.launch { pagerState.scrollToPage(index) }
|
|
|
- viewModel.setInFolderRoot(false)
|
|
|
- },
|
|
|
- onLongClick = {
|
|
|
- viewModel.showOverlay(BookshelfOverlay.GroupEditSheet(group.groupId))
|
|
|
- }
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- } else {
|
|
|
- if (isUsingStandaloneSearchGroup) {
|
|
|
- BookshelfPage(
|
|
|
- paddingValues = paddingValues,
|
|
|
- books = uiState.items,
|
|
|
- uiState = uiState,
|
|
|
- selectedBookUrls = selectedBookUrls,
|
|
|
- canReorderBooks = false,
|
|
|
- onToggleBookSelection = { toggleBookSelection(it.book.bookUrl) },
|
|
|
- draggingBooks = null,
|
|
|
- pendingSavedBooks = null,
|
|
|
- onDragStarted = {},
|
|
|
- onMoveBook = { _, _, _ -> },
|
|
|
- onDragFinished = {},
|
|
|
- onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
|
|
|
- onBookClick = onBookClick,
|
|
|
- onBookLongClick = onBookLongClick,
|
|
|
- isCurrentPage = true,
|
|
|
- sharedTransitionScope = sharedTransitionScope,
|
|
|
- animatedVisibilityScope = animatedVisibilityScope,
|
|
|
- )
|
|
|
- } else {
|
|
|
- HorizontalPager(
|
|
|
- state = pagerState,
|
|
|
- modifier = Modifier
|
|
|
- .fillMaxSize()
|
|
|
- .then(
|
|
|
- with(sharedTransitionScope) {
|
|
|
- if (this != null) Modifier.skipToLookaheadSize() else Modifier
|
|
|
- }
|
|
|
- ),
|
|
|
- beyondViewportPageCount = 0,
|
|
|
- key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
|
|
|
- ) { pageIndex ->
|
|
|
- val group = uiState.groups.getOrNull(pageIndex)
|
|
|
- if (group != null) {
|
|
|
- val isSelectedGroup = group.groupId == uiState.selectedGroupId
|
|
|
- val books = uiState.allGroupBooks[group.groupId]
|
|
|
- ?: if (isSelectedGroup) uiState.items
|
|
|
- else persistentListOf()
|
|
|
- val canReorderBooks = isEditMode &&
|
|
|
- !uiState.isSearch &&
|
|
|
- (group.bookSort.takeIf { it >= 0 }
|
|
|
- ?: uiState.bookshelfSort) == 3 &&
|
|
|
- isSelectedGroup
|
|
|
- BookshelfPage(
|
|
|
- paddingValues = paddingValues,
|
|
|
- books = books,
|
|
|
- uiState = uiState,
|
|
|
- selectedBookUrls = selectedBookUrls,
|
|
|
- canReorderBooks = canReorderBooks,
|
|
|
- onToggleBookSelection = { toggleBookSelection(it.book.bookUrl) },
|
|
|
- draggingBooks = if (isSelectedGroup) {
|
|
|
- uiState.draggingBooks
|
|
|
- } else {
|
|
|
- null
|
|
|
- },
|
|
|
- pendingSavedBooks = if (isSelectedGroup) {
|
|
|
- uiState.pendingSavedBooks
|
|
|
- } else {
|
|
|
- null
|
|
|
- },
|
|
|
- onDragStarted = {
|
|
|
- if (isSelectedGroup) viewModel.startDraggingBooks(it)
|
|
|
- },
|
|
|
- onMoveBook = { from, to, currentBooks ->
|
|
|
- if (isSelectedGroup) {
|
|
|
- viewModel.moveDraggingBook(from, to, currentBooks)
|
|
|
- }
|
|
|
- },
|
|
|
- onDragFinished = {
|
|
|
- if (isSelectedGroup) viewModel.finishDraggingBooks()
|
|
|
- },
|
|
|
- onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
|
|
|
- onBookClick = onBookClick,
|
|
|
- onBookLongClick = onBookLongClick,
|
|
|
- isCurrentPage = isSelectedGroup,
|
|
|
- sharedTransitionScope = sharedTransitionScope,
|
|
|
- animatedVisibilityScope = animatedVisibilityScope,
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- TopFloatingStickyItem(
|
|
|
- item = editStickySummary,
|
|
|
- modifier = Modifier
|
|
|
- .align(Alignment.TopCenter)
|
|
|
- .padding(top = paddingValues.calculateTopPadding() + 6.dp),
|
|
|
- ) { summary ->
|
|
|
- Box {
|
|
|
- NormalCard(
|
|
|
- cornerRadius = 32.dp,
|
|
|
- containerColor = LegadoTheme.colorScheme.surfaceContainer,
|
|
|
- contentColor = LegadoTheme.colorScheme.onCardContainer
|
|
|
- ) {
|
|
|
- Row(
|
|
|
- modifier = Modifier.padding(all = 8.dp),
|
|
|
- verticalAlignment = Alignment.CenterVertically
|
|
|
- ) {
|
|
|
- TextCard(
|
|
|
- icon = AppIcons.Close,
|
|
|
- backgroundColor = LegadoTheme.colorScheme.surfaceContainerHighest,
|
|
|
- cornerRadius = 16.dp,
|
|
|
- verticalPadding = 8.dp,
|
|
|
- horizontalPadding = 8.dp,
|
|
|
- onClick = exitEditMode
|
|
|
- )
|
|
|
- Spacer(modifier = Modifier.width(8.dp))
|
|
|
- AppText(
|
|
|
- text = stringResource(
|
|
|
- R.string.bookshelf_selected_count,
|
|
|
- summary.selectedCount
|
|
|
- ),
|
|
|
- style = LegadoTheme.typography.labelSmallEmphasized
|
|
|
- )
|
|
|
- AppText(
|
|
|
- text = " · ${
|
|
|
- stringResource(
|
|
|
- R.string.bookshelf_total_count,
|
|
|
- summary.currentGroupTotalCount
|
|
|
- )
|
|
|
- }",
|
|
|
- style = LegadoTheme.typography.labelSmallEmphasized
|
|
|
- )
|
|
|
- Spacer(modifier = Modifier.width(8.dp))
|
|
|
- if (summary.showGroupName && !summary.groupName.isNullOrBlank()) {
|
|
|
- TextCard(
|
|
|
- text = summary.groupName,
|
|
|
- textStyle = LegadoTheme.typography.labelSmallEmphasized,
|
|
|
- backgroundColor = LegadoTheme.colorScheme.surfaceContainerHighest,
|
|
|
- cornerRadius = 16.dp,
|
|
|
- verticalPadding = 8.dp,
|
|
|
- horizontalPadding = 12.dp,
|
|
|
- onClick = {
|
|
|
- viewModel.showOverlay(BookshelfOverlay.GroupMenu)
|
|
|
- }
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- if (summary.showGroupName) {
|
|
|
- RoundDropdownMenu(
|
|
|
- expanded = showGroupMenu,
|
|
|
- onDismissRequest = { viewModel.dismissOverlay() }
|
|
|
- ) { dismiss ->
|
|
|
- uiState.groups.forEach { group ->
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = group.groupName,
|
|
|
- onClick = {
|
|
|
- val targetIndex =
|
|
|
- uiState.groups.indexOfFirst { it.groupId == group.groupId }
|
|
|
- if (targetIndex >= 0) {
|
|
|
- scope.launch {
|
|
|
- if (pagerState.currentPage != targetIndex) {
|
|
|
- pagerState.animateScrollToPage(targetIndex)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- if (uiState.isSearch || uiState.selectedGroupId != group.groupId) {
|
|
|
- viewModel.changeGroup(group.groupId)
|
|
|
- }
|
|
|
- if (bookGroupStyle == 2) {
|
|
|
- viewModel.setInFolderRoot(false)
|
|
|
- }
|
|
|
- dismiss()
|
|
|
- },
|
|
|
- trailingIcon = {
|
|
|
- if (currentMenuGroupId == group.groupId) {
|
|
|
- Icon(
|
|
|
- Icons.Default.Check,
|
|
|
- null,
|
|
|
- modifier = Modifier.size(18.dp)
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- BookshelfOverlays(
|
|
|
- activeOverlay = activeOverlay,
|
|
|
- uiState = uiState,
|
|
|
- viewModel = viewModel,
|
|
|
- selectedBookUrls = selectedBookUrls,
|
|
|
- importLauncher = importLauncher,
|
|
|
- exportLauncher = exportLauncher,
|
|
|
- clearSelection = clearSelection
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-@Composable
|
|
|
-private fun BookshelfTopBar(
|
|
|
- uiState: BookshelfUiState,
|
|
|
- scrollBehavior: GlassTopAppBarScrollBehavior,
|
|
|
- onSearchClick: () -> Unit,
|
|
|
- onSearchQueryChange: (String) -> Unit,
|
|
|
- onSearchSubmit: (String) -> Unit,
|
|
|
- onClearSearch: () -> Unit,
|
|
|
- actions: @Composable RowScope.() -> Unit = {},
|
|
|
- bottomContent: @Composable ColumnScope.() -> Unit = {}
|
|
|
-) {
|
|
|
- val searchContentDescription = stringResource(R.string.search)
|
|
|
- GlassMediumFlexibleTopAppBar(
|
|
|
- modifier = Modifier.fillMaxWidth(),
|
|
|
- title = if (uiState.isLoading) {
|
|
|
- stringResource(R.string.loading)
|
|
|
- } else {
|
|
|
- uiState.title.ifEmpty { stringResource(R.string.bookshelf) }
|
|
|
- },
|
|
|
- useCharMode = uiState.isLoading,
|
|
|
- subtitle = uiState.subtitle,
|
|
|
- scrollBehavior = scrollBehavior,
|
|
|
- actions = {
|
|
|
- TopBarActionButton(
|
|
|
- onClick = onSearchClick,
|
|
|
- imageVector = AppIcons.Search,
|
|
|
- contentDescription = searchContentDescription
|
|
|
- )
|
|
|
- actions()
|
|
|
- },
|
|
|
- bottomContent = {
|
|
|
- AnimatedVisibility(
|
|
|
- modifier = Modifier.adaptiveHorizontalPadding(),
|
|
|
- visible = uiState.isSearch,
|
|
|
- enter = expandVertically() + fadeIn(),
|
|
|
- exit = shrinkVertically() + fadeOut()
|
|
|
- ) {
|
|
|
- SearchBar(
|
|
|
- query = uiState.searchKey,
|
|
|
- onQueryChange = onSearchQueryChange,
|
|
|
- onSearch = onSearchSubmit,
|
|
|
- placeholder = stringResource(R.string.input_search_key),
|
|
|
- leadingIcon = {
|
|
|
- AppIcon(
|
|
|
- imageVector = AppIcons.Search,
|
|
|
- contentDescription = null
|
|
|
- )
|
|
|
- },
|
|
|
- trailingIcon = {
|
|
|
- if (uiState.searchKey.isNotEmpty()) {
|
|
|
- SmallPlainButton(
|
|
|
- modifier = Modifier.padding(start = 12.dp),
|
|
|
- onClick = onClearSearch,
|
|
|
- icon = AppIcons.Close,
|
|
|
- contentDescription = stringResource(R.string.clear)
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- )
|
|
|
- }
|
|
|
- bottomContent()
|
|
|
- }
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-@Composable
|
|
|
-private fun BookshelfOverlays(
|
|
|
- activeOverlay: BookshelfOverlay?,
|
|
|
- uiState: BookshelfUiState,
|
|
|
- viewModel: BookshelfViewModel,
|
|
|
- selectedBookUrls: Set<String>,
|
|
|
- importLauncher: ManagedActivityResultLauncher<Array<String>, Uri?>,
|
|
|
- exportLauncher: ManagedActivityResultLauncher<String, Uri?>,
|
|
|
- clearSelection: () -> Unit
|
|
|
-) {
|
|
|
- BookshelfConfigSheet(
|
|
|
- show = activeOverlay == BookshelfOverlay.ConfigSheet,
|
|
|
- onDismissRequest = { viewModel.dismissOverlay() }
|
|
|
- )
|
|
|
-
|
|
|
- GroupManageSheet(
|
|
|
- show = activeOverlay == BookshelfOverlay.GroupManageSheet,
|
|
|
- onDismissRequest = { viewModel.dismissOverlay() }
|
|
|
- )
|
|
|
-
|
|
|
- val groups by viewModel.allGroupsFlow.collectAsStateWithLifecycle()
|
|
|
-
|
|
|
- if (activeOverlay is BookshelfOverlay.GroupEditSheet) {
|
|
|
- val editGroup = groups.firstOrNull { it.groupId == activeOverlay.groupId }
|
|
|
- if (editGroup != null) {
|
|
|
- GroupEditSheet(
|
|
|
- show = true,
|
|
|
- group = editGroup,
|
|
|
- onDismissRequest = { viewModel.dismissOverlay() }
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- GroupSelectSheet(
|
|
|
- show = activeOverlay == BookshelfOverlay.GroupSelectSheet,
|
|
|
- groups = groups.filter { it.groupId > 0 },
|
|
|
- currentGroupId = 0L,
|
|
|
- onDismissRequest = { viewModel.dismissOverlay() },
|
|
|
- onConfirm = { groupId ->
|
|
|
- viewModel.moveBooksToGroup(selectedBookUrls, groupId)
|
|
|
- viewModel.dismissOverlay()
|
|
|
- clearSelection()
|
|
|
- }
|
|
|
- )
|
|
|
-
|
|
|
- SourceInputDialog(
|
|
|
- show = activeOverlay == BookshelfOverlay.AddUrlDialog,
|
|
|
- title = stringResource(R.string.add_book_url),
|
|
|
- onDismissRequest = { viewModel.dismissOverlay() },
|
|
|
- onConfirm = { url ->
|
|
|
- viewModel.addBookByUrl(url)
|
|
|
- viewModel.dismissOverlay()
|
|
|
- }
|
|
|
- )
|
|
|
-
|
|
|
- FilePickerSheet(
|
|
|
- show = activeOverlay == BookshelfOverlay.ImportSheet,
|
|
|
- onDismissRequest = { viewModel.dismissOverlay() },
|
|
|
- title = stringResource(R.string.import_bookshelf),
|
|
|
- onSelectSysFile = { types ->
|
|
|
- importLauncher.launch(types)
|
|
|
- viewModel.dismissOverlay()
|
|
|
- },
|
|
|
- onManualInput = {
|
|
|
- viewModel.showOverlay(BookshelfOverlay.AddUrlDialog)
|
|
|
- },
|
|
|
- allowExtensions = arrayOf("json", "txt")
|
|
|
- )
|
|
|
-
|
|
|
- FilePickerSheet(
|
|
|
- show = activeOverlay == BookshelfOverlay.ExportSheet,
|
|
|
- onDismissRequest = { viewModel.dismissOverlay() },
|
|
|
- title = stringResource(R.string.export_bookshelf),
|
|
|
- onSelectSysDir = {
|
|
|
- viewModel.dismissOverlay()
|
|
|
- exportLauncher.launch("bookshelf.json")
|
|
|
- },
|
|
|
- onUpload = {
|
|
|
- viewModel.dismissOverlay()
|
|
|
- viewModel.uploadBookshelf(uiState.items)
|
|
|
- }
|
|
|
- )
|
|
|
-
|
|
|
- AppLogSheet(
|
|
|
- show = activeOverlay == BookshelfOverlay.LogSheet,
|
|
|
- onDismissRequest = { viewModel.dismissOverlay() }
|
|
|
- )
|
|
|
-
|
|
|
- AppAlertDialog(
|
|
|
- show = activeOverlay == BookshelfOverlay.BatchDownloadConfirmDialog,
|
|
|
- onDismissRequest = { viewModel.dismissOverlay() },
|
|
|
- title = stringResource(R.string.draw),
|
|
|
- text = stringResource(R.string.sure_cache_book),
|
|
|
- confirmText = stringResource(android.R.string.ok),
|
|
|
- onConfirm = {
|
|
|
- viewModel.dismissOverlay()
|
|
|
- viewModel.downloadBooks(selectedBookUrls)
|
|
|
- },
|
|
|
- dismissText = stringResource(android.R.string.cancel),
|
|
|
- onDismiss = { viewModel.dismissOverlay() }
|
|
|
- )
|
|
|
-
|
|
|
- if (uiState.isLoading) {
|
|
|
- Dialog(onDismissRequest = {}) {
|
|
|
- NormalCard(
|
|
|
- cornerRadius = 12.dp,
|
|
|
- containerColor = LegadoTheme.colorScheme.surfaceContainerHigh
|
|
|
- ) {
|
|
|
- Column(
|
|
|
- modifier = Modifier.padding(24.dp),
|
|
|
- horizontalAlignment = Alignment.CenterHorizontally
|
|
|
- ) {
|
|
|
- AppCircularProgressIndicator()
|
|
|
- uiState.loadingText?.let {
|
|
|
- AppText(
|
|
|
- text = it,
|
|
|
- modifier = Modifier.padding(top = 16.dp),
|
|
|
- style = LegadoTheme.typography.bodyMedium
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-private data class BookshelfEditStickySummary(
|
|
|
- val selectedCount: Int,
|
|
|
- val currentGroupTotalCount: Int,
|
|
|
- val groupName: String?,
|
|
|
- val showGroupName: Boolean,
|
|
|
-)
|
|
|
-
|
|
|
-@Composable
|
|
|
-fun BookshelfPage(
|
|
|
- paddingValues: PaddingValues,
|
|
|
- books: ImmutableList<BookUiItem>,
|
|
|
- uiState: BookshelfUiState,
|
|
|
- selectedBookUrls: ImmutableSet<String>,
|
|
|
- canReorderBooks: Boolean,
|
|
|
- onToggleBookSelection: (BookUiItem) -> Unit,
|
|
|
- draggingBooks: ImmutableList<BookUiItem>?,
|
|
|
- pendingSavedBooks: ImmutableList<BookUiItem>?,
|
|
|
- onDragStarted: (ImmutableList<BookUiItem>) -> Unit,
|
|
|
- onMoveBook: (fromIndex: Int, toIndex: Int, currentBooks: ImmutableList<BookUiItem>) -> Unit,
|
|
|
- onDragFinished: () -> Unit,
|
|
|
- onGlobalSearch: () -> Unit,
|
|
|
- onBookClick: (BookShelfItem) -> Unit,
|
|
|
- onBookLongClick: (BookShelfItem, String?) -> Unit,
|
|
|
- isCurrentPage: Boolean = true,
|
|
|
- sharedTransitionScope: SharedTransitionScope? = null,
|
|
|
- animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
|
|
-) {
|
|
|
- if (books.isEmpty()) {
|
|
|
- if (!isCurrentPage) return
|
|
|
- if (uiState.isSearch) {
|
|
|
- EmptyMessage(
|
|
|
- modifier = Modifier
|
|
|
- .fillMaxSize()
|
|
|
- .padding(
|
|
|
- top = paddingValues.calculateTopPadding(),
|
|
|
- bottom = 120.dp
|
|
|
- ),
|
|
|
- message = stringResource(R.string.bookshelf_empty_global_search),
|
|
|
- buttonText = stringResource(R.string.global_search),
|
|
|
- onButtonClick = onGlobalSearch
|
|
|
- )
|
|
|
- } else if (!uiState.isInitialLoading) {
|
|
|
- EmptyMessage(
|
|
|
- modifier = Modifier
|
|
|
- .fillMaxSize()
|
|
|
- .padding(
|
|
|
- top = paddingValues.calculateTopPadding(),
|
|
|
- bottom = 120.dp
|
|
|
- ),
|
|
|
- messageResId = R.string.bookshelf_empty
|
|
|
- )
|
|
|
- }
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- val configuration = LocalConfiguration.current
|
|
|
- val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
|
|
- val bookshelfLayoutMode by remember(isLandscape) {
|
|
|
- derivedStateOf {
|
|
|
- if (isLandscape) BookshelfConfig.bookshelfLayoutModeLandscapeState.value
|
|
|
- else BookshelfConfig.bookshelfLayoutModePortraitState.value
|
|
|
- }
|
|
|
- }
|
|
|
- val bookshelfLayoutGrid by remember(isLandscape) {
|
|
|
- derivedStateOf {
|
|
|
- if (isLandscape) BookshelfConfig.bookshelfLayoutGridLandscapeState.value
|
|
|
- else BookshelfConfig.bookshelfLayoutGridPortraitState.value
|
|
|
- }
|
|
|
- }
|
|
|
- val bookshelfLayoutList by remember(isLandscape) {
|
|
|
- derivedStateOf {
|
|
|
- if (isLandscape) BookshelfConfig.bookshelfLayoutListLandscapeState.value
|
|
|
- else BookshelfConfig.bookshelfLayoutListPortraitState.value
|
|
|
- }
|
|
|
- }
|
|
|
- val columns by remember {
|
|
|
- derivedStateOf {
|
|
|
- if (bookshelfLayoutMode == 0) bookshelfLayoutList else bookshelfLayoutGrid
|
|
|
- }
|
|
|
- }
|
|
|
- val isGridMode by remember { derivedStateOf { bookshelfLayoutMode != 0 } }
|
|
|
- val bookItemGridStyle by BookshelfConfig.bookshelfGridLayoutState
|
|
|
- val bookItemIsCompact by BookshelfConfig.bookshelfLayoutCompactState
|
|
|
- val bookItemTitleSmallFont by BookshelfConfig.bookshelfTitleSmallFontState
|
|
|
- val bookItemTitleCenter by BookshelfConfig.bookshelfTitleCenterState
|
|
|
- val bookItemTitleMaxLines by BookshelfConfig.bookshelfTitleMaxLinesState
|
|
|
- val bookItemCoverShadow by BookshelfConfig.bookshelfCoverShadowState
|
|
|
- val showFastScroll by BookshelfConfig.showBookshelfFastScrollerState
|
|
|
- val totalHorizontalPadding =
|
|
|
- if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) 12.dp else 16.dp
|
|
|
- val gridContentHorizontalPadding = totalHorizontalPadding / 2
|
|
|
- val gridInnerHorizontalPadding = totalHorizontalPadding / 2
|
|
|
- val hapticFeedback = LocalHapticFeedback.current
|
|
|
- val displayBooks = draggingBooks ?: pendingSavedBooks ?: books
|
|
|
- val gridState = rememberLazyGridState()
|
|
|
- val reorderableState = rememberReorderableLazyGridState(gridState) { from, to ->
|
|
|
- if (canReorderBooks) {
|
|
|
- onMoveBook(from.index, to.index, displayBooks)
|
|
|
- hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
|
|
|
- }
|
|
|
- }
|
|
|
- LaunchedEffect(reorderableState.isAnyItemDragging) {
|
|
|
- if (!reorderableState.isAnyItemDragging) {
|
|
|
- onDragFinished()
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- Box(
|
|
|
- modifier = Modifier
|
|
|
- .fillMaxSize()
|
|
|
- .then(
|
|
|
- with(sharedTransitionScope) {
|
|
|
- if (this != null) Modifier.skipToLookaheadSize() else Modifier
|
|
|
- }
|
|
|
- )
|
|
|
- ) {
|
|
|
- FastScrollLazyVerticalGrid(
|
|
|
- columns = GridCells.Fixed(columns.coerceAtLeast(1)),
|
|
|
- state = gridState,
|
|
|
- modifier = Modifier
|
|
|
- .fillMaxSize()
|
|
|
- .semantics(mergeDescendants = true) { contentDescription = "bookshelf_list" }
|
|
|
- .then(
|
|
|
- with(sharedTransitionScope) {
|
|
|
- if (this != null) Modifier.skipToLookaheadSize() else Modifier
|
|
|
- }
|
|
|
- ),
|
|
|
- contentPadding = adaptiveContentPaddingBookshelf(
|
|
|
- top = paddingValues.calculateTopPadding(),
|
|
|
- bottom = if (ThemeConfig.useFloatingBottomBar || ThemeConfig.enableBlur) 120.dp else 8.dp,
|
|
|
- horizontal = 8.dp
|
|
|
- ),
|
|
|
- verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
|
|
|
- horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
|
|
|
- showFastScroll = showFastScroll
|
|
|
- ) {
|
|
|
- items(displayBooks, key = { it.book.bookUrl }) { bookUi ->
|
|
|
- val isSelected = selectedBookUrls.contains(bookUi.book.bookUrl)
|
|
|
- val sharedCoverKey = if (isCurrentPage) {
|
|
|
- bookCoverSharedElementKey(
|
|
|
- bookUi.book.bookUrl,
|
|
|
- "bookshelf:${uiState.selectedGroupId}"
|
|
|
- )
|
|
|
- } else {
|
|
|
- null
|
|
|
- }
|
|
|
- ReorderableItem(
|
|
|
- state = reorderableState,
|
|
|
- key = bookUi.book.bookUrl,
|
|
|
- enabled = canReorderBooks
|
|
|
- ) {
|
|
|
- BookItem(
|
|
|
- bookUi = bookUi,
|
|
|
- modifier = Modifier.then(
|
|
|
- if (canReorderBooks) {
|
|
|
- Modifier.longPressDraggableHandle(
|
|
|
- onDragStarted = {
|
|
|
- onDragStarted(displayBooks)
|
|
|
- hapticFeedback.performHapticFeedback(
|
|
|
- HapticFeedbackType.GestureThresholdActivate
|
|
|
- )
|
|
|
- },
|
|
|
- onDragStopped = {
|
|
|
- hapticFeedback.performHapticFeedback(
|
|
|
- HapticFeedbackType.GestureEnd
|
|
|
- )
|
|
|
- }
|
|
|
- )
|
|
|
- } else {
|
|
|
- Modifier
|
|
|
- }
|
|
|
- ),
|
|
|
- layoutMode = bookshelfLayoutMode,
|
|
|
- isSelected = isSelected,
|
|
|
- gridStyle = bookItemGridStyle,
|
|
|
- isCompact = bookItemIsCompact,
|
|
|
- isUpdating = uiState.updatingBooks.contains(bookUi.book.bookUrl),
|
|
|
- titleSmallFont = bookItemTitleSmallFont,
|
|
|
- titleCenter = bookItemTitleCenter,
|
|
|
- titleMaxLines = bookItemTitleMaxLines,
|
|
|
- coverShadow = bookItemCoverShadow,
|
|
|
- isSearchMode = uiState.isSearch,
|
|
|
- searchKey = uiState.searchKey,
|
|
|
- sharedTransitionScope = sharedTransitionScope,
|
|
|
- animatedVisibilityScope = animatedVisibilityScope,
|
|
|
- sharedCoverKey = sharedCoverKey,
|
|
|
- onClick = {
|
|
|
- if (uiState.isEditMode) {
|
|
|
- onToggleBookSelection(bookUi)
|
|
|
- } else {
|
|
|
- onBookClick(bookUi.book)
|
|
|
- }
|
|
|
- },
|
|
|
- onLongClick = if (canReorderBooks) {
|
|
|
- null
|
|
|
- } else {
|
|
|
- {
|
|
|
- if (uiState.isEditMode) {
|
|
|
- onToggleBookSelection(bookUi)
|
|
|
- } else {
|
|
|
- onBookLongClick(bookUi.book, sharedCoverKey)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
+import sh.calvin.reorderable.rememberReorderableLazyGridState
|