V1.0.46 oss (#253)

* add a UI trigger for demo annotations

* Implement internal link navigation

* Simplify bottom padding logic in `EpubReaderScreen` for vertical scroll mode

* Add support for Calibre metadata

* Implement advanced library management with Room-backed collections, tags, and smart rules.

* Display book series information in book details and remove tags from home screen

* Implement zoom reset functionality in PDF viewer

* Add a "Pages" thumbnail view to the PDF navigation drawer

* Implement jump-back navigation in PDF viewer

* MainViewModel: disable FolderSyncWorker, add log export, and enhance PDF position logging

* Replace Gson with Kotlin Serialization in SmartCollectionEngine

* Implement ONNX-based speech bubble detection

* Implement speech bubble detection in PDF viewer

* Implement "Smart Comic Zoom" feature for PDF manga/comic reading

* Implement reader session persistence and restoration in `MainViewModel`

* Add tap-to-turn page feature to PDF viewer

* Improve book cache management and recovery

* Refactor top overlay padding logic in `PdfViewerScreen`

* Implement stylus eraser support in PDF reader

* Refactor ReaderTextFormatPanel to use ModalBottomSheet and add new formatting controls

* Introduce a customizable horizontal margin setting for the EPUB reader

* Refine folder sync and metadata handling for better conflict resolution and stability.

* Introduce user-adjustable image scaling for the EPUB reader

* Use maxWidthPx as default width fallback for blocks

* Improve cross-page text selection and header styling in the paginated reader

* Refactor speech bubble detection and UI to support segmentation masks and interactive scaling

* Improve speech bubble detection masking and rendering quality

* Update ONNX speech bubble detector to use `.ort` model and optimize inference

* Improve pagination accuracy

* Implement hierarchical folder navigation for the library.

* Refactor library item layout for improved space efficiency

* Add support for browsing and downloading Google Fonts

* Persist library landing state and add shelf search functionality

* Fix base tts sample.

* Move SpeechBubbleDetector to main source set and update ONNX dependency

* Implement a "locate" feature and improve synchronization for TTS (Text-to-Speech) playback across EPUB and PDF readers.

* Refine TTS synchronization and voice management in the EPUB reader

* Adjust OCR checks for OSS flavor and optimize external dictionary intent flags

* Add "Locate" button to PDF drawer's pages tab and move the page number to bottom right of thumbnail

* fix various crashes

* Refactor SpeechBubbleDetector into product flavors

* Implement speech bubble detection caching and background prefetching

* Implement on-demand download for Bubble Zoom ML model

* Add smooth animation for PDF speech bubble expansion

* fixes #252

* hide Google Fonts option, in FontsScreen, for offline variant

* Bump version to 1.0.46(46)
This commit is contained in:
Aryan 2026-04-29 16:37:53 +05:30 committed by GitHub
parent 579b6f25bf
commit d16bdd70d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 9724 additions and 1890 deletions

View file

@ -17,9 +17,13 @@
*
* mail: epistemereader@gmail.com
*/
// FontsScreen.kt
@file:Suppress("KotlinConstantConditions")
package com.aryan.reader
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -34,24 +38,32 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.CloudDownload
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -59,6 +71,8 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
@ -69,6 +83,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.aryan.reader.data.CustomFontEntity
import java.io.File
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FontsScreen(
viewModel: MainViewModel,
@ -76,24 +91,21 @@ fun FontsScreen(
) {
val fonts: List<CustomFontEntity> by viewModel.customFonts.collectAsStateWithLifecycle()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
// Dialog state
var showDeleteDialog by remember { mutableStateOf(false) }
var fontToDelete by remember { mutableStateOf<CustomFontEntity?>(null) }
var showGoogleFontsSheet by remember { mutableStateOf(false) }
val pickFontLauncher = rememberFilePickerLauncher { uris ->
uris.firstOrNull()?.let { viewModel.importFont(it) }
}
// Font mime types filter
val fontMimeTypes = arrayOf(
"font/ttf",
"font/otf",
"font/woff2",
"application/x-font-ttf",
"application/x-font-otf",
"application/font-woff2",
"application/vnd.ms-opentype",
"font/ttf", "font/otf", "font/woff2",
"application/x-font-ttf", "application/x-font-otf",
"application/font-woff2", "application/vnd.ms-opentype",
"application/x-font-opentype"
)
@ -111,11 +123,24 @@ fun FontsScreen(
},
floatingActionButton = {
if (fonts.isNotEmpty()) {
ExtendedFloatingActionButton(
onClick = { pickFontLauncher.launch(fontMimeTypes) },
icon = { Icon(Icons.Default.Add, contentDescription = null) },
text = { Text(stringResource(R.string.import_font)) }
)
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
ExtendedFloatingActionButton(
onClick = { showGoogleFontsSheet = true },
icon = { Icon(Icons.Default.CloudDownload, contentDescription = null) },
text = { Text("Google Fonts") },
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
ExtendedFloatingActionButton(
onClick = { pickFontLauncher.launch(fontMimeTypes) },
icon = { Icon(Icons.Default.Add, contentDescription = null) },
text = { Text(stringResource(R.string.import_font)) }
)
}
}
}
) { padding ->
@ -125,7 +150,9 @@ fun FontsScreen(
title = stringResource(R.string.no_custom_fonts),
message = stringResource(R.string.import_fonts_desc),
onSelectFileClick = { pickFontLauncher.launch(fontMimeTypes) },
modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize(),
secondaryButtonText = "Browse Google Fonts",
onSecondaryClick = { showGoogleFontsSheet = true }
)
} else {
LazyColumn(
@ -155,7 +182,6 @@ fun FontsScreen(
}
}
}
// Banner messages removed as requested
}
}
@ -173,8 +199,172 @@ fun FontsScreen(
}
)
}
if (showGoogleFontsSheet) {
GoogleFontsBottomSheet(
onDismiss = { showGoogleFontsSheet = false },
existingFonts = fonts,
getFullFontList = { viewModel.loadGoogleFontsList(context) },
onDownloadFont = { fontName, onComplete ->
viewModel.downloadGoogleFont(fontName, onComplete)
}
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GoogleFontsBottomSheet(
onDismiss: () -> Unit,
existingFonts: List<CustomFontEntity>,
getFullFontList: () -> List<String>,
onDownloadFont: (String, () -> Unit) -> Unit
) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var searchQuery by remember { mutableStateOf("") }
var downloadingFontName by remember { mutableStateOf<String?>(null) }
// Curated presets to show when search is empty
val popularPresets = remember {
listOf(
"Merriweather", "Open Sans", "Playfair Display", "Montserrat", "Oswald", "Raleway", "Nunito",
"Poppins", "Ubuntu", "Fira Sans", "Quicksand", "Crimson Text",
"Literata", "EB Garamond", "Libre Baskerville", "Inter", "Work Sans"
)
}
// Lazy evaluation of the full list only when typing
val displayList = remember(searchQuery) {
if (searchQuery.isBlank()) {
popularPresets
} else {
val allFonts = getFullFontList()
allFonts.filter { it.contains(searchQuery, ignoreCase = true) }.take(50) // Limit to 50 for performance
}
}
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
containerColor = MaterialTheme.colorScheme.surface
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text(
text = "Browse Google Fonts",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 12.dp)
)
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
modifier = Modifier.fillMaxWidth(),
placeholder = { Text("Search 1900+ fonts...") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
singleLine = true,
shape = RoundedCornerShape(12.dp)
)
Spacer(modifier = Modifier.height(16.dp))
LazyColumn(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(bottom = 24.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
if (searchQuery.isBlank()) {
item {
Text(
text = "Popular Choices",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 4.dp)
)
}
} else if (displayList.isEmpty()) {
item {
Text(
text = "No fonts found matching '$searchQuery'",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(16.dp)
)
}
}
items(displayList) { fontName ->
val isDownloaded = remember(existingFonts, fontName) {
existingFonts.any { it.displayName.equals(fontName, ignoreCase = true) }
}
val isDownloading = downloadingFontName == fontName
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable(enabled = !isDownloaded && !isDownloading) {
downloadingFontName = fontName
onDownloadFont(fontName) {
if (downloadingFontName == fontName) {
downloadingFontName = null
}
}
}
.background(
if (isDownloaded) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2f)
else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
)
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = fontName,
style = MaterialTheme.typography.bodyLarge,
fontWeight = if (isDownloaded) FontWeight.Bold else FontWeight.Medium,
color = if (isDownloaded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
)
Box(modifier = Modifier.padding(start = 12.dp)) {
when {
isDownloaded -> {
Icon(
Icons.Default.Check,
contentDescription = "Already Downloaded",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
}
isDownloading -> {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp,
color = MaterialTheme.colorScheme.primary
)
}
else -> {
Icon(
Icons.Default.CloudDownload,
contentDescription = "Download",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp)
)
}
}
}
}
}
}
}
}
}
// Existing unchanged components
@Composable
fun FontListItem(
font: CustomFontEntity,