Library update (#78)
* Added print functionality to PDF viewer * feat(pdf): add interactive button support and visibility reveal heuristic * Moved search state management to MainViewModel and improved search UI interaction. * Updated `pdfiumandroid` to version 2.0.0 and handled resulting API changes, including nullable page/text page returns and revised native pointer access. increased compileSdk to 36. * Fixed Table of Contents (TOC) truncation bug and improved the TOC UI in `PdfViewerScreen`. - Implemented `getFixedTableOfContents` using reflection to bypass a library issue where sibling nodes were incorrectly truncated during traversal. - Enhanced the TOC drawer with a nested, expandable tree structure using the new `PdfTocTreeItem` component. - Added a custom `VerticalScrollbar` with draggable support for better navigation within long TOC lists. - Integrated `animateColorAsState` and `animateFloatAsState` for smoother UI transitions in the TOC and scrollbar. - Optimized TOC loading by flattening the tree structure and managing expansion states with `rememberSaveable`. * Improved zoom pivot calculation and interaction handling in PdfVerticalReader * Fixed high-res PDF tile bleeding by implementing clipRect in PdfBitmapLayer * fix(pdf): resolve zoom stuttering, in pagination mode, by removing eager scale snapping * Added book pinning and library filtering functionality * Updated folder sync logic, improved metadata extraction, and added persistent banner message while syncing * build: bump version to 1.0.37 (38)
This commit is contained in:
parent
1884ace646
commit
d6f7509e81
7 changed files with 338 additions and 37 deletions
|
|
@ -27,6 +27,15 @@ import androidx.compose.foundation.BorderStroke
|
|||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.material.icons.filled.PushPin
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -140,6 +149,7 @@ fun LibraryScreen(
|
|||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
var showFilterSheet by remember { mutableStateOf(false) }
|
||||
|
||||
val isSearchActive = uiState.isSearchActive
|
||||
val searchQuery = uiState.searchQuery
|
||||
|
|
@ -222,11 +232,13 @@ fun LibraryScreen(
|
|||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LibraryScreenContent(
|
||||
recentFiles = uiState.recentFiles,
|
||||
recentFiles = uiState.allRecentFiles,
|
||||
shelves = shelves,
|
||||
selectedItems = selectedItems,
|
||||
selectedShelves = selectedShelves,
|
||||
sortOrder = sortOrder,
|
||||
libraryFilters = uiState.libraryFilters,
|
||||
pinnedLibraryBookIds = uiState.pinnedLibraryBookIds,
|
||||
pagerState = pagerState,
|
||||
scope = scope,
|
||||
searchQuery = searchQuery,
|
||||
|
|
@ -234,6 +246,10 @@ fun LibraryScreen(
|
|||
onSearchQueryChange = viewModel::onSearchQueryChange,
|
||||
onSearchActiveChange = viewModel::setSearchActive,
|
||||
onSortOrderChange = viewModel::setSortOrder,
|
||||
onFilterClick = { showFilterSheet = true },
|
||||
onClearFilters = { viewModel.updateLibraryFilters(LibraryFilters()) },
|
||||
onRemoveFilter = { viewModel.updateLibraryFilters(it) },
|
||||
onPinClick = { viewModel.togglePinForContextualItems(isHome = false) },
|
||||
onClearSelection = { viewModel.clearContextualAction() },
|
||||
onItemClick = viewModel::onRecentFileClicked,
|
||||
onItemLongClick = viewModel::onRecentItemLongPress,
|
||||
|
|
@ -260,7 +276,8 @@ fun LibraryScreen(
|
|||
onDisconnectSyncFolderClick = viewModel::disconnectAllSyncedFolders,
|
||||
downloadingBookIds = uiState.downloadingBookIds,
|
||||
lastFolderScanTime = uiState.lastFolderScanTime,
|
||||
isLoading = uiState.isLoading
|
||||
isLoading = uiState.isLoading,
|
||||
isRefreshing = uiState.isRefreshing,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -284,6 +301,15 @@ fun LibraryScreen(
|
|||
)
|
||||
}
|
||||
|
||||
if (showFilterSheet) {
|
||||
LibraryFilterSheet(
|
||||
filters = uiState.libraryFilters,
|
||||
syncedFolders = uiState.syncedFolders,
|
||||
onApply = { viewModel.updateLibraryFilters(it) },
|
||||
onDismiss = { showFilterSheet = false }
|
||||
)
|
||||
}
|
||||
|
||||
if (showDeleteShelvesDialog) {
|
||||
DeleteShelvesConfirmationDialog(
|
||||
count = selectedShelves.size,
|
||||
|
|
@ -421,6 +447,7 @@ fun ShelfScreen(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun LibraryScreenContent(
|
||||
|
|
@ -429,6 +456,8 @@ fun LibraryScreenContent(
|
|||
selectedItems: Set<RecentFileItem>,
|
||||
selectedShelves: Set<String>,
|
||||
sortOrder: SortOrder,
|
||||
libraryFilters: LibraryFilters,
|
||||
pinnedLibraryBookIds: Set<String>,
|
||||
pagerState: PagerState,
|
||||
scope: CoroutineScope,
|
||||
searchQuery: String,
|
||||
|
|
@ -436,6 +465,10 @@ fun LibraryScreenContent(
|
|||
onSearchQueryChange: (String) -> Unit,
|
||||
onSearchActiveChange: (Boolean) -> Unit,
|
||||
onSortOrderChange: (SortOrder) -> Unit,
|
||||
onFilterClick: () -> Unit,
|
||||
onClearFilters: () -> Unit,
|
||||
onRemoveFilter: (LibraryFilters) -> Unit,
|
||||
onPinClick: () -> Unit,
|
||||
onClearSelection: () -> Unit,
|
||||
onItemClick: (RecentFileItem) -> Unit,
|
||||
onItemLongClick: (RecentFileItem) -> Unit,
|
||||
|
|
@ -455,6 +488,7 @@ fun LibraryScreenContent(
|
|||
downloadingBookIds: Set<String>,
|
||||
lastFolderScanTime: Long?,
|
||||
isLoading: Boolean,
|
||||
isRefreshing: Boolean,
|
||||
syncedFolders: List<SyncedFolder>,
|
||||
onAddFolderClick: (android.net.Uri) -> Unit,
|
||||
onRemoveFolderClick: (SyncedFolder) -> Unit,
|
||||
|
|
@ -492,6 +526,7 @@ fun LibraryScreenContent(
|
|||
ContextualTopAppBar(
|
||||
selectedItemCount = selectedItems.size,
|
||||
onNavIconClick = onClearSelection,
|
||||
onPinClick = onPinClick,
|
||||
onInfoClick = onInfoClick,
|
||||
onDeleteClick = onDeleteClick,
|
||||
onSelectAllClick = onSelectAllClick
|
||||
|
|
@ -550,6 +585,9 @@ fun LibraryScreenContent(
|
|||
title = { Text("Library") },
|
||||
actions = {
|
||||
if (pagerState.currentPage == 0) {
|
||||
IconButton(onClick = onFilterClick) {
|
||||
Icon(Icons.Default.FilterList, contentDescription = "Filter")
|
||||
}
|
||||
Box {
|
||||
TextButton(onClick = { showSortMenu = true }) {
|
||||
Icon(
|
||||
|
|
@ -600,6 +638,40 @@ fun LibraryScreenContent(
|
|||
)
|
||||
}
|
||||
}
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
visible = libraryFilters.isActive && pagerState.currentPage == 0
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
if (libraryFilters.fileTypes.isNotEmpty()) {
|
||||
AssistChip(
|
||||
onClick = { onRemoveFilter(libraryFilters.copy(fileTypes = emptySet())) },
|
||||
label = { Text("Types: ${libraryFilters.fileTypes.joinToString { it.name }}") },
|
||||
trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) }
|
||||
)
|
||||
}
|
||||
if (libraryFilters.sourceFolders.isNotEmpty()) {
|
||||
AssistChip(
|
||||
onClick = { onRemoveFilter(libraryFilters.copy(sourceFolders = emptySet())) },
|
||||
label = { Text("Folders: ${libraryFilters.sourceFolders.size}") },
|
||||
trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) }
|
||||
)
|
||||
}
|
||||
if (libraryFilters.readStatus != ReadStatusFilter.ALL) {
|
||||
AssistChip(
|
||||
onClick = { onRemoveFilter(libraryFilters.copy(readStatus = ReadStatusFilter.ALL)) },
|
||||
label = { Text("Status: ${libraryFilters.readStatus.displayName}") },
|
||||
trailingIcon = { Icon(Icons.Default.Close, contentDescription = "Clear", modifier = Modifier.size(16.dp)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -658,6 +730,7 @@ fun LibraryScreenContent(
|
|||
LibraryListItem(
|
||||
item = item,
|
||||
isSelected = selectedItems.any { it.bookId == item.bookId },
|
||||
isPinned = item.bookId in pinnedLibraryBookIds,
|
||||
onItemClick = { onItemClick(item) },
|
||||
onItemLongClick = { onItemLongClick(item) },
|
||||
isDownloading = item.bookId in downloadingBookIds
|
||||
|
|
@ -681,7 +754,7 @@ fun LibraryScreenContent(
|
|||
onRemoveFolderClick = onRemoveFolderClick,
|
||||
onScanNowClick = onScanNowClick,
|
||||
onSyncMetadataClick = onSyncMetadataClick,
|
||||
isLoading = isLoading
|
||||
isLoading = isLoading || isRefreshing
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1159,6 +1232,7 @@ private fun ShelfListItem(
|
|||
private fun LibraryListItem(
|
||||
item: RecentFileItem,
|
||||
isSelected: Boolean,
|
||||
isPinned: Boolean = false,
|
||||
onItemClick: () -> Unit,
|
||||
onItemLongClick: () -> Unit,
|
||||
isDownloading: Boolean,
|
||||
|
|
@ -1217,6 +1291,16 @@ private fun LibraryListItem(
|
|||
Spacer(modifier = Modifier.width(4.dp))
|
||||
}
|
||||
|
||||
if (isPinned) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PushPin,
|
||||
contentDescription = "Pinned",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
}
|
||||
|
||||
Text(
|
||||
text = item.title ?: item.displayName,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
|
|
@ -1555,4 +1639,91 @@ private fun FolderCard(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LibraryFilterSheet(
|
||||
filters: LibraryFilters,
|
||||
syncedFolders: List<SyncedFolder>,
|
||||
onApply: (LibraryFilters) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
var currentFilters by remember { mutableStateOf(filters) }
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text("Filter Library", style = MaterialTheme.typography.titleLarge)
|
||||
|
||||
Text("File Type", style = MaterialTheme.typography.titleMedium)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
FileType.entries.forEach { type ->
|
||||
FilterChip(
|
||||
selected = type in currentFilters.fileTypes,
|
||||
onClick = {
|
||||
val newSet = if (type in currentFilters.fileTypes) currentFilters.fileTypes - type else currentFilters.fileTypes + type
|
||||
currentFilters = currentFilters.copy(fileTypes = newSet)
|
||||
},
|
||||
label = { Text(type.name) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (syncedFolders.isNotEmpty()) {
|
||||
Text("Source Folder", style = MaterialTheme.typography.titleMedium)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
syncedFolders.forEach { folder ->
|
||||
FilterChip(
|
||||
selected = folder.uriString in currentFilters.sourceFolders,
|
||||
onClick = {
|
||||
val newSet = if (folder.uriString in currentFilters.sourceFolders) currentFilters.sourceFolders - folder.uriString else currentFilters.sourceFolders + folder.uriString
|
||||
currentFilters = currentFilters.copy(sourceFolders = newSet)
|
||||
},
|
||||
label = { Text(folder.name) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text("Read Status", style = MaterialTheme.typography.titleMedium)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ReadStatusFilter.entries.forEach { status ->
|
||||
FilterChip(
|
||||
selected = currentFilters.readStatus == status,
|
||||
onClick = { currentFilters = currentFilters.copy(readStatus = status) },
|
||||
label = { Text(status.displayName) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(top = 16.dp), horizontalArrangement = Arrangement.End) {
|
||||
TextButton(onClick = { currentFilters = LibraryFilters() }) {
|
||||
Text("Clear All")
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
androidx.compose.material3.Button(onClick = { onApply(currentFilters); onDismiss() }) {
|
||||
Text("Apply")
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue