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:
parent
c0d0e57e79
commit
b20ade9946
247 changed files with 43321 additions and 7087 deletions
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue