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
|
|
@ -26,8 +26,8 @@ android {
|
||||||
applicationId = "com.aryan.reader"
|
applicationId = "com.aryan.reader"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 37
|
versionCode = 38
|
||||||
versionName = "1.0.36"
|
versionName = "1.0.37"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.Menu
|
import androidx.compose.material.icons.filled.Menu
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.PhoneAndroid
|
import androidx.compose.material.icons.filled.PhoneAndroid
|
||||||
|
import androidx.compose.material.icons.filled.PushPin
|
||||||
import androidx.compose.material.icons.filled.VerifiedUser
|
import androidx.compose.material.icons.filled.VerifiedUser
|
||||||
import androidx.compose.material.icons.outlined.AccountCircle
|
import androidx.compose.material.icons.outlined.AccountCircle
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
|
@ -187,11 +188,13 @@ fun HomeScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(uiState.bannerMessage) {
|
LaunchedEffect(uiState.bannerMessage) {
|
||||||
if (uiState.bannerMessage != null) {
|
uiState.bannerMessage?.let { msg ->
|
||||||
|
if (!msg.isPersistent) {
|
||||||
delay(3000L)
|
delay(3000L)
|
||||||
viewModel.bannerMessageShown()
|
viewModel.bannerMessageShown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(uiState.errorMessage) {
|
LaunchedEffect(uiState.errorMessage) {
|
||||||
uiState.errorMessage?.let { message ->
|
uiState.errorMessage?.let { message ->
|
||||||
|
|
@ -304,6 +307,7 @@ fun HomeScreen(
|
||||||
ContextualTopAppBar(
|
ContextualTopAppBar(
|
||||||
selectedItemCount = selectedContextItems.size,
|
selectedItemCount = selectedContextItems.size,
|
||||||
onNavIconClick = { viewModel.clearContextualAction() },
|
onNavIconClick = { viewModel.clearContextualAction() },
|
||||||
|
onPinClick = { viewModel.togglePinForContextualItems(isHome = true) },
|
||||||
onDeleteClick = { showDeleteConfirmDialog = true },
|
onDeleteClick = { showDeleteConfirmDialog = true },
|
||||||
onSelectAllClick = { viewModel.selectAllRecentFiles() })
|
onSelectAllClick = { viewModel.selectAllRecentFiles() })
|
||||||
}
|
}
|
||||||
|
|
@ -336,6 +340,7 @@ fun HomeScreen(
|
||||||
RecentFilesContent(
|
RecentFilesContent(
|
||||||
recentFiles = recentFilesForHome,
|
recentFiles = recentFilesForHome,
|
||||||
selectedContextItems = selectedContextItems,
|
selectedContextItems = selectedContextItems,
|
||||||
|
pinnedHomeBookIds = uiState.pinnedHomeBookIds,
|
||||||
onItemClick = { item -> viewModel.onRecentFileClicked(item) },
|
onItemClick = { item -> viewModel.onRecentFileClicked(item) },
|
||||||
onItemLongClick = { item -> viewModel.onRecentItemLongPress(item) },
|
onItemLongClick = { item -> viewModel.onRecentItemLongPress(item) },
|
||||||
onSelectFileClick = onSelectFileClick,
|
onSelectFileClick = onSelectFileClick,
|
||||||
|
|
@ -435,6 +440,7 @@ fun HomeScreen(
|
||||||
private fun RecentFilesContent(
|
private fun RecentFilesContent(
|
||||||
recentFiles: List<RecentFileItem>,
|
recentFiles: List<RecentFileItem>,
|
||||||
selectedContextItems: Collection<RecentFileItem>,
|
selectedContextItems: Collection<RecentFileItem>,
|
||||||
|
pinnedHomeBookIds: Set<String>,
|
||||||
onItemClick: (RecentFileItem) -> Unit,
|
onItemClick: (RecentFileItem) -> Unit,
|
||||||
onItemLongClick: (RecentFileItem) -> Unit,
|
onItemLongClick: (RecentFileItem) -> Unit,
|
||||||
onSelectFileClick: () -> Unit,
|
onSelectFileClick: () -> Unit,
|
||||||
|
|
@ -456,6 +462,7 @@ private fun RecentFilesContent(
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
recentFiles = recentFiles,
|
recentFiles = recentFiles,
|
||||||
selectedItemUris = selectedContextItems.mapNotNull { it.uriString }.toSet(),
|
selectedItemUris = selectedContextItems.mapNotNull { it.uriString }.toSet(),
|
||||||
|
pinnedHomeBookIds = pinnedHomeBookIds,
|
||||||
onItemClick = onItemClick,
|
onItemClick = onItemClick,
|
||||||
onItemLongClick = onItemLongClick,
|
onItemLongClick = onItemLongClick,
|
||||||
windowSizeClass = windowSizeClass,
|
windowSizeClass = windowSizeClass,
|
||||||
|
|
@ -498,6 +505,7 @@ private fun RecentFilesContent(
|
||||||
private fun RecentFilesGrid(
|
private fun RecentFilesGrid(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
recentFiles: List<RecentFileItem>,
|
recentFiles: List<RecentFileItem>,
|
||||||
|
pinnedHomeBookIds: Set<String>,
|
||||||
selectedItemUris: Set<String>,
|
selectedItemUris: Set<String>,
|
||||||
onItemClick: (RecentFileItem) -> Unit,
|
onItemClick: (RecentFileItem) -> Unit,
|
||||||
onItemLongClick: (RecentFileItem) -> Unit,
|
onItemLongClick: (RecentFileItem) -> Unit,
|
||||||
|
|
@ -527,6 +535,7 @@ private fun RecentFilesGrid(
|
||||||
RecentFileCard(
|
RecentFileCard(
|
||||||
item = item,
|
item = item,
|
||||||
isSelected = item.uriString in selectedItemUris,
|
isSelected = item.uriString in selectedItemUris,
|
||||||
|
isPinned = item.bookId in pinnedHomeBookIds,
|
||||||
onClick = { onItemClick(item) },
|
onClick = { onItemClick(item) },
|
||||||
onLongClick = { onItemLongClick(item) },
|
onLongClick = { onItemLongClick(item) },
|
||||||
isDownloading = item.bookId in downloadingBookIds
|
isDownloading = item.bookId in downloadingBookIds
|
||||||
|
|
@ -541,9 +550,10 @@ private fun RecentFilesGrid(
|
||||||
fun RecentFileCard(
|
fun RecentFileCard(
|
||||||
item: RecentFileItem,
|
item: RecentFileItem,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isPinned: Boolean = false,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
isDownloading: Boolean,
|
isDownloading: Boolean,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
@ -598,6 +608,26 @@ fun RecentFileCard(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isPinned) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
.padding(8.dp)
|
||||||
|
.background(
|
||||||
|
color = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
.padding(4.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PushPin,
|
||||||
|
contentDescription = "Pinned",
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!item.isAvailable) {
|
if (!item.isAvailable) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,15 @@ import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -140,6 +149,7 @@ fun LibraryScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
var showFilterSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val isSearchActive = uiState.isSearchActive
|
val isSearchActive = uiState.isSearchActive
|
||||||
val searchQuery = uiState.searchQuery
|
val searchQuery = uiState.searchQuery
|
||||||
|
|
@ -222,11 +232,13 @@ fun LibraryScreen(
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
LibraryScreenContent(
|
LibraryScreenContent(
|
||||||
recentFiles = uiState.recentFiles,
|
recentFiles = uiState.allRecentFiles,
|
||||||
shelves = shelves,
|
shelves = shelves,
|
||||||
selectedItems = selectedItems,
|
selectedItems = selectedItems,
|
||||||
selectedShelves = selectedShelves,
|
selectedShelves = selectedShelves,
|
||||||
sortOrder = sortOrder,
|
sortOrder = sortOrder,
|
||||||
|
libraryFilters = uiState.libraryFilters,
|
||||||
|
pinnedLibraryBookIds = uiState.pinnedLibraryBookIds,
|
||||||
pagerState = pagerState,
|
pagerState = pagerState,
|
||||||
scope = scope,
|
scope = scope,
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
|
|
@ -234,6 +246,10 @@ fun LibraryScreen(
|
||||||
onSearchQueryChange = viewModel::onSearchQueryChange,
|
onSearchQueryChange = viewModel::onSearchQueryChange,
|
||||||
onSearchActiveChange = viewModel::setSearchActive,
|
onSearchActiveChange = viewModel::setSearchActive,
|
||||||
onSortOrderChange = viewModel::setSortOrder,
|
onSortOrderChange = viewModel::setSortOrder,
|
||||||
|
onFilterClick = { showFilterSheet = true },
|
||||||
|
onClearFilters = { viewModel.updateLibraryFilters(LibraryFilters()) },
|
||||||
|
onRemoveFilter = { viewModel.updateLibraryFilters(it) },
|
||||||
|
onPinClick = { viewModel.togglePinForContextualItems(isHome = false) },
|
||||||
onClearSelection = { viewModel.clearContextualAction() },
|
onClearSelection = { viewModel.clearContextualAction() },
|
||||||
onItemClick = viewModel::onRecentFileClicked,
|
onItemClick = viewModel::onRecentFileClicked,
|
||||||
onItemLongClick = viewModel::onRecentItemLongPress,
|
onItemLongClick = viewModel::onRecentItemLongPress,
|
||||||
|
|
@ -260,7 +276,8 @@ fun LibraryScreen(
|
||||||
onDisconnectSyncFolderClick = viewModel::disconnectAllSyncedFolders,
|
onDisconnectSyncFolderClick = viewModel::disconnectAllSyncedFolders,
|
||||||
downloadingBookIds = uiState.downloadingBookIds,
|
downloadingBookIds = uiState.downloadingBookIds,
|
||||||
lastFolderScanTime = uiState.lastFolderScanTime,
|
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) {
|
if (showDeleteShelvesDialog) {
|
||||||
DeleteShelvesConfirmationDialog(
|
DeleteShelvesConfirmationDialog(
|
||||||
count = selectedShelves.size,
|
count = selectedShelves.size,
|
||||||
|
|
@ -421,6 +447,7 @@ fun ShelfScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryScreenContent(
|
fun LibraryScreenContent(
|
||||||
|
|
@ -429,6 +456,8 @@ fun LibraryScreenContent(
|
||||||
selectedItems: Set<RecentFileItem>,
|
selectedItems: Set<RecentFileItem>,
|
||||||
selectedShelves: Set<String>,
|
selectedShelves: Set<String>,
|
||||||
sortOrder: SortOrder,
|
sortOrder: SortOrder,
|
||||||
|
libraryFilters: LibraryFilters,
|
||||||
|
pinnedLibraryBookIds: Set<String>,
|
||||||
pagerState: PagerState,
|
pagerState: PagerState,
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
searchQuery: String,
|
searchQuery: String,
|
||||||
|
|
@ -436,6 +465,10 @@ fun LibraryScreenContent(
|
||||||
onSearchQueryChange: (String) -> Unit,
|
onSearchQueryChange: (String) -> Unit,
|
||||||
onSearchActiveChange: (Boolean) -> Unit,
|
onSearchActiveChange: (Boolean) -> Unit,
|
||||||
onSortOrderChange: (SortOrder) -> Unit,
|
onSortOrderChange: (SortOrder) -> Unit,
|
||||||
|
onFilterClick: () -> Unit,
|
||||||
|
onClearFilters: () -> Unit,
|
||||||
|
onRemoveFilter: (LibraryFilters) -> Unit,
|
||||||
|
onPinClick: () -> Unit,
|
||||||
onClearSelection: () -> Unit,
|
onClearSelection: () -> Unit,
|
||||||
onItemClick: (RecentFileItem) -> Unit,
|
onItemClick: (RecentFileItem) -> Unit,
|
||||||
onItemLongClick: (RecentFileItem) -> Unit,
|
onItemLongClick: (RecentFileItem) -> Unit,
|
||||||
|
|
@ -455,6 +488,7 @@ fun LibraryScreenContent(
|
||||||
downloadingBookIds: Set<String>,
|
downloadingBookIds: Set<String>,
|
||||||
lastFolderScanTime: Long?,
|
lastFolderScanTime: Long?,
|
||||||
isLoading: Boolean,
|
isLoading: Boolean,
|
||||||
|
isRefreshing: Boolean,
|
||||||
syncedFolders: List<SyncedFolder>,
|
syncedFolders: List<SyncedFolder>,
|
||||||
onAddFolderClick: (android.net.Uri) -> Unit,
|
onAddFolderClick: (android.net.Uri) -> Unit,
|
||||||
onRemoveFolderClick: (SyncedFolder) -> Unit,
|
onRemoveFolderClick: (SyncedFolder) -> Unit,
|
||||||
|
|
@ -492,6 +526,7 @@ fun LibraryScreenContent(
|
||||||
ContextualTopAppBar(
|
ContextualTopAppBar(
|
||||||
selectedItemCount = selectedItems.size,
|
selectedItemCount = selectedItems.size,
|
||||||
onNavIconClick = onClearSelection,
|
onNavIconClick = onClearSelection,
|
||||||
|
onPinClick = onPinClick,
|
||||||
onInfoClick = onInfoClick,
|
onInfoClick = onInfoClick,
|
||||||
onDeleteClick = onDeleteClick,
|
onDeleteClick = onDeleteClick,
|
||||||
onSelectAllClick = onSelectAllClick
|
onSelectAllClick = onSelectAllClick
|
||||||
|
|
@ -550,6 +585,9 @@ fun LibraryScreenContent(
|
||||||
title = { Text("Library") },
|
title = { Text("Library") },
|
||||||
actions = {
|
actions = {
|
||||||
if (pagerState.currentPage == 0) {
|
if (pagerState.currentPage == 0) {
|
||||||
|
IconButton(onClick = onFilterClick) {
|
||||||
|
Icon(Icons.Default.FilterList, contentDescription = "Filter")
|
||||||
|
}
|
||||||
Box {
|
Box {
|
||||||
TextButton(onClick = { showSortMenu = true }) {
|
TextButton(onClick = { showSortMenu = true }) {
|
||||||
Icon(
|
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(
|
LibraryListItem(
|
||||||
item = item,
|
item = item,
|
||||||
isSelected = selectedItems.any { it.bookId == item.bookId },
|
isSelected = selectedItems.any { it.bookId == item.bookId },
|
||||||
|
isPinned = item.bookId in pinnedLibraryBookIds,
|
||||||
onItemClick = { onItemClick(item) },
|
onItemClick = { onItemClick(item) },
|
||||||
onItemLongClick = { onItemLongClick(item) },
|
onItemLongClick = { onItemLongClick(item) },
|
||||||
isDownloading = item.bookId in downloadingBookIds
|
isDownloading = item.bookId in downloadingBookIds
|
||||||
|
|
@ -681,7 +754,7 @@ fun LibraryScreenContent(
|
||||||
onRemoveFolderClick = onRemoveFolderClick,
|
onRemoveFolderClick = onRemoveFolderClick,
|
||||||
onScanNowClick = onScanNowClick,
|
onScanNowClick = onScanNowClick,
|
||||||
onSyncMetadataClick = onSyncMetadataClick,
|
onSyncMetadataClick = onSyncMetadataClick,
|
||||||
isLoading = isLoading
|
isLoading = isLoading || isRefreshing
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1159,6 +1232,7 @@ private fun ShelfListItem(
|
||||||
private fun LibraryListItem(
|
private fun LibraryListItem(
|
||||||
item: RecentFileItem,
|
item: RecentFileItem,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
|
isPinned: Boolean = false,
|
||||||
onItemClick: () -> Unit,
|
onItemClick: () -> Unit,
|
||||||
onItemLongClick: () -> Unit,
|
onItemLongClick: () -> Unit,
|
||||||
isDownloading: Boolean,
|
isDownloading: Boolean,
|
||||||
|
|
@ -1217,6 +1291,16 @@ private fun LibraryListItem(
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
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(
|
||||||
text = item.title ?: item.displayName,
|
text = item.title ?: item.displayName,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
|
@ -1556,3 +1640,90 @@ 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -116,7 +116,11 @@ private const val KEY_RENDER_MODE = "render_mode"
|
||||||
private const val KEY_FOLDER_SYNC_ENABLED = "folder_sync_enabled"
|
private const val KEY_FOLDER_SYNC_ENABLED = "folder_sync_enabled"
|
||||||
private const val KEY_FOLDER_MIGRATION_COMPLETED = "folder_migration_completed_v2"
|
private const val KEY_FOLDER_MIGRATION_COMPLETED = "folder_migration_completed_v2"
|
||||||
|
|
||||||
data class BannerMessage(val message: String, val isError: Boolean = false)
|
private const val KEY_FILTER_FILE_TYPES = "filter_file_types"
|
||||||
|
private const val KEY_FILTER_FOLDERS = "filter_folders"
|
||||||
|
private const val KEY_FILTER_READ_STATUS = "filter_read_status"
|
||||||
|
|
||||||
|
data class BannerMessage(val message: String, val isError: Boolean = false, val isPersistent: Boolean = false)
|
||||||
|
|
||||||
data class UserData(
|
data class UserData(
|
||||||
val uid: String, val displayName: String?, val photoUrl: String?, val email: String?
|
val uid: String, val displayName: String?, val photoUrl: String?, val email: String?
|
||||||
|
|
@ -161,6 +165,19 @@ enum class SortOrder(val displayName: String) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class ReadStatusFilter(val displayName: String) {
|
||||||
|
ALL("All"), UNREAD("Unread"), IN_PROGRESS("In Progress"), COMPLETED("Completed")
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LibraryFilters(
|
||||||
|
val fileTypes: Set<FileType> = emptySet(),
|
||||||
|
val sourceFolders: Set<String> = emptySet(),
|
||||||
|
val readStatus: ReadStatusFilter = ReadStatusFilter.ALL
|
||||||
|
) {
|
||||||
|
val isActive: Boolean
|
||||||
|
get() = fileTypes.isNotEmpty() || sourceFolders.isNotEmpty() || readStatus != ReadStatusFilter.ALL
|
||||||
|
}
|
||||||
|
|
||||||
data class ReaderScreenState(
|
data class ReaderScreenState(
|
||||||
val selectedPdfUri: Uri? = null,
|
val selectedPdfUri: Uri? = null,
|
||||||
val selectedBookId: String? = null,
|
val selectedBookId: String? = null,
|
||||||
|
|
@ -209,6 +226,9 @@ data class ReaderScreenState(
|
||||||
val reflowProgress: Float? = null,
|
val reflowProgress: Float? = null,
|
||||||
val recentFiles: List<RecentFileItem> = emptyList(),
|
val recentFiles: List<RecentFileItem> = emptyList(),
|
||||||
val allRecentFiles: List<RecentFileItem> = emptyList(),
|
val allRecentFiles: List<RecentFileItem> = emptyList(),
|
||||||
|
val pinnedHomeBookIds: Set<String> = emptySet(),
|
||||||
|
val pinnedLibraryBookIds: Set<String> = emptySet(),
|
||||||
|
val libraryFilters: LibraryFilters = LibraryFilters(),
|
||||||
)
|
)
|
||||||
|
|
||||||
open class MainViewModel(application: Application) : AndroidViewModel(application) {
|
open class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
@ -291,11 +311,22 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
currentUser = authRepository.getSignedInUser(),
|
currentUser = authRepository.getSignedInUser(),
|
||||||
isSyncEnabled = prefs.getBoolean(KEY_SYNC_ENABLED, false),
|
isSyncEnabled = prefs.getBoolean(KEY_SYNC_ENABLED, false),
|
||||||
isFolderSyncEnabled = prefs.getBoolean(KEY_FOLDER_SYNC_ENABLED, false),
|
isFolderSyncEnabled = prefs.getBoolean(KEY_FOLDER_SYNC_ENABLED, false),
|
||||||
|
libraryFilters = LibraryFilters(
|
||||||
|
fileTypes = prefs.getStringSet(KEY_FILTER_FILE_TYPES, emptySet())?.mapNotNull {
|
||||||
|
runCatching { FileType.valueOf(it) }.getOrNull()
|
||||||
|
}?.toSet() ?: emptySet(),
|
||||||
|
sourceFolders = prefs.getStringSet(KEY_FILTER_FOLDERS, emptySet()) ?: emptySet(),
|
||||||
|
readStatus = runCatching {
|
||||||
|
ReadStatusFilter.valueOf(prefs.getString(KEY_FILTER_READ_STATUS, ReadStatusFilter.ALL.name) ?: ReadStatusFilter.ALL.name)
|
||||||
|
}.getOrDefault(ReadStatusFilter.ALL)
|
||||||
|
),
|
||||||
syncedFolders = loadSyncedFoldersFromPrefs(),
|
syncedFolders = loadSyncedFoldersFromPrefs(),
|
||||||
lastFolderScanTime = if (prefs.contains(KEY_LAST_FOLDER_SCAN_TIME)) prefs.getLong(
|
lastFolderScanTime = if (prefs.contains(KEY_LAST_FOLDER_SCAN_TIME)) prefs.getLong(
|
||||||
KEY_LAST_FOLDER_SCAN_TIME, 0L
|
KEY_LAST_FOLDER_SCAN_TIME, 0L
|
||||||
)
|
)
|
||||||
else null
|
else null,
|
||||||
|
pinnedHomeBookIds = prefs.getStringSet(KEY_PINNED_HOME, emptySet()) ?: emptySet(),
|
||||||
|
pinnedLibraryBookIds = prefs.getStringSet(KEY_PINNED_LIBRARY, emptySet()) ?: emptySet()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -313,18 +344,46 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val sortedAllFiles = when (internalState.sortOrder) {
|
val baseVisibleFiles = rawFilteredByQuery.filterNot { it.bookId.endsWith("_reflow") }
|
||||||
SortOrder.RECENT -> rawFilteredByQuery
|
|
||||||
SortOrder.TITLE_ASC -> rawFilteredByQuery.sortedBy { it.title?.lowercase() ?: it.displayName.lowercase() }
|
val filters = internalState.libraryFilters
|
||||||
SortOrder.AUTHOR_ASC -> rawFilteredByQuery.sortedWith(compareBy(nullsLast()) { it.author?.lowercase() })
|
val libraryFiltered = baseVisibleFiles.filter { item ->
|
||||||
SortOrder.PERCENT_ASC -> rawFilteredByQuery.sortedBy { it.progressPercentage ?: 0f }
|
val matchType = if (filters.fileTypes.isNotEmpty()) item.type in filters.fileTypes else true
|
||||||
SortOrder.PERCENT_DESC -> rawFilteredByQuery.sortedByDescending { it.progressPercentage ?: 0f }
|
val matchFolder = if (filters.sourceFolders.isNotEmpty()) item.sourceFolderUri in filters.sourceFolders else true
|
||||||
|
val progress = item.progressPercentage ?: 0f
|
||||||
|
val matchStatus = when (filters.readStatus) {
|
||||||
|
ReadStatusFilter.ALL -> true
|
||||||
|
ReadStatusFilter.UNREAD -> progress == 0f
|
||||||
|
ReadStatusFilter.IN_PROGRESS -> progress > 0f && progress < 100f
|
||||||
|
ReadStatusFilter.COMPLETED -> progress >= 100f
|
||||||
|
}
|
||||||
|
matchType && matchFolder && matchStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
val visibleRecentFiles = sortedAllFiles.filterNot { it.bookId.endsWith("_reflow") }
|
fun sortFiles(files: List<RecentFileItem>): List<RecentFileItem> {
|
||||||
|
return when (internalState.sortOrder) {
|
||||||
|
SortOrder.RECENT -> files.sortedByDescending { it.timestamp }
|
||||||
|
SortOrder.TITLE_ASC -> files.sortedBy { it.title?.lowercase() ?: it.displayName.lowercase() }
|
||||||
|
SortOrder.AUTHOR_ASC -> files.sortedWith(compareBy(nullsLast()) { it.author?.lowercase() })
|
||||||
|
SortOrder.PERCENT_ASC -> files.sortedBy { it.progressPercentage ?: 0f }
|
||||||
|
SortOrder.PERCENT_DESC -> files.sortedByDescending { it.progressPercentage ?: 0f }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortedLibraryFiles = sortFiles(libraryFiltered).let { list ->
|
||||||
|
val pinned = list.filter { it.bookId in internalState.pinnedLibraryBookIds }
|
||||||
|
val unpinned = list.filter { it.bookId !in internalState.pinnedLibraryBookIds }
|
||||||
|
pinned + unpinned
|
||||||
|
}
|
||||||
|
|
||||||
|
val visibleRecentFiles = sortFiles(baseVisibleFiles.filter { it.isRecent }).let { list ->
|
||||||
|
val pinned = list.filter { it.bookId in internalState.pinnedHomeBookIds }
|
||||||
|
val unpinned = list.filter { it.bookId !in internalState.pinnedHomeBookIds }
|
||||||
|
pinned + unpinned
|
||||||
|
}
|
||||||
|
|
||||||
val validContextualItems = internalState.contextualActionItems.filter { contextItem ->
|
val validContextualItems = internalState.contextualActionItems.filter { contextItem ->
|
||||||
visibleRecentFiles.any { dbItem -> dbItem.uriString == contextItem.uriString }
|
baseVisibleFiles.any { dbItem -> dbItem.uriString == contextItem.uriString }
|
||||||
}.toSet()
|
}.toSet()
|
||||||
|
|
||||||
val shelfNames = prefs.getStringSet(KEY_SHELVES, emptySet()) ?: emptySet()
|
val shelfNames = prefs.getStringSet(KEY_SHELVES, emptySet()) ?: emptySet()
|
||||||
|
|
@ -332,12 +391,12 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
|
|
||||||
val shelvesFromPrefs = shelfNames.map { shelfName ->
|
val shelvesFromPrefs = shelfNames.map { shelfName ->
|
||||||
val bookIds = prefs.getStringSet("$KEY_SHELF_CONTENT_PREFIX$shelfName", emptySet()) ?: emptySet()
|
val bookIds = prefs.getStringSet("$KEY_SHELF_CONTENT_PREFIX$shelfName", emptySet()) ?: emptySet()
|
||||||
val booksForShelf = visibleRecentFiles.filter { it.bookId in bookIds }
|
val booksForShelf = baseVisibleFiles.filter { it.bookId in bookIds }
|
||||||
shelvedBookIds.addAll(booksForShelf.map { it.bookId })
|
shelvedBookIds.addAll(booksForShelf.map { it.bookId })
|
||||||
Shelf(shelfName, booksForShelf)
|
Shelf(shelfName, booksForShelf)
|
||||||
}.sortedBy { it.name }
|
}.sortedBy { it.name }
|
||||||
|
|
||||||
val unshelvedBooks = visibleRecentFiles.filter { it.bookId !in shelvedBookIds }
|
val unshelvedBooks = baseVisibleFiles.filter { it.bookId !in shelvedBookIds }
|
||||||
val allShelves = shelvesFromPrefs + Shelf("Unshelved", unshelvedBooks)
|
val allShelves = shelvesFromPrefs + Shelf("Unshelved", unshelvedBooks)
|
||||||
|
|
||||||
val booksAvailableForAdding =
|
val booksAvailableForAdding =
|
||||||
|
|
@ -348,7 +407,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
|
|
||||||
when (internalState.addBooksSource) {
|
when (internalState.addBooksSource) {
|
||||||
AddBooksSource.UNSHELVED -> unshelvedBooks
|
AddBooksSource.UNSHELVED -> unshelvedBooks
|
||||||
AddBooksSource.ALL_BOOKS -> visibleRecentFiles.filter {
|
AddBooksSource.ALL_BOOKS -> baseVisibleFiles.filter {
|
||||||
it.uriString !in currentShelfBooksUris
|
it.uriString !in currentShelfBooksUris
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -358,7 +417,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
|
|
||||||
internalState.copy(
|
internalState.copy(
|
||||||
recentFiles = visibleRecentFiles,
|
recentFiles = visibleRecentFiles,
|
||||||
allRecentFiles = sortedAllFiles,
|
allRecentFiles = sortedLibraryFiles,
|
||||||
contextualActionItems = validContextualItems,
|
contextualActionItems = validContextualItems,
|
||||||
shelves = allShelves,
|
shelves = allShelves,
|
||||||
booksAvailableForAdding = booksAvailableForAdding
|
booksAvailableForAdding = booksAvailableForAdding
|
||||||
|
|
@ -1009,6 +1068,38 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun togglePinForContextualItems(isHome: Boolean) {
|
||||||
|
val selectedIds = _internalState.value.contextualActionItems.map { it.bookId }.toSet()
|
||||||
|
if (selectedIds.isEmpty()) return
|
||||||
|
|
||||||
|
_internalState.update { state ->
|
||||||
|
val currentPins = if (isHome) state.pinnedHomeBookIds else state.pinnedLibraryBookIds
|
||||||
|
val allPinned = selectedIds.all { it in currentPins }
|
||||||
|
|
||||||
|
val newPins = if (allPinned) currentPins - selectedIds else currentPins + selectedIds
|
||||||
|
|
||||||
|
prefs.edit { putStringSet(if (isHome) KEY_PINNED_HOME else KEY_PINNED_LIBRARY, newPins) }
|
||||||
|
|
||||||
|
if (isHome) {
|
||||||
|
state.copy(pinnedHomeBookIds = newPins, contextualActionItems = emptySet())
|
||||||
|
} else {
|
||||||
|
state.copy(pinnedLibraryBookIds = newPins, contextualActionItems = emptySet())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLibraryFilters(filters: LibraryFilters) {
|
||||||
|
_internalState.update { it.copy(libraryFilters = filters) }
|
||||||
|
|
||||||
|
prefs.edit {
|
||||||
|
putStringSet(KEY_FILTER_FILE_TYPES, filters.fileTypes.map { it.name }.toSet())
|
||||||
|
putStringSet(KEY_FILTER_FOLDERS, filters.sourceFolders)
|
||||||
|
putString(KEY_FILTER_READ_STATUS, filters.readStatus.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("Library filters updated and persisted: $filters")
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun sharePdf(
|
suspend fun sharePdf(
|
||||||
activityContext: Context,
|
activityContext: Context,
|
||||||
sourceUri: Uri,
|
sourceUri: Uri,
|
||||||
|
|
@ -1480,7 +1571,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isRefreshing = true,
|
isRefreshing = true,
|
||||||
bannerMessage = BannerMessage(msg)
|
bannerMessage = BannerMessage(msg, isPersistent = true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1492,7 +1583,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
bannerMessage = if (showFeedback) BannerMessage("Folder Sync: Scan complete.") else it.bannerMessage,
|
bannerMessage = if (showFeedback) BannerMessage("Folder Sync: Scan complete.") else it.bannerMessage,
|
||||||
lastFolderScanTime = System.currentTimeMillis()
|
lastFolderScanTime = System.currentTimeMillis(),
|
||||||
|
syncedFolders = loadSyncedFoldersFromPrefs()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1502,7 +1594,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
errorMessage = if (showFeedback) "Sync failed." else it.errorMessage
|
errorMessage = if (showFeedback) "Sync failed." else it.errorMessage,
|
||||||
|
bannerMessage = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3738,5 +3831,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
internal const val KEY_LAST_FOLDER_SCAN_TIME = "last_folder_scan_time"
|
internal const val KEY_LAST_FOLDER_SCAN_TIME = "last_folder_scan_time"
|
||||||
private const val KEY_SYNCED_FOLDERS_JSON = "synced_folders_list_json"
|
private const val KEY_SYNCED_FOLDERS_JSON = "synced_folders_list_json"
|
||||||
private const val MAX_FOLDER_LIMIT = 3
|
private const val MAX_FOLDER_LIMIT = 3
|
||||||
|
internal const val KEY_PINNED_HOME = "pinned_home_books"
|
||||||
|
internal const val KEY_PINNED_LIBRARY = "pinned_library_books"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,15 +68,15 @@ class MetadataExtractionWorker(
|
||||||
originalBookNameHint = item.displayName,
|
originalBookNameHint = item.displayName,
|
||||||
parseContent = false
|
parseContent = false
|
||||||
)
|
)
|
||||||
title = book.title.takeIf { it.isNotBlank() }
|
title = book.title.takeIf { it.isNotBlank() && it != "content" }
|
||||||
author = book.author.takeIf { it.isNotBlank() }
|
author = book.author.takeIf { it.isNotBlank() && !it.equals("Unknown", ignoreCase = true) }
|
||||||
book.coverImage?.let { coverPath = recentFilesRepository.saveCoverToCache(it, uri) }
|
book.coverImage?.let { coverPath = recentFilesRepository.saveCoverToCache(it, uri) }
|
||||||
}
|
}
|
||||||
FileType.MOBI -> {
|
FileType.MOBI -> {
|
||||||
val book = mobiParser.createMobiBook(inputStream, item.displayName)
|
val book = mobiParser.createMobiBook(inputStream, item.displayName)
|
||||||
book?.let {
|
book?.let {
|
||||||
title = it.title.takeIf { t -> t.isNotBlank() }
|
title = it.title.takeIf { t -> t.isNotBlank() && t != "content" }
|
||||||
author = it.author.takeIf { a -> a.isNotBlank() }
|
author = it.author.takeIf { a -> a.isNotBlank() && !a.equals("Unknown", ignoreCase = true) }
|
||||||
it.coverImage?.let { img -> coverPath = recentFilesRepository.saveCoverToCache(img, uri) }
|
it.coverImage?.let { img -> coverPath = recentFilesRepository.saveCoverToCache(img, uri) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -84,9 +84,11 @@ class MetadataExtractionWorker(
|
||||||
pdfCoverGenerator.generateCover(uri)?.let {
|
pdfCoverGenerator.generateCover(uri)?.let {
|
||||||
coverPath = recentFilesRepository.saveCoverToCache(it, uri)
|
coverPath = recentFilesRepository.saveCoverToCache(it, uri)
|
||||||
}
|
}
|
||||||
title = item.displayName.substringBeforeLast(".")
|
title = item.displayName
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
title = item.displayName
|
||||||
}
|
}
|
||||||
else -> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.material.icons.filled.PushPin
|
||||||
import androidx.compose.material.icons.filled.SelectAll
|
import androidx.compose.material.icons.filled.SelectAll
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
|
@ -192,6 +193,7 @@ fun ContextualTopAppBar(
|
||||||
onNavIconClick: () -> Unit,
|
onNavIconClick: () -> Unit,
|
||||||
onInfoClick: (() -> Unit)? = null,
|
onInfoClick: (() -> Unit)? = null,
|
||||||
onSelectAllClick: (() -> Unit)? = null,
|
onSelectAllClick: (() -> Unit)? = null,
|
||||||
|
onPinClick: (() -> Unit)? = null,
|
||||||
onDeleteClick: () -> Unit
|
onDeleteClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
CustomTopAppBar(
|
CustomTopAppBar(
|
||||||
|
|
@ -202,6 +204,11 @@ fun ContextualTopAppBar(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
|
if (onPinClick != null) {
|
||||||
|
IconButton(onClick = onPinClick) {
|
||||||
|
Icon(Icons.Filled.PushPin, contentDescription = "Pin/Unpin")
|
||||||
|
}
|
||||||
|
}
|
||||||
if (selectedItemCount == 1 && onInfoClick != null) {
|
if (selectedItemCount == 1 && onInfoClick != null) {
|
||||||
IconButton(onClick = onInfoClick) {
|
IconButton(onClick = onInfoClick) {
|
||||||
Icon(Icons.Filled.Info, contentDescription = "Info")
|
Icon(Icons.Filled.Info, contentDescription = "Info")
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ data class FolderBookMetadata(
|
||||||
val lastPositionCfi: String?,
|
val lastPositionCfi: String?,
|
||||||
val progressPercentage: Float,
|
val progressPercentage: Float,
|
||||||
val isRecent: Boolean,
|
val isRecent: Boolean,
|
||||||
// REMOVED: val isDeleted: Boolean,
|
|
||||||
val lastModifiedTimestamp: Long,
|
val lastModifiedTimestamp: Long,
|
||||||
val bookmarksJson: String?,
|
val bookmarksJson: String?,
|
||||||
val locatorBlockIndex: Int?,
|
val locatorBlockIndex: Int?,
|
||||||
|
|
@ -33,7 +32,6 @@ data class FolderBookMetadata(
|
||||||
json.put("lastPositionCfi", lastPositionCfi)
|
json.put("lastPositionCfi", lastPositionCfi)
|
||||||
json.put("progressPercentage", progressPercentage.toDouble())
|
json.put("progressPercentage", progressPercentage.toDouble())
|
||||||
json.put("isRecent", isRecent)
|
json.put("isRecent", isRecent)
|
||||||
// REMOVED: json.put("isDeleted", isDeleted)
|
|
||||||
json.put("lastModifiedTimestamp", lastModifiedTimestamp)
|
json.put("lastModifiedTimestamp", lastModifiedTimestamp)
|
||||||
json.put("bookmarksJson", bookmarksJson)
|
json.put("bookmarksJson", bookmarksJson)
|
||||||
json.put("locatorBlockIndex", locatorBlockIndex ?: -1)
|
json.put("locatorBlockIndex", locatorBlockIndex ?: -1)
|
||||||
|
|
@ -65,7 +63,6 @@ data class FolderBookMetadata(
|
||||||
lastPositionCfi = json.optStringNull("lastPositionCfi"),
|
lastPositionCfi = json.optStringNull("lastPositionCfi"),
|
||||||
progressPercentage = json.optDouble("progressPercentage", 0.0).toFloat(),
|
progressPercentage = json.optDouble("progressPercentage", 0.0).toFloat(),
|
||||||
isRecent = json.optBoolean("isRecent", true),
|
isRecent = json.optBoolean("isRecent", true),
|
||||||
// REMOVED: isDeleted deserialization
|
|
||||||
lastModifiedTimestamp = json.optLong("lastModifiedTimestamp", 0L),
|
lastModifiedTimestamp = json.optLong("lastModifiedTimestamp", 0L),
|
||||||
bookmarksJson = json.optStringNull("bookmarksJson"),
|
bookmarksJson = json.optStringNull("bookmarksJson"),
|
||||||
locatorBlockIndex = json.optIntNull("locatorBlockIndex"),
|
locatorBlockIndex = json.optIntNull("locatorBlockIndex"),
|
||||||
|
|
@ -75,7 +72,6 @@ data class FolderBookMetadata(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the converter
|
|
||||||
fun FolderBookMetadata.toRecentFileItem(uriString: String?, coverPath: String?, sourceFolderUri: String?): RecentFileItem {
|
fun FolderBookMetadata.toRecentFileItem(uriString: String?, coverPath: String?, sourceFolderUri: String?): RecentFileItem {
|
||||||
return RecentFileItem(
|
return RecentFileItem(
|
||||||
bookId = this.bookId,
|
bookId = this.bookId,
|
||||||
|
|
@ -95,7 +91,7 @@ fun FolderBookMetadata.toRecentFileItem(uriString: String?, coverPath: String?,
|
||||||
isRecent = this.isRecent,
|
isRecent = this.isRecent,
|
||||||
isAvailable = true,
|
isAvailable = true,
|
||||||
lastModifiedTimestamp = this.lastModifiedTimestamp,
|
lastModifiedTimestamp = this.lastModifiedTimestamp,
|
||||||
isDeleted = false, // ALWAYS FALSE for folder sync
|
isDeleted = false,
|
||||||
bookmarksJson = this.bookmarksJson,
|
bookmarksJson = this.bookmarksJson,
|
||||||
sourceFolderUri = sourceFolderUri
|
sourceFolderUri = sourceFolderUri
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue