|
|
@@ -2,166 +2,178 @@ package io.legado.app.ui.book.bookmark
|
|
|
|
|
|
import android.net.Uri
|
|
|
import android.widget.Toast
|
|
|
+import androidx.activity.compose.BackHandler
|
|
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
|
import androidx.compose.animation.AnimatedContent
|
|
|
import androidx.compose.animation.AnimatedVisibility
|
|
|
-import androidx.compose.animation.animateColorAsState
|
|
|
-import androidx.compose.animation.core.FastOutSlowInEasing
|
|
|
-import androidx.compose.animation.core.tween
|
|
|
import androidx.compose.animation.expandVertically
|
|
|
import androidx.compose.animation.fadeIn
|
|
|
import androidx.compose.animation.fadeOut
|
|
|
import androidx.compose.animation.shrinkVertically
|
|
|
+import androidx.compose.animation.togetherWith
|
|
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
|
-import androidx.compose.foundation.combinedClickable
|
|
|
-import androidx.compose.foundation.layout.Box
|
|
|
+import androidx.compose.foundation.layout.Arrangement
|
|
|
import androidx.compose.foundation.layout.Column
|
|
|
+import androidx.compose.foundation.layout.Row
|
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
|
import androidx.compose.foundation.layout.padding
|
|
|
+import androidx.compose.foundation.layout.width
|
|
|
+import androidx.compose.foundation.lazy.LazyColumn
|
|
|
import androidx.compose.foundation.lazy.items
|
|
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
|
+import androidx.compose.foundation.pager.HorizontalPager
|
|
|
+import androidx.compose.foundation.pager.rememberPagerState
|
|
|
import androidx.compose.material.icons.Icons
|
|
|
import androidx.compose.material.icons.filled.Search
|
|
|
-import androidx.compose.material.icons.filled.UnfoldLess
|
|
|
-import androidx.compose.material.icons.filled.UnfoldMore
|
|
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
|
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
|
|
-import androidx.compose.material3.HorizontalDivider
|
|
|
-import androidx.compose.material3.ListItem
|
|
|
-import androidx.compose.material3.ListItemDefaults
|
|
|
-import androidx.compose.material3.MaterialTheme
|
|
|
import androidx.compose.runtime.Composable
|
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
|
-import androidx.compose.runtime.derivedStateOf
|
|
|
import androidx.compose.runtime.getValue
|
|
|
import androidx.compose.runtime.mutableStateOf
|
|
|
import androidx.compose.runtime.remember
|
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
|
import androidx.compose.runtime.setValue
|
|
|
+import androidx.compose.runtime.snapshotFlow
|
|
|
+import androidx.compose.ui.Alignment
|
|
|
import androidx.compose.ui.Modifier
|
|
|
-import androidx.compose.ui.graphics.Color
|
|
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
|
import androidx.compose.ui.platform.LocalContext
|
|
|
import androidx.compose.ui.res.stringResource
|
|
|
-import androidx.compose.ui.semantics.Role
|
|
|
-import androidx.compose.ui.semantics.contentDescription
|
|
|
-import androidx.compose.ui.semantics.onClick
|
|
|
-import androidx.compose.ui.semantics.role
|
|
|
-import androidx.compose.ui.semantics.semantics
|
|
|
-import androidx.compose.ui.semantics.stateDescription
|
|
|
+import androidx.compose.ui.text.style.TextOverflow
|
|
|
import androidx.compose.ui.unit.dp
|
|
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
|
import io.legado.app.R
|
|
|
import io.legado.app.data.entities.Bookmark
|
|
|
import io.legado.app.ui.theme.LegadoTheme
|
|
|
-import io.legado.app.ui.theme.ThemeResolver
|
|
|
import io.legado.app.ui.theme.adaptiveContentPadding
|
|
|
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
|
|
import io.legado.app.ui.widget.components.AppScaffold
|
|
|
import io.legado.app.ui.widget.components.EmptyMessage
|
|
|
import io.legado.app.ui.widget.components.SearchBar
|
|
|
import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet
|
|
|
-import io.legado.app.ui.widget.components.bookmark.BookmarkItem
|
|
|
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.icon.AppIcons
|
|
|
-import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
|
|
|
-import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
|
|
|
+import io.legado.app.ui.widget.components.image.cover.CoilBookCover
|
|
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
|
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
|
|
+import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
|
|
import io.legado.app.ui.widget.components.text.AppText
|
|
|
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
|
|
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
|
|
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
|
|
|
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
|
|
import kotlinx.coroutines.flow.collectLatest
|
|
|
+import kotlinx.coroutines.flow.drop
|
|
|
import kotlinx.coroutines.launch
|
|
|
import org.koin.androidx.compose.koinViewModel
|
|
|
-import top.yukonga.miuix.kmp.theme.MiuixTheme
|
|
|
|
|
|
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
|
|
|
- ExperimentalMaterial3ExpressiveApi::class
|
|
|
-)
|
|
|
+private const val BOOKMARK_TAB = 0
|
|
|
+private const val NOTE_TAB = 1
|
|
|
+private const val DETAIL_PAGE_COUNT = 2
|
|
|
+private const val DETAIL_TEXT_MAX_LINES = 3
|
|
|
+
|
|
|
+/**
|
|
|
+ * 书签页主体所处的状态,用于在「总览 / 详情」等状态切换时驱动过渡动画。
|
|
|
+ */
|
|
|
+private enum class BookmarkBodyState { Loading, EmptyLibrary, Overview, Detail }
|
|
|
+
|
|
|
@Composable
|
|
|
fun AllBookmarkRouteScreen(
|
|
|
viewModel: AllBookmarkViewModel = koinViewModel(),
|
|
|
onBack: () -> Unit,
|
|
|
) {
|
|
|
val context = LocalContext.current
|
|
|
- val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
|
|
- var pendingExportIsMd by remember { mutableStateOf(false) }
|
|
|
- val exportLauncher = rememberLauncherForActivityResult(
|
|
|
- contract = ActivityResultContracts.OpenDocumentTree()
|
|
|
- ) { uri: Uri? ->
|
|
|
- uri?.let {
|
|
|
- viewModel.onIntent(AllBookmarkIntent.Export(it, pendingExportIsMd))
|
|
|
+ val state by viewModel.uiState.collectAsStateWithLifecycle()
|
|
|
+ var exportMarkdown by remember { mutableStateOf(false) }
|
|
|
+ var exportBook by remember { mutableStateOf<BookmarkBookKey?>(null) }
|
|
|
+ val exportLauncher =
|
|
|
+ rememberLauncherForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri: Uri? ->
|
|
|
+ val book = exportBook
|
|
|
+ if (uri != null && book != null) {
|
|
|
+ viewModel.onIntent(AllBookmarkIntent.Export(uri, exportMarkdown, book))
|
|
|
Toast.makeText(context, context.getString(R.string.export_started), Toast.LENGTH_SHORT).show()
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
LaunchedEffect(viewModel) {
|
|
|
viewModel.effects.collectLatest { effect ->
|
|
|
when (effect) {
|
|
|
- is AllBookmarkEffect.ShowMessage ->
|
|
|
- Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
|
|
|
+ is AllBookmarkEffect.ShowMessage -> Toast.makeText(
|
|
|
+ context,
|
|
|
+ effect.message,
|
|
|
+ Toast.LENGTH_SHORT
|
|
|
+ ).show()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
AllBookmarkScreen(
|
|
|
- state = uiState,
|
|
|
+ state = state,
|
|
|
onIntent = viewModel::onIntent,
|
|
|
- onRequestExport = { isMarkdown ->
|
|
|
- pendingExportIsMd = isMarkdown
|
|
|
+ onRequestExport = { book, markdown ->
|
|
|
+ exportBook = book
|
|
|
+ exportMarkdown = markdown
|
|
|
exportLauncher.launch(null)
|
|
|
},
|
|
|
onBack = onBack,
|
|
|
)
|
|
|
}
|
|
|
|
|
|
+@OptIn(ExperimentalMaterial3Api::class)
|
|
|
@Composable
|
|
|
fun AllBookmarkScreen(
|
|
|
state: BookmarkUiState,
|
|
|
onIntent: (AllBookmarkIntent) -> Unit,
|
|
|
- onRequestExport: (Boolean) -> Unit,
|
|
|
+ onRequestExport: (BookmarkBookKey, Boolean) -> Unit,
|
|
|
onBack: () -> Unit,
|
|
|
) {
|
|
|
-
|
|
|
- val contentState = when {
|
|
|
- state.isLoading -> "LOADING"
|
|
|
- state.bookmarks.isEmpty() -> "EMPTY"
|
|
|
- else -> "CONTENT"
|
|
|
- }
|
|
|
- val searchText = state.searchQuery
|
|
|
- val collapsedGroups = state.collapsedGroups
|
|
|
- val bookmarksGrouped = state.bookmarks
|
|
|
- val bookmarkGroups = remember(bookmarksGrouped) { bookmarksGrouped.entries.toList() }
|
|
|
- val allKeys = bookmarksGrouped.keys
|
|
|
- val isAllCollapsed =
|
|
|
- allKeys.isNotEmpty() && allKeys.all { collapsedGroups.contains(it.toString()) }
|
|
|
- val listState = rememberLazyListState()
|
|
|
- val scope = rememberCoroutineScope()
|
|
|
+ val selectedBook = state.selectedBook
|
|
|
+ var showSearch by remember(selectedBook?.key) { mutableStateOf(false) }
|
|
|
var showMenu by remember { mutableStateOf(false) }
|
|
|
- var showSearch by remember { mutableStateOf(false) }
|
|
|
- var editingBookmark by remember { mutableStateOf<Bookmark?>(null) }
|
|
|
- var showBottomSheet by remember { mutableStateOf(false) }
|
|
|
+ val overviewListState = rememberLazyListState()
|
|
|
+ val bookmarkListState = rememberLazyListState()
|
|
|
+ val noteListState = rememberLazyListState()
|
|
|
+ val scope = rememberCoroutineScope()
|
|
|
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
|
|
- val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
|
|
- val stickyGroup by remember(bookmarkGroups, collapsedGroups, listState) {
|
|
|
- derivedStateOf {
|
|
|
- val firstVisibleIndex = listState.firstVisibleItemIndex
|
|
|
- val firstVisibleGroup = bookmarkGroups.getOrNull(firstVisibleIndex)
|
|
|
- ?: return@derivedStateOf null
|
|
|
- val isCollapsed = collapsedGroups.contains(firstVisibleGroup.key.toString())
|
|
|
- val shouldStick = firstVisibleIndex > 0 || listState.firstVisibleItemScrollOffset > 24
|
|
|
- if (!isCollapsed && shouldStick) {
|
|
|
- firstVisibleGroup.key
|
|
|
- } else {
|
|
|
- null
|
|
|
- }
|
|
|
- }
|
|
|
+ val pagerState = rememberPagerState(pageCount = { DETAIL_PAGE_COUNT })
|
|
|
+ val selectedTab = pagerState.currentPage
|
|
|
+ val bookmarkItems = remember(state.detailItems) {
|
|
|
+ state.detailItems.filter { it is BookmarkDetailItemUi.SavedBookmark }
|
|
|
+ }
|
|
|
+ val noteItems = remember(state.detailItems) {
|
|
|
+ state.detailItems.filterIsInstance<BookmarkDetailItemUi.Marking>()
|
|
|
+ }
|
|
|
+ val activeListState = when {
|
|
|
+ selectedBook == null -> overviewListState
|
|
|
+ selectedTab == NOTE_TAB -> noteListState
|
|
|
+ else -> bookmarkListState
|
|
|
+ }
|
|
|
+ val bodyState = when {
|
|
|
+ state.isLoading -> BookmarkBodyState.Loading
|
|
|
+ selectedBook == null && state.books.isEmpty() -> BookmarkBodyState.EmptyLibrary
|
|
|
+ selectedBook == null -> BookmarkBodyState.Overview
|
|
|
+ else -> BookmarkBodyState.Detail
|
|
|
+ }
|
|
|
+
|
|
|
+ BackHandler(enabled = selectedBook != null) { onIntent(AllBookmarkIntent.CloseBook) }
|
|
|
+ // 换书时回到该书的初始页:带定位目标直接落在备注页,否则落在书签页
|
|
|
+ LaunchedEffect(selectedBook?.key) {
|
|
|
+ if (selectedBook == null) return@LaunchedEffect
|
|
|
+ val initialPage = if (state.targetMarkingId != null) NOTE_TAB else BOOKMARK_TAB
|
|
|
+ if (pagerState.currentPage != initialPage) pagerState.scrollToPage(initialPage)
|
|
|
+ }
|
|
|
+ // 横滑落位后清掉上一个 tab 留下的搜索词
|
|
|
+ LaunchedEffect(pagerState) {
|
|
|
+ snapshotFlow { pagerState.settledPage }
|
|
|
+ .drop(1)
|
|
|
+ .collect { onIntent(AllBookmarkIntent.SetSearchQuery("")) }
|
|
|
+ }
|
|
|
+ LaunchedEffect(state.targetMarkingId, state.detailItems) {
|
|
|
+ val target = state.targetMarkingId ?: return@LaunchedEffect
|
|
|
+ if (pagerState.currentPage != NOTE_TAB) pagerState.scrollToPage(NOTE_TAB)
|
|
|
+ val index = noteItems.indexOfFirst { it.marking.id == target }
|
|
|
+ if (index >= 0) noteListState.scrollToItem(index)
|
|
|
+ onIntent(AllBookmarkIntent.TargetConsumed)
|
|
|
}
|
|
|
|
|
|
AppScaffold(
|
|
|
@@ -169,279 +181,311 @@ fun AllBookmarkScreen(
|
|
|
topBar = {
|
|
|
Column {
|
|
|
GlassMediumFlexibleTopAppBar(
|
|
|
- title = stringResource(R.string.all_bookmark),
|
|
|
+ title = selectedBook?.key?.name ?: stringResource(R.string.all_bookmark),
|
|
|
scrollBehavior = scrollBehavior,
|
|
|
navigationIcon = {
|
|
|
- TopBarNavigationButton(onClick = onBack)
|
|
|
+ TopBarNavigationButton(
|
|
|
+ onClick = {
|
|
|
+ if (selectedBook == null) onBack() else onIntent(AllBookmarkIntent.CloseBook)
|
|
|
+ }
|
|
|
+ )
|
|
|
},
|
|
|
actions = {
|
|
|
- if (bookmarksGrouped.isNotEmpty()) {
|
|
|
- TopBarActionButton(
|
|
|
- onClick = { onIntent(AllBookmarkIntent.ToggleAllCollapse(allKeys)) },
|
|
|
- imageVector = if (isAllCollapsed) Icons.Default.UnfoldMore else Icons.Default.UnfoldLess,
|
|
|
- contentDescription = stringResource(
|
|
|
- if (isAllCollapsed) {
|
|
|
- R.string.a11y_expand_all_bookmark_groups
|
|
|
- } else {
|
|
|
- R.string.a11y_collapse_all_bookmark_groups
|
|
|
- }
|
|
|
- )
|
|
|
- )
|
|
|
- }
|
|
|
TopBarActionButton(
|
|
|
onClick = {
|
|
|
showSearch = !showSearch
|
|
|
- if (!showSearch) {
|
|
|
- onIntent(AllBookmarkIntent.SetSearchQuery(""))
|
|
|
- }
|
|
|
+ if (!showSearch) onIntent(AllBookmarkIntent.SetSearchQuery(""))
|
|
|
},
|
|
|
imageVector = Icons.Default.Search,
|
|
|
- contentDescription = stringResource(R.string.search)
|
|
|
- )
|
|
|
- TopBarActionButton(
|
|
|
- onClick = { showMenu = true },
|
|
|
- imageVector = AppIcons.MoreVert,
|
|
|
- contentDescription = stringResource(R.string.more_menu)
|
|
|
+ contentDescription = stringResource(R.string.search),
|
|
|
)
|
|
|
- RoundDropdownMenu(
|
|
|
- expanded = showMenu,
|
|
|
- onDismissRequest = { showMenu = false }
|
|
|
- ) {
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.export_bookmarks_json),
|
|
|
- onClick = {
|
|
|
- showMenu = false
|
|
|
- onRequestExport(false)
|
|
|
- }
|
|
|
- )
|
|
|
- RoundDropdownMenuItem(
|
|
|
- text = stringResource(R.string.export_bookmarks_markdown),
|
|
|
- onClick = {
|
|
|
- showMenu = false
|
|
|
- onRequestExport(true)
|
|
|
- }
|
|
|
+ if (selectedBook != null) {
|
|
|
+ TopBarActionButton(
|
|
|
+ onClick = { showMenu = true },
|
|
|
+ imageVector = AppIcons.MoreVert,
|
|
|
+ contentDescription = stringResource(R.string.more_menu),
|
|
|
)
|
|
|
+ RoundDropdownMenu(
|
|
|
+ expanded = showMenu,
|
|
|
+ onDismissRequest = { showMenu = false }) {
|
|
|
+ RoundDropdownMenuItem(
|
|
|
+ text = stringResource(R.string.export_bookmarks_json),
|
|
|
+ onClick = {
|
|
|
+ showMenu = false
|
|
|
+ onRequestExport(selectedBook.key, false)
|
|
|
+ },
|
|
|
+ )
|
|
|
+ RoundDropdownMenuItem(
|
|
|
+ text = stringResource(R.string.export_bookmarks_markdown),
|
|
|
+ onClick = {
|
|
|
+ showMenu = false
|
|
|
+ onRequestExport(selectedBook.key, true)
|
|
|
+ },
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
+ },
|
|
|
)
|
|
|
-
|
|
|
AnimatedVisibility(
|
|
|
modifier = Modifier.adaptiveHorizontalPadding(),
|
|
|
visible = showSearch,
|
|
|
enter = expandVertically() + fadeIn(),
|
|
|
- exit = shrinkVertically() + fadeOut()
|
|
|
+ exit = shrinkVertically() + fadeOut(),
|
|
|
) {
|
|
|
SearchBar(
|
|
|
- query = searchText,
|
|
|
+ query = state.searchQuery,
|
|
|
onQueryChange = { onIntent(AllBookmarkIntent.SetSearchQuery(it)) },
|
|
|
- placeholder = stringResource(R.string.search),
|
|
|
- scrollState = listState,
|
|
|
- scope = scope
|
|
|
+ placeholder = if (selectedBook == null) {
|
|
|
+ stringResource(R.string.feature_bookmarks_search_notes)
|
|
|
+ } else {
|
|
|
+ stringResource(
|
|
|
+ R.string.feature_bookmarks_search_current,
|
|
|
+ stringResource(if (selectedTab == BOOKMARK_TAB) R.string.bookmark else R.string.bookmark_mark_note),
|
|
|
+ )
|
|
|
+ },
|
|
|
+ scrollState = activeListState,
|
|
|
+ scope = scope,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ if (selectedBook != null) {
|
|
|
+ AppTabRow(
|
|
|
+ tabTitles = listOf(
|
|
|
+ "${stringResource(R.string.bookmark)} ${selectedBook.bookmarkCount}",
|
|
|
+ "${stringResource(R.string.bookmark_mark_note)} ${selectedBook.markingCount}",
|
|
|
+ ),
|
|
|
+ selectedTabIndex = selectedTab,
|
|
|
+ onTabSelected = { index ->
|
|
|
+ scope.launch { pagerState.animateScrollToPage(index) }
|
|
|
+ onIntent(AllBookmarkIntent.SetSearchQuery(""))
|
|
|
+ },
|
|
|
+ modifier = Modifier.fillMaxWidth(),
|
|
|
+ isScrollable = false,
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
+ },
|
|
|
) { paddingValues ->
|
|
|
- Column(
|
|
|
- modifier = Modifier
|
|
|
- .fillMaxSize()
|
|
|
- ) {
|
|
|
- AnimatedContent(
|
|
|
- targetState = contentState,
|
|
|
- label = "bookmarkTransition"
|
|
|
- ) { state ->
|
|
|
- when (state) {
|
|
|
- "LOADING" -> {
|
|
|
- EmptyMessage(
|
|
|
- message = stringResource(R.string.loading),
|
|
|
- isLoading = true,
|
|
|
- modifier = Modifier
|
|
|
- .fillMaxSize()
|
|
|
- .padding(
|
|
|
- top = paddingValues.calculateTopPadding(),
|
|
|
- bottom = 120.dp
|
|
|
- )
|
|
|
+ val topPadding = paddingValues.calculateTopPadding()
|
|
|
+ AnimatedContent(
|
|
|
+ targetState = bodyState,
|
|
|
+ transitionSpec = { fadeIn() togetherWith fadeOut() },
|
|
|
+ label = "BookmarkBodyTransition",
|
|
|
+ modifier = Modifier.fillMaxSize(),
|
|
|
+ ) { body ->
|
|
|
+ when (body) {
|
|
|
+ BookmarkBodyState.Loading -> EmptyMessage(
|
|
|
+ message = stringResource(R.string.loading),
|
|
|
+ isLoading = true,
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxSize()
|
|
|
+ .padding(top = topPadding),
|
|
|
+ )
|
|
|
+
|
|
|
+ BookmarkBodyState.EmptyLibrary -> EmptyMessage(
|
|
|
+ message = if (state.searchQuery.isBlank()) stringResource(R.string.no_bookmark)
|
|
|
+ else stringResource(R.string.feature_bookmarks_no_matching_notes),
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxSize()
|
|
|
+ .padding(top = topPadding),
|
|
|
+ )
|
|
|
+
|
|
|
+ BookmarkBodyState.Overview -> BookOverview(
|
|
|
+ books = state.books,
|
|
|
+ listState = overviewListState,
|
|
|
+ topPadding = topPadding,
|
|
|
+ onBookClick = {
|
|
|
+ onIntent(
|
|
|
+ AllBookmarkIntent.OpenBook(
|
|
|
+ it.key,
|
|
|
+ it.matchedMarkingId
|
|
|
+ )
|
|
|
)
|
|
|
- }
|
|
|
+ },
|
|
|
+ )
|
|
|
|
|
|
- "EMPTY" -> {
|
|
|
+ BookmarkBodyState.Detail -> HorizontalPager(
|
|
|
+ state = pagerState,
|
|
|
+ modifier = Modifier.fillMaxSize(),
|
|
|
+ ) { tab ->
|
|
|
+ val pageItems = if (tab == BOOKMARK_TAB) bookmarkItems else noteItems
|
|
|
+ if (pageItems.isEmpty()) {
|
|
|
EmptyMessage(
|
|
|
- message = stringResource(R.string.no_bookmark),
|
|
|
+ message = if (tab == BOOKMARK_TAB) stringResource(R.string.no_bookmark)
|
|
|
+ else stringResource(R.string.feature_bookmarks_no_notes),
|
|
|
modifier = Modifier
|
|
|
.fillMaxSize()
|
|
|
- .padding(
|
|
|
- top = paddingValues.calculateTopPadding(),
|
|
|
- bottom = 120.dp
|
|
|
- )
|
|
|
+ .padding(top = topPadding),
|
|
|
+ )
|
|
|
+ } else {
|
|
|
+ DetailList(
|
|
|
+ items = pageItems,
|
|
|
+ listState = if (tab == BOOKMARK_TAB) bookmarkListState else noteListState,
|
|
|
+ topPadding = topPadding,
|
|
|
+ onIntent = onIntent,
|
|
|
)
|
|
|
- }
|
|
|
-
|
|
|
- "CONTENT" -> {
|
|
|
- Box(
|
|
|
- modifier = Modifier.fillMaxSize()
|
|
|
- ) {
|
|
|
- FastScrollLazyColumn(
|
|
|
- state = listState,
|
|
|
- modifier = Modifier.fillMaxSize(),
|
|
|
- contentPadding = adaptiveContentPadding(
|
|
|
- top = paddingValues.calculateTopPadding(),
|
|
|
- bottom = 120.dp
|
|
|
- )
|
|
|
- ) {
|
|
|
- items(
|
|
|
- items = bookmarkGroups,
|
|
|
- key = { it.key.toString() }
|
|
|
- ) { (headerKey, bookmarks) ->
|
|
|
- val isCollapsed = collapsedGroups.contains(headerKey.toString())
|
|
|
-
|
|
|
- GlassCard(
|
|
|
- modifier = Modifier
|
|
|
- .animateItem()
|
|
|
- .fillMaxWidth()
|
|
|
- .padding(vertical = 4.dp),
|
|
|
- cornerRadius = 12.dp,
|
|
|
- containerColor = LegadoTheme.colorScheme.surfaceContainer
|
|
|
- ) {
|
|
|
- BookmarkGroupHeaderContent(
|
|
|
- title = headerKey.bookName,
|
|
|
- subtitle = headerKey.bookAuthor,
|
|
|
- isCollapsed = isCollapsed,
|
|
|
- onToggle = {
|
|
|
- onIntent(AllBookmarkIntent.ToggleGroupCollapse(headerKey))
|
|
|
- },
|
|
|
- isMiuix = isMiuix
|
|
|
- )
|
|
|
-
|
|
|
- AnimatedVisibility(
|
|
|
- visible = !isCollapsed && bookmarks.isNotEmpty()
|
|
|
- ) {
|
|
|
- Column() {
|
|
|
- HorizontalDivider(
|
|
|
- color = LegadoTheme.colorScheme.surface
|
|
|
- )
|
|
|
- bookmarks.forEach { bookmarkUi ->
|
|
|
- BookmarkItem(
|
|
|
- bookmark = bookmarkUi.rawBookmark,
|
|
|
- modifier = Modifier.fillMaxWidth(),
|
|
|
- isDur = false,
|
|
|
- onClick = {
|
|
|
- editingBookmark = bookmarkUi.rawBookmark
|
|
|
- showBottomSheet = true
|
|
|
- },
|
|
|
- onLongClick = {
|
|
|
- editingBookmark = bookmarkUi.rawBookmark
|
|
|
- showBottomSheet = true
|
|
|
- }
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- TopFloatingStickyItem(
|
|
|
- item = stickyGroup,
|
|
|
- modifier = Modifier
|
|
|
- .padding(
|
|
|
- top = paddingValues.calculateTopPadding() + 4.dp,
|
|
|
- start = 8.dp
|
|
|
- )
|
|
|
- ) { group ->
|
|
|
- TextCard(
|
|
|
- text = group.bookName,
|
|
|
- textStyle = LegadoTheme.typography.labelLarge,
|
|
|
- cornerRadius = 8.dp,
|
|
|
- horizontalPadding = 8.dp,
|
|
|
- verticalPadding = 6.dp,
|
|
|
- onClick = {
|
|
|
- scope.launch {
|
|
|
- val index =
|
|
|
- bookmarkGroups.indexOfFirst { it.key == group }
|
|
|
- if (index >= 0) {
|
|
|
- listState.animateScrollToItem(index)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
- BookmarkEditSheet(
|
|
|
- show = showBottomSheet && editingBookmark != null,
|
|
|
- bookmark = editingBookmark ?: Bookmark(),
|
|
|
- onDismiss = {
|
|
|
- showBottomSheet = false
|
|
|
- editingBookmark = null
|
|
|
- },
|
|
|
- onSave = { updatedBookmark ->
|
|
|
- onIntent(AllBookmarkIntent.UpdateBookmark(updatedBookmark))
|
|
|
- showBottomSheet = false
|
|
|
- },
|
|
|
- onDelete = { bookmarkToDelete ->
|
|
|
- onIntent(AllBookmarkIntent.DeleteBookmark(bookmarkToDelete))
|
|
|
- showBottomSheet = false
|
|
|
+@Composable
|
|
|
+private fun BookOverview(
|
|
|
+ books: List<BookmarkBookUi>,
|
|
|
+ listState: androidx.compose.foundation.lazy.LazyListState,
|
|
|
+ topPadding: androidx.compose.ui.unit.Dp,
|
|
|
+ onBookClick: (BookmarkBookUi) -> Unit,
|
|
|
+) {
|
|
|
+ LazyColumn(
|
|
|
+ state = listState,
|
|
|
+ modifier = Modifier.fillMaxSize(),
|
|
|
+ contentPadding = adaptiveContentPadding(top = topPadding, bottom = 120.dp),
|
|
|
+ verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
|
+ ) {
|
|
|
+ items(books, key = { "${it.key.name}|${it.key.author}" }) { book ->
|
|
|
+ GlassCard(
|
|
|
+ modifier = Modifier.fillMaxWidth(),
|
|
|
+ cornerRadius = 16.dp,
|
|
|
+ containerColor = LegadoTheme.colorScheme.surfaceContainer,
|
|
|
+ onClick = { onBookClick(book) },
|
|
|
+ ) {
|
|
|
+ Row(
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxWidth()
|
|
|
+ .padding(12.dp),
|
|
|
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
|
+ verticalAlignment = Alignment.CenterVertically,
|
|
|
+ ) {
|
|
|
+ Column(modifier = Modifier.weight(1f)) {
|
|
|
+ AppText(
|
|
|
+ text = book.key.name,
|
|
|
+ style = LegadoTheme.typography.titleMediumEmphasized,
|
|
|
+ maxLines = 2,
|
|
|
+ overflow = TextOverflow.Ellipsis,
|
|
|
+ )
|
|
|
+ if (book.key.author.isNotBlank()) AppText(
|
|
|
+ text = book.key.author,
|
|
|
+ style = LegadoTheme.typography.labelMediumEmphasized,
|
|
|
+ color = LegadoTheme.colorScheme.onSurfaceVariant,
|
|
|
+ )
|
|
|
+ AppText(
|
|
|
+ text = stringResource(
|
|
|
+ R.string.feature_bookmarks_counts,
|
|
|
+ book.bookmarkCount,
|
|
|
+ book.markingCount,
|
|
|
+ ),
|
|
|
+ style = LegadoTheme.typography.labelSmall,
|
|
|
+ color = LegadoTheme.colorScheme.onSurfaceVariant,
|
|
|
+ )
|
|
|
+ if (book.matchedMarkingId != null) AppText(
|
|
|
+ text = stringResource(R.string.feature_bookmarks_match_hint),
|
|
|
+ style = LegadoTheme.typography.labelSmall,
|
|
|
+ color = LegadoTheme.colorScheme.primary,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ CoilBookCover(
|
|
|
+ name = book.key.name,
|
|
|
+ author = book.key.author,
|
|
|
+ path = book.coverPath,
|
|
|
+ sourceOrigin = book.sourceOrigin,
|
|
|
+ modifier = Modifier.width(52.dp),
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
- )
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+@OptIn(ExperimentalFoundationApi::class)
|
|
|
@Composable
|
|
|
-private fun BookmarkGroupHeaderContent(
|
|
|
- modifier: Modifier = Modifier,
|
|
|
- title: String,
|
|
|
- subtitle: String?,
|
|
|
- isCollapsed: Boolean,
|
|
|
- onToggle: () -> Unit,
|
|
|
- isMiuix: Boolean
|
|
|
+private fun DetailList(
|
|
|
+ items: List<BookmarkDetailItemUi>,
|
|
|
+ listState: androidx.compose.foundation.lazy.LazyListState,
|
|
|
+ topPadding: androidx.compose.ui.unit.Dp,
|
|
|
+ onIntent: (AllBookmarkIntent) -> Unit,
|
|
|
) {
|
|
|
+ var editingBookmark by remember { mutableStateOf<Bookmark?>(null) }
|
|
|
+ LazyColumn(
|
|
|
+ state = listState,
|
|
|
+ modifier = Modifier.fillMaxSize(),
|
|
|
+ contentPadding = adaptiveContentPadding(top = topPadding, bottom = 120.dp),
|
|
|
+ verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
|
+ ) {
|
|
|
+ items(items, key = { it.stableId }, contentType = { it::class }) { item ->
|
|
|
+ GlassCard(
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxWidth()
|
|
|
+ .animateItem(),
|
|
|
+ cornerRadius = 14.dp,
|
|
|
+ containerColor = LegadoTheme.colorScheme.surfaceContainer,
|
|
|
+ onClick = {
|
|
|
+ if (item is BookmarkDetailItemUi.SavedBookmark) editingBookmark = item.bookmark
|
|
|
+ },
|
|
|
+ onLongClick = {
|
|
|
+ if (item is BookmarkDetailItemUi.SavedBookmark) editingBookmark = item.bookmark
|
|
|
+ }
|
|
|
+ ) {
|
|
|
+ Column(
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxWidth()
|
|
|
+ .padding(16.dp),
|
|
|
+ verticalArrangement = Arrangement.spacedBy(8.dp)
|
|
|
+ ) {
|
|
|
+ AppText(
|
|
|
+ text = item.chapterName.ifBlank {
|
|
|
+ stringResource(
|
|
|
+ R.string.feature_bookmarks_chapter_fallback,
|
|
|
+ item.chapterIndex + 1
|
|
|
+ )
|
|
|
+ },
|
|
|
+ style = LegadoTheme.typography.labelLarge,
|
|
|
+ color = LegadoTheme.colorScheme.primary,
|
|
|
+ maxLines = 1,
|
|
|
+ overflow = TextOverflow.Ellipsis,
|
|
|
+ )
|
|
|
+ when (item) {
|
|
|
+ is BookmarkDetailItemUi.SavedBookmark -> {
|
|
|
+ AppText(
|
|
|
+ text = item.bookmark.bookText,
|
|
|
+ style = LegadoTheme.typography.labelMedium,
|
|
|
+ maxLines = DETAIL_TEXT_MAX_LINES,
|
|
|
+ overflow = TextOverflow.Ellipsis,
|
|
|
+ )
|
|
|
+ if (item.bookmark.content.isNotBlank()) AppText(
|
|
|
+ text = item.bookmark.content,
|
|
|
+ style = LegadoTheme.typography.labelMedium,
|
|
|
+ color = LegadoTheme.colorScheme.onSurfaceVariant,
|
|
|
+ maxLines = DETAIL_TEXT_MAX_LINES,
|
|
|
+ overflow = TextOverflow.Ellipsis,
|
|
|
+ )
|
|
|
+ }
|
|
|
|
|
|
- val contentColor by animateColorAsState(
|
|
|
- if (isMiuix) MiuixTheme.colorScheme.primary else MaterialTheme.colorScheme.primary,
|
|
|
- animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing),
|
|
|
- label = "CardColor"
|
|
|
- )
|
|
|
- val headerDescription = listOfNotNull(title, subtitle).joinToString()
|
|
|
- val headerStateDescription = stringResource(
|
|
|
- if (isCollapsed) R.string.a11y_collapsed else R.string.a11y_expanded
|
|
|
- )
|
|
|
- val clickLabel = stringResource(
|
|
|
- if (isCollapsed) R.string.expand else R.string.collapse
|
|
|
- )
|
|
|
-
|
|
|
- ListItem(
|
|
|
- modifier = modifier
|
|
|
- .fillMaxWidth()
|
|
|
- .semantics {
|
|
|
- role = Role.Button
|
|
|
- contentDescription = headerDescription
|
|
|
- stateDescription = headerStateDescription
|
|
|
- onClick(label = clickLabel, action = null)
|
|
|
- }
|
|
|
- .combinedClickable(onClick = onToggle),
|
|
|
- colors = ListItemDefaults.colors(
|
|
|
- containerColor = Color.Transparent,
|
|
|
- supportingColor = LegadoTheme.colorScheme.onSurfaceVariant,
|
|
|
- trailingIconColor = LegadoTheme.colorScheme.onSurfaceVariant
|
|
|
- ),
|
|
|
- supportingContent = {
|
|
|
- subtitle?.let {
|
|
|
- AppText(
|
|
|
- text = it,
|
|
|
- style = LegadoTheme.typography.labelMedium,
|
|
|
- color = LegadoTheme.colorScheme.onSurfaceVariant
|
|
|
- )
|
|
|
+ is BookmarkDetailItemUi.Marking -> {
|
|
|
+ AppText(
|
|
|
+ text = item.selectedText,
|
|
|
+ style = LegadoTheme.typography.labelMedium,
|
|
|
+ maxLines = DETAIL_TEXT_MAX_LINES,
|
|
|
+ overflow = TextOverflow.Ellipsis,
|
|
|
+ )
|
|
|
+ if (item.marking.note.isNotBlank()) AppText(
|
|
|
+ text = item.marking.note,
|
|
|
+ style = LegadoTheme.typography.labelMedium,
|
|
|
+ color = LegadoTheme.colorScheme.onSurfaceVariant,
|
|
|
+ maxLines = DETAIL_TEXT_MAX_LINES,
|
|
|
+ overflow = TextOverflow.Ellipsis,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- ) {
|
|
|
- AppText(
|
|
|
- text = title,
|
|
|
- style = LegadoTheme.typography.titleMedium,
|
|
|
- color = contentColor
|
|
|
- )
|
|
|
}
|
|
|
+ BookmarkEditSheet(
|
|
|
+ show = editingBookmark != null,
|
|
|
+ bookmark = editingBookmark ?: Bookmark(),
|
|
|
+ onDismiss = { editingBookmark = null },
|
|
|
+ onSave = { onIntent(AllBookmarkIntent.UpdateBookmark(it)); editingBookmark = null },
|
|
|
+ onDelete = { onIntent(AllBookmarkIntent.DeleteBookmark(it)); editingBookmark = null },
|
|
|
+ )
|
|
|
}
|