diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4174b4c..a25c278 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,8 +26,8 @@ android { applicationId = "com.aryan.reader" minSdk = 26 targetSdk = 35 - versionCode = 37 - versionName = "1.0.36" + versionCode = 38 + versionName = "1.0.37" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { diff --git a/app/src/main/java/com/aryan/reader/HomeScreen.kt b/app/src/main/java/com/aryan/reader/HomeScreen.kt index f5118fe..5ea2791 100644 --- a/app/src/main/java/com/aryan/reader/HomeScreen.kt +++ b/app/src/main/java/com/aryan/reader/HomeScreen.kt @@ -60,6 +60,7 @@ import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.MoreVert 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.outlined.AccountCircle import androidx.compose.material3.AlertDialog @@ -187,9 +188,11 @@ fun HomeScreen( } LaunchedEffect(uiState.bannerMessage) { - if (uiState.bannerMessage != null) { - delay(3000L) - viewModel.bannerMessageShown() + uiState.bannerMessage?.let { msg -> + if (!msg.isPersistent) { + delay(3000L) + viewModel.bannerMessageShown() + } } } @@ -304,6 +307,7 @@ fun HomeScreen( ContextualTopAppBar( selectedItemCount = selectedContextItems.size, onNavIconClick = { viewModel.clearContextualAction() }, + onPinClick = { viewModel.togglePinForContextualItems(isHome = true) }, onDeleteClick = { showDeleteConfirmDialog = true }, onSelectAllClick = { viewModel.selectAllRecentFiles() }) } @@ -336,6 +340,7 @@ fun HomeScreen( RecentFilesContent( recentFiles = recentFilesForHome, selectedContextItems = selectedContextItems, + pinnedHomeBookIds = uiState.pinnedHomeBookIds, onItemClick = { item -> viewModel.onRecentFileClicked(item) }, onItemLongClick = { item -> viewModel.onRecentItemLongPress(item) }, onSelectFileClick = onSelectFileClick, @@ -435,6 +440,7 @@ fun HomeScreen( private fun RecentFilesContent( recentFiles: List, selectedContextItems: Collection, + pinnedHomeBookIds: Set, onItemClick: (RecentFileItem) -> Unit, onItemLongClick: (RecentFileItem) -> Unit, onSelectFileClick: () -> Unit, @@ -456,6 +462,7 @@ private fun RecentFilesContent( .padding(horizontal = 16.dp), recentFiles = recentFiles, selectedItemUris = selectedContextItems.mapNotNull { it.uriString }.toSet(), + pinnedHomeBookIds = pinnedHomeBookIds, onItemClick = onItemClick, onItemLongClick = onItemLongClick, windowSizeClass = windowSizeClass, @@ -498,6 +505,7 @@ private fun RecentFilesContent( private fun RecentFilesGrid( modifier: Modifier = Modifier, recentFiles: List, + pinnedHomeBookIds: Set, selectedItemUris: Set, onItemClick: (RecentFileItem) -> Unit, onItemLongClick: (RecentFileItem) -> Unit, @@ -527,6 +535,7 @@ private fun RecentFilesGrid( RecentFileCard( item = item, isSelected = item.uriString in selectedItemUris, + isPinned = item.bookId in pinnedHomeBookIds, onClick = { onItemClick(item) }, onLongClick = { onItemLongClick(item) }, isDownloading = item.bookId in downloadingBookIds @@ -541,9 +550,10 @@ private fun RecentFilesGrid( fun RecentFileCard( item: RecentFileItem, isSelected: Boolean, + modifier: Modifier = Modifier, + isPinned: Boolean = false, onClick: () -> Unit, onLongClick: () -> Unit, - modifier: Modifier = Modifier, isDownloading: Boolean, ) { 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) { Box( modifier = Modifier diff --git a/app/src/main/java/com/aryan/reader/LibraryScreen.kt b/app/src/main/java/com/aryan/reader/LibraryScreen.kt index c849b6b..b5f8bda 100644 --- a/app/src/main/java/com/aryan/reader/LibraryScreen.kt +++ b/app/src/main/java/com/aryan/reader/LibraryScreen.kt @@ -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, selectedShelves: Set, sortOrder: SortOrder, + libraryFilters: LibraryFilters, + pinnedLibraryBookIds: Set, 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, lastFolderScanTime: Long?, isLoading: Boolean, + isRefreshing: Boolean, syncedFolders: List, 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, + 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)) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/aryan/reader/MainViewModel.kt b/app/src/main/java/com/aryan/reader/MainViewModel.kt index 6a3c8e5..0a0c008 100644 --- a/app/src/main/java/com/aryan/reader/MainViewModel.kt +++ b/app/src/main/java/com/aryan/reader/MainViewModel.kt @@ -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_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( 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 = emptySet(), + val sourceFolders: Set = emptySet(), + val readStatus: ReadStatusFilter = ReadStatusFilter.ALL +) { + val isActive: Boolean + get() = fileTypes.isNotEmpty() || sourceFolders.isNotEmpty() || readStatus != ReadStatusFilter.ALL +} + data class ReaderScreenState( val selectedPdfUri: Uri? = null, val selectedBookId: String? = null, @@ -209,6 +226,9 @@ data class ReaderScreenState( val reflowProgress: Float? = null, val recentFiles: List = emptyList(), val allRecentFiles: List = emptyList(), + val pinnedHomeBookIds: Set = emptySet(), + val pinnedLibraryBookIds: Set = emptySet(), + val libraryFilters: LibraryFilters = LibraryFilters(), ) open class MainViewModel(application: Application) : AndroidViewModel(application) { @@ -291,11 +311,22 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio currentUser = authRepository.getSignedInUser(), isSyncEnabled = prefs.getBoolean(KEY_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(), lastFolderScanTime = if (prefs.contains(KEY_LAST_FOLDER_SCAN_TIME)) prefs.getLong( 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) { - SortOrder.RECENT -> rawFilteredByQuery - SortOrder.TITLE_ASC -> rawFilteredByQuery.sortedBy { it.title?.lowercase() ?: it.displayName.lowercase() } - SortOrder.AUTHOR_ASC -> rawFilteredByQuery.sortedWith(compareBy(nullsLast()) { it.author?.lowercase() }) - SortOrder.PERCENT_ASC -> rawFilteredByQuery.sortedBy { it.progressPercentage ?: 0f } - SortOrder.PERCENT_DESC -> rawFilteredByQuery.sortedByDescending { it.progressPercentage ?: 0f } + val baseVisibleFiles = rawFilteredByQuery.filterNot { it.bookId.endsWith("_reflow") } + + val filters = internalState.libraryFilters + val libraryFiltered = baseVisibleFiles.filter { item -> + val matchType = if (filters.fileTypes.isNotEmpty()) item.type in filters.fileTypes else true + 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): List { + 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 -> - visibleRecentFiles.any { dbItem -> dbItem.uriString == contextItem.uriString } + baseVisibleFiles.any { dbItem -> dbItem.uriString == contextItem.uriString } }.toSet() 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 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 }) Shelf(shelfName, booksForShelf) }.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 booksAvailableForAdding = @@ -348,7 +407,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio when (internalState.addBooksSource) { AddBooksSource.UNSHELVED -> unshelvedBooks - AddBooksSource.ALL_BOOKS -> visibleRecentFiles.filter { + AddBooksSource.ALL_BOOKS -> baseVisibleFiles.filter { it.uriString !in currentShelfBooksUris } } @@ -358,7 +417,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio internalState.copy( recentFiles = visibleRecentFiles, - allRecentFiles = sortedAllFiles, + allRecentFiles = sortedLibraryFiles, contextualActionItems = validContextualItems, shelves = allShelves, 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( activityContext: Context, sourceUri: Uri, @@ -1480,7 +1571,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio it.copy( isLoading = false, isRefreshing = true, - bannerMessage = BannerMessage(msg) + bannerMessage = BannerMessage(msg, isPersistent = true) ) } } @@ -1492,7 +1583,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio isLoading = false, isRefreshing = false, 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( isLoading = 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" private const val KEY_SYNCED_FOLDERS_JSON = "synced_folders_list_json" 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" } } diff --git a/app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt b/app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt index 9794d3c..58c8785 100644 --- a/app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt +++ b/app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt @@ -68,15 +68,15 @@ class MetadataExtractionWorker( originalBookNameHint = item.displayName, parseContent = false ) - title = book.title.takeIf { it.isNotBlank() } - author = book.author.takeIf { it.isNotBlank() } + title = book.title.takeIf { it.isNotBlank() && it != "content" } + author = book.author.takeIf { it.isNotBlank() && !it.equals("Unknown", ignoreCase = true) } book.coverImage?.let { coverPath = recentFilesRepository.saveCoverToCache(it, uri) } } FileType.MOBI -> { val book = mobiParser.createMobiBook(inputStream, item.displayName) book?.let { - title = it.title.takeIf { t -> t.isNotBlank() } - author = it.author.takeIf { a -> a.isNotBlank() } + title = it.title.takeIf { t -> t.isNotBlank() && t != "content" } + author = it.author.takeIf { a -> a.isNotBlank() && !a.equals("Unknown", ignoreCase = true) } it.coverImage?.let { img -> coverPath = recentFilesRepository.saveCoverToCache(img, uri) } } } @@ -84,9 +84,11 @@ class MetadataExtractionWorker( pdfCoverGenerator.generateCover(uri)?.let { coverPath = recentFilesRepository.saveCoverToCache(it, uri) } - title = item.displayName.substringBeforeLast(".") + title = item.displayName + } + else -> { + title = item.displayName } - else -> {} } } diff --git a/app/src/main/java/com/aryan/reader/SharedComposables.kt b/app/src/main/java/com/aryan/reader/SharedComposables.kt index ce79060..19034d6 100644 --- a/app/src/main/java/com/aryan/reader/SharedComposables.kt +++ b/app/src/main/java/com/aryan/reader/SharedComposables.kt @@ -84,6 +84,7 @@ import android.content.Context import android.content.Intent import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.foundation.clickable +import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.filled.SelectAll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.OutlinedButton @@ -192,6 +193,7 @@ fun ContextualTopAppBar( onNavIconClick: () -> Unit, onInfoClick: (() -> Unit)? = null, onSelectAllClick: (() -> Unit)? = null, + onPinClick: (() -> Unit)? = null, onDeleteClick: () -> Unit ) { CustomTopAppBar( @@ -202,6 +204,11 @@ fun ContextualTopAppBar( } }, actions = { + if (onPinClick != null) { + IconButton(onClick = onPinClick) { + Icon(Icons.Filled.PushPin, contentDescription = "Pin/Unpin") + } + } if (selectedItemCount == 1 && onInfoClick != null) { IconButton(onClick = onInfoClick) { Icon(Icons.Filled.Info, contentDescription = "Info") diff --git a/app/src/main/java/com/aryan/reader/data/FolderBookMetadata.kt b/app/src/main/java/com/aryan/reader/data/FolderBookMetadata.kt index 0e2a92b..bf8e034 100644 --- a/app/src/main/java/com/aryan/reader/data/FolderBookMetadata.kt +++ b/app/src/main/java/com/aryan/reader/data/FolderBookMetadata.kt @@ -15,7 +15,6 @@ data class FolderBookMetadata( val lastPositionCfi: String?, val progressPercentage: Float, val isRecent: Boolean, - // REMOVED: val isDeleted: Boolean, val lastModifiedTimestamp: Long, val bookmarksJson: String?, val locatorBlockIndex: Int?, @@ -33,7 +32,6 @@ data class FolderBookMetadata( json.put("lastPositionCfi", lastPositionCfi) json.put("progressPercentage", progressPercentage.toDouble()) json.put("isRecent", isRecent) - // REMOVED: json.put("isDeleted", isDeleted) json.put("lastModifiedTimestamp", lastModifiedTimestamp) json.put("bookmarksJson", bookmarksJson) json.put("locatorBlockIndex", locatorBlockIndex ?: -1) @@ -65,7 +63,6 @@ data class FolderBookMetadata( lastPositionCfi = json.optStringNull("lastPositionCfi"), progressPercentage = json.optDouble("progressPercentage", 0.0).toFloat(), isRecent = json.optBoolean("isRecent", true), - // REMOVED: isDeleted deserialization lastModifiedTimestamp = json.optLong("lastModifiedTimestamp", 0L), bookmarksJson = json.optStringNull("bookmarksJson"), locatorBlockIndex = json.optIntNull("locatorBlockIndex"), @@ -75,7 +72,6 @@ data class FolderBookMetadata( } } -// Update the converter fun FolderBookMetadata.toRecentFileItem(uriString: String?, coverPath: String?, sourceFolderUri: String?): RecentFileItem { return RecentFileItem( bookId = this.bookId, @@ -95,7 +91,7 @@ fun FolderBookMetadata.toRecentFileItem(uriString: String?, coverPath: String?, isRecent = this.isRecent, isAvailable = true, lastModifiedTimestamp = this.lastModifiedTimestamp, - isDeleted = false, // ALWAYS FALSE for folder sync + isDeleted = false, bookmarksJson = this.bookmarksJson, sourceFolderUri = sourceFolderUri )