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:
Aryan 2026-03-16 17:42:42 +05:30 committed by GitHub
parent 1884ace646
commit d6f7509e81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 338 additions and 37 deletions

View file

@ -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))
}
}
}