Desktop app (#308)

* Implement build profiles and feature policy for offline desktop builds

* Introduce unified cross-platform Settings Hub

* Refactor main settings into a hierarchical page-based navigation model

* Refactor library projection to use shared multiplatform logic

* Refactor UI state consumption by removing intermediate screen models

* Introduce AndroidSharedStateBridge to centralize state mapping and reduction logic

* Refactor state management for tabs, selection, and pinning to use shared bridge logic

* Refactor file type management and validation into a centralized shared module

* Centralize file type resolution and improve handling of unknown types

* Centralize book import logic with SharedImportPlanner

* Refactor magnifier geometry logic and coordinate mapping

* Properly handle orientation changes in scroll-locked PDF reader

* Add screen orientation controls to EPUB and PDF readers

* Implement right-to-left (RTL) pagination support and refactor reader menus

* Separate right-to-left pagination settings for PDF and EPUB

* Ensure PDF page data is scoped by document key for multi tab support

* Implement theme-aware link styling for the epub reader

* Implement jump history for back and forward navigation in the epub reader

* Improve locator handling and navigation logic in paginated reader mode

* Implement stable pagination navigation and location tracking

* Centralize banner message management and auto-dismiss logic in MainViewModel

* Implement zoom and pan state preservation for PDF pan lock mode

* Enhance reader navigation UI and workspace layout management in desktop app

* Refactor reader navigation sidebar and relocate search controls in desktop app

* Enhance reader UI with redesigned selection menus and bottom sheet overlays

* Implement custom highlight palettes and reader theme customization in desktop app

* Implement cross-platform modal layer and refine reader UI styling

* Improve highlight accuracy and implement metadata enrichment on book open in desktop app

* Implement two-page spread layout for paginated reader on desktop

* Implement persistent caching for book loading and pagination in desktop app

* Implement persistent caching for book loading and pagination in desktop app

* Optimize reader settings updates by separating layout and appearance changes in desktop app

* Improve desktop window branding and native Windows styling

* Enhance reader selection interactions and UI across EPUB and PDF viewers in desktop app

* Refine selection handle positioning and interaction logic

* Implement EPUB selection debug logging and improve handle targeting

* Optimize desktop book loading performance and UI responsiveness

* Implement anchored zoom gestures and rendering optimizations for the Desktop PDF viewer.

* Implement smooth zoom preview for the PDF reader in desktop app

* Optimize PDF rendering performance and responsiveness in the desktop reader

* Implement conditional diagnostic logging and update desktop build configuration

* Implemented hierarchical TOC, custom scrollbars, and improved desktop modal handling

* Added management options for annotations and highlights in the sidebar in desktop app

* Implemented `SharedStableOutlinedTextField` and updated text input fields to use `TextFieldValue` for improved cursor and selection stability.

* Refined library filters and enhanced OPDS functionality in desktop app

* Improved EPUB pagination measurement and implemented layout diagnostic logging for desktop app

* Added PPTX support including document parsing, rendering, and indexing

* Improved PPTX rendering and layout accuracy

* Implemented text autofit support for PPTX rendering

* Enhanced PPTX rendering with support for custom geometry, automatic numbering, table styles, and image opacity

* Improved EPUB pagination accuracy and added layout telemetry in desktop app

* Improved folder synchronization with metadata-only mode and hashed sidecar management in desktop app

* Implemented rich text font scaling and migrated desktop ink tools to custom pointer input handling

* Implemented billing account obfuscation

* Implemented hierarchical folder navigation and improved library selection functionality in desktop app

* Implemented platform-aware directory resolution and multi-platform native library support for desktop

* Added full-screen mode for the reader workspace

* Added PDF zoom indicator and interactive vertical scrollbar with page tooltips

* Refactored speech bubble prefetching to use a limited radius and improved ML detector initialization and lifecycle management

* Updated PDF indexing to replace existing page text and removed search result item keys

* Implemented "preparing" foreground notification for TTS service

* Optimized PDF rendering performance by pre-calculating page-specific annotations

* Refactored desktop packaging tasks and improved distribution configuration

* Optimized EPUB parser memory usage and added path traversal protection

* Refactored WorkManager monitoring logic and added work pruning

* Implemented comprehensive resource cleanup and memory management for WebView-based components to prevent memory leaks

* Implemented bitmap size limits and scaling to prevent canvas rendering errors

* Split long text paragraphs into multiple semantic blocks during HTML parsing

* Implemented local ActionMode for text selection to prevent platform crashes

* Refactored PPTX text layout, optimized HtmlParser block detection, and improved banner dismissal logic

* Added desktop startup splash screen and deferred WebView initialization

* Reorganized settings hub and added separate PDF reader defaults

* Implemented embedded cover extraction and metadata support for MOBI and FB2 formats

* Implemented batching for MetadataExtractionWorker and optimized EPUB metadata extraction performance.

* Implemented procedurally generated book covers and replaced static placeholders

* Redesigned search UI with a top bar and results overlay in desktop app

* Added PDF page gap and overlay visibility options and implemented DesktopBookImporter

* Refactored PDF reader UI with tabbed inspector and improved theme background handling in desktop

* Implemented PDF viewport persistence for zoom and scroll positions in desktop app

* Improved desktop fullscreen implementation and state restoration

* Implemented desktop window state persistence

* Implemented flavor-based branding and ProGuard configuration for desktop builds

* Implemented precise reader positioning and improved highlight rendering logic in desktop app

* Added support for user-editable book metadata

* Enhanced book metadata support and integrated info/edit dialogs

* Implemented embedded EPUB metadata editing

* Improved highlight mapping and added custom scrollbar styling for the reader.

* Reduced desktop WebView bundle size by excluding unused locales and runtime files

* Added neutral pan mode as the default PDF interaction state.

* Refactored library empty states and updated primary navigation tabs in desktop app

* Implemented native paginated reader and unified content rendering architecture in desktop epub reader

* Implemented native EPUB image rendering for desktop and improved block layout spacing with margin collapsing.

* Improved pagination overflow detection in desktop

* Implemented multi-block text selection with interactive handles and CFI support in desktop epub pagination
This commit is contained in:
Aryan 2026-05-15 22:36:51 +05:30 committed by GitHub
parent c0d0e57e79
commit b20ade9946
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
247 changed files with 43321 additions and 7087 deletions

View file

@ -79,6 +79,7 @@ import androidx.compose.material.icons.filled.FolderSpecial
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AssistChip
@ -133,6 +134,7 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.media3.common.util.UnstableApi
import androidx.navigation.NavHostController
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.aryan.reader.data.RecentFileItem
@ -148,7 +150,6 @@ import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import org.jsoup.Jsoup
import timber.log.Timber
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@ -163,6 +164,7 @@ private fun getBookCountString(count: Int): String {
@Composable
fun LibraryScreen(
viewModel: MainViewModel,
navController: NavHostController,
) {
val compStart = remember { System.currentTimeMillis() }
LaunchedEffect(Unit) {
@ -170,14 +172,13 @@ fun LibraryScreen(
}
val context = LocalContext.current
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val screenModel = remember(uiState) { uiState.toLibraryScreenModel() }
val selectedItems = screenModel.selectedItems
val isContextualModeActive = screenModel.isContextualModeActive
val selectedShelves = screenModel.selectedShelves
val isShelfContextualModeActive = screenModel.isShelfContextualModeActive
val sortOrder = screenModel.sortOrder
val shelves = screenModel.shelves
val rawLibraryFiles = screenModel.rawLibraryFiles
val selectedItems = uiState.contextualActionItems
val isContextualModeActive = selectedItems.isNotEmpty()
val selectedShelves = uiState.contextualActionShelfIds
val isShelfContextualModeActive = selectedShelves.isNotEmpty()
val sortOrder = uiState.sortOrder
val shelves = uiState.shelves
val rawLibraryFiles = uiState.rawLibraryFiles
val tabTitles = remember {
buildList {
add(context.getString(R.string.tab_all_books))
@ -193,13 +194,13 @@ fun LibraryScreen(
pageCount = { tabTitles.size }
)
val containsFolderItems = screenModel.containsFolderItemsInSelection
val containsFolderItems = selectedItems.any { it.sourceFolderUri != null }
val scope = rememberCoroutineScope()
var showFilterSheet by remember { mutableStateOf(false) }
val isSearchActive = screenModel.isSearchActive
val searchQuery = screenModel.searchQuery
val isSearchActive = uiState.isSearchActive
val searchQuery = uiState.searchQuery
val pickFolderLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree()
@ -341,7 +342,8 @@ fun LibraryScreen(
catalogId = catalog?.id
)
},
onDeleteCatalogStreams = viewModel::deleteStreamedBooksForCatalog
onDeleteCatalogStreams = viewModel::deleteStreamedBooksForCatalog,
onSettingsClick = { navController.navigate(AppDestinations.SETTINGS_SCREEN_ROUTE) }
)
@ -394,8 +396,14 @@ fun LibraryScreen(
showInfoDialog = false
itemForInfoDialog = null
},
onUpdateName = { newName ->
viewModel.updateCustomName(item.bookId, newName)
onSaveMetadata = { metadata ->
viewModel.updateBookMetadata(item.bookId, metadata)
},
onSaveDisplayName = { name ->
viewModel.updateCustomName(item.bookId, name)
},
onRestoreMetadata = {
viewModel.restoreOriginalBookMetadata(item.bookId)
},
onOpenTags = { viewModel.openTagSelection(setOf(item.bookId)) }
)
@ -516,7 +524,9 @@ fun ShelfScreen(
FileInfoDialog(
item = item,
onDismiss = { showInfoDialog = false; itemForInfoDialog = null },
onUpdateName = { newName -> viewModel.updateCustomName(item.bookId, newName) },
onSaveMetadata = { metadata -> viewModel.updateBookMetadata(item.bookId, metadata) },
onSaveDisplayName = { name -> viewModel.updateCustomName(item.bookId, name) },
onRestoreMetadata = { viewModel.restoreOriginalBookMetadata(item.bookId) },
onOpenTags = { viewModel.openTagSelection(setOf(item.bookId)) }
)
}
@ -577,6 +587,7 @@ fun LibraryScreenContent(
onOpdsBookDownloaded: (Uri, String) -> Unit,
onStreamOpdsBook: (OpdsEntry, OpdsCatalog?) -> Unit,
onDeleteCatalogStreams: (String) -> Unit,
onSettingsClick: () -> Unit,
) {
val isBookContextualModeActive = selectedItems.isNotEmpty()
val isShelfContextualModeActive = selectedShelves.isNotEmpty()
@ -711,6 +722,9 @@ fun LibraryScreenContent(
Icon(Icons.Default.Search, contentDescription = stringResource(R.string.action_search))
}
}
IconButton(onClick = onSettingsClick) {
Icon(Icons.Default.Settings, contentDescription = "Settings")
}
}
)
TabRow(selectedTabIndex = pagerState.currentPage) {
@ -1442,8 +1456,6 @@ private fun AddBooksModeScreen(
@Composable
private fun ShelfCover(shelf: Shelf) {
val context = LocalContext.current
val placeholder = R.drawable.epub_placeholder
val booksForCovers = shelf.books.take(4).reversed()
val coverWidth = 52.dp
val coverHeight = 75.dp
@ -1457,22 +1469,24 @@ private fun ShelfCover(shelf: Shelf) {
contentAlignment = Alignment.CenterStart
) {
if (booksForCovers.size <= 1) {
val imageModel = remember(shelf.topBook?.coverImagePath) {
shelf.topBook?.coverImagePath?.let { File(it) } ?: placeholder
val topBook = shelf.topBook
if (topBook != null) {
ThemedBookCover(
item = topBook,
contentDescription = stringResource(R.string.content_desc_shelf_cover, shelf.name),
contentScale = ContentScale.Crop,
modifier = Modifier
.size(width = coverWidth, height = coverHeight)
.clip(MaterialTheme.shapes.small)
)
} else {
EmptyShelfCover(
shelfName = shelf.name,
modifier = Modifier
.size(width = coverWidth, height = coverHeight)
.clip(MaterialTheme.shapes.small)
)
}
AsyncImage(
model = ImageRequest.Builder(context)
.data(imageModel)
.error(placeholder)
.fallback(placeholder)
.crossfade(true)
.build(),
contentDescription = stringResource(R.string.content_desc_shelf_cover, shelf.name),
contentScale = ContentScale.Crop,
modifier = Modifier
.size(width = coverWidth, height = coverHeight)
.clip(MaterialTheme.shapes.small)
)
} else {
Box(
modifier = Modifier
@ -1480,9 +1494,6 @@ private fun ShelfCover(shelf: Shelf) {
.height(coverHeight)
) {
booksForCovers.forEachIndexed { index, book ->
val imageModel = remember(book.coverImagePath) {
book.coverImagePath?.let { File(it) } ?: placeholder
}
Surface(
shape = MaterialTheme.shapes.small,
shadowElevation = 4.dp,
@ -1491,13 +1502,8 @@ private fun ShelfCover(shelf: Shelf) {
.align(Alignment.CenterEnd)
.offset(x = -horizontalOffset * index)
) {
AsyncImage(
model = ImageRequest.Builder(context)
.data(imageModel)
.error(placeholder)
.fallback(placeholder)
.crossfade(true)
.build(),
ThemedBookCover(
item = book,
contentDescription = null,
contentScale = ContentScale.Crop
)
@ -1508,6 +1514,36 @@ private fun ShelfCover(shelf: Shelf) {
}
}
@Composable
private fun EmptyShelfCover(
shelfName: String,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.background(
androidx.compose.ui.graphics.Brush.linearGradient(
colors = listOf(
MaterialTheme.colorScheme.secondaryContainer,
MaterialTheme.colorScheme.surfaceContainerHighest
)
)
)
.border(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)),
contentAlignment = Alignment.Center
) {
Text(
text = shelfName.takeIf { it.isNotBlank() } ?: stringResource(R.string.tab_shelves),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(8.dp)
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ShelfListItem(
@ -1596,15 +1632,6 @@ private fun LibraryListItem(
onItemLongClick: () -> Unit,
isDownloading: Boolean,
) {
val context = LocalContext.current
val placeholder = when (item.type) {
FileType.PDF -> R.drawable.pdf_placeholder
FileType.EPUB, FileType.MOBI, FileType.FB2, FileType.MD, FileType.TXT, FileType.HTML, FileType.CBZ, FileType.CBR, FileType.CB7, FileType.DOCX, FileType.ODT, FileType.FODT -> R.drawable.epub_placeholder
}
val imageModel = remember(item.coverImagePath) {
item.coverImagePath?.let { File(it) } ?: placeholder
}
androidx.compose.material3.ElevatedCard(
shape = MaterialTheme.shapes.large,
colors = androidx.compose.material3.CardDefaults.elevatedCardColors(
@ -1641,13 +1668,8 @@ private fun LibraryListItem(
.clip(MaterialTheme.shapes.medium)
.border(0.5.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), MaterialTheme.shapes.medium)
) {
AsyncImage(
model = ImageRequest.Builder(context)
.data(imageModel)
.error(placeholder)
.fallback(placeholder)
.crossfade(true)
.build(),
ThemedBookCover(
item = item,
contentDescription = item.displayName,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
@ -2148,7 +2170,7 @@ private fun EditFolderFiltersDialog(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
FileType.entries.forEach { type ->
ANDROID_SYNCABLE_FILE_TYPES.forEach { type ->
val isSelected = type in selectedTypes
FilterChip(
selected = isSelected,
@ -2226,7 +2248,7 @@ fun LibraryFilterSheet(
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FileType.entries.forEach { type ->
ANDROID_READABLE_FILE_TYPES.forEach { type ->
FilterChip(
selected = type in currentFilters.fileTypes,
onClick = {