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:
parent
579b6f25bf
commit
d16bdd70d4
67 changed files with 9724 additions and 1890 deletions
|
|
@ -33,7 +33,19 @@ import androidx.compose.animation.fadeIn
|
|||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import androidx.compose.material3.TriStateCheckbox
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import com.aryan.reader.data.TagEntity
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -90,7 +102,6 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
|
|
@ -220,6 +231,7 @@ fun ContextualTopAppBar(
|
|||
selectedItemCount: Int,
|
||||
onNavIconClick: () -> Unit,
|
||||
onInfoClick: (() -> Unit)? = null,
|
||||
onTagClick: (() -> Unit)? = null,
|
||||
onSelectAllClick: (() -> Unit)? = null,
|
||||
onPinClick: (() -> Unit)? = null,
|
||||
onDeleteClick: () -> Unit
|
||||
|
|
@ -232,6 +244,11 @@ fun ContextualTopAppBar(
|
|||
}
|
||||
},
|
||||
actions = {
|
||||
if (onTagClick != null) {
|
||||
IconButton(onClick = onTagClick) {
|
||||
Icon(painterResource(id = R.drawable.tag), contentDescription = "Tag")
|
||||
}
|
||||
}
|
||||
if (onPinClick != null) {
|
||||
IconButton(onClick = onPinClick) {
|
||||
Icon(Icons.Filled.PushPin, contentDescription = stringResource(R.string.pin_unpin))
|
||||
|
|
@ -342,7 +359,7 @@ fun DeleteConfirmationDialog(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (String?) -> Unit) {
|
||||
fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (String?) -> Unit, onOpenTags: () -> Unit) {
|
||||
LocalContext.current
|
||||
@Suppress("DEPRECATION") val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
|
|
@ -465,6 +482,14 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S
|
|||
item.author?.takeIf { it.isNotBlank() && !it.equals("Unknown", ignoreCase = true) }?.let {
|
||||
InfoRowDetailed(stringResource(R.string.author), it)
|
||||
}
|
||||
item.seriesName?.takeIf { it.isNotBlank() }?.let { series ->
|
||||
val seriesText = if (item.seriesIndex != null && item.seriesIndex > 0) {
|
||||
"$series #${item.seriesIndex.toInt()}"
|
||||
} else {
|
||||
series
|
||||
}
|
||||
InfoRowDetailed("Series", seriesText)
|
||||
}
|
||||
InfoRowDetailed(stringResource(R.string.format), item.type.name)
|
||||
InfoRowDetailed(stringResource(R.string.size), formatFileSize(item.fileSize))
|
||||
InfoRowDetailed(stringResource(R.string.added), formattedDate)
|
||||
|
|
@ -488,6 +513,19 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S
|
|||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Tags", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
||||
TextButton(onClick = onOpenTags) { Text("+ Add / Edit") }
|
||||
}
|
||||
|
||||
if (item.tags.isNotEmpty()) {
|
||||
BookTagChipsRow(tags = item.tags, compact = false)
|
||||
} else {
|
||||
Text("No tags assigned.", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -901,6 +939,53 @@ fun FileTypeBadge(type: FileType, modifier: Modifier = Modifier, overlay: Boolea
|
|||
}
|
||||
}
|
||||
|
||||
private fun TagEntity.displayColor(): Color = Color(color ?: 0xFF64B5F6.toInt())
|
||||
|
||||
@Composable
|
||||
fun BookTagChipsRow(
|
||||
tags: List<TagEntity>,
|
||||
modifier: Modifier = Modifier,
|
||||
compact: Boolean = true,
|
||||
) {
|
||||
if (tags.isEmpty()) return
|
||||
|
||||
Row(
|
||||
modifier = modifier.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(if (compact) 6.dp else 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
tags.forEach { tag ->
|
||||
val tagColor = tag.displayColor()
|
||||
Surface(
|
||||
shape = RoundedCornerShape(50),
|
||||
color = tagColor.copy(alpha = 0.14f),
|
||||
contentColor = tagColor
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = if (compact) 8.dp else 10.dp,
|
||||
vertical = if (compact) 4.dp else 6.dp
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(if (compact) 6.dp else 8.dp)
|
||||
.background(tagColor, androidx.compose.foundation.shape.CircleShape)
|
||||
)
|
||||
Text(
|
||||
text = tag.name,
|
||||
style = if (compact) MaterialTheme.typography.labelSmall else MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val UNKNOWN_AUTHOR_LABEL = "No author listed"
|
||||
|
||||
fun RecentFileItem.cardTitle(): String {
|
||||
|
|
@ -1046,4 +1131,92 @@ fun ReadingProgressSection(
|
|||
trackColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.4f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TagSelectionBottomSheet(
|
||||
allTags: List<TagEntity>,
|
||||
selectedBookIds: Set<String>,
|
||||
booksWithTags: List<RecentFileItem>,
|
||||
onCreateAndAssign: (String) -> Unit,
|
||||
onToggleTag: (String, Boolean) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
|
||||
val filteredTags = remember(allTags, searchQuery) {
|
||||
if (searchQuery.isBlank()) allTags else allTags.filter { it.name.contains(searchQuery, ignoreCase = true) }
|
||||
}
|
||||
|
||||
val exactMatch = allTags.any { it.name.equals(searchQuery.trim(), ignoreCase = true) }
|
||||
|
||||
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) {
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp).heightIn(max = 500.dp)) {
|
||||
Text("Apply Tags", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, modifier = Modifier.padding(bottom = 16.dp))
|
||||
|
||||
androidx.compose.material3.OutlinedTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text("Search or create tag...") },
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
leadingIcon = { Icon(Icons.Default.Search, null) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth().weight(1f, fill = false)) {
|
||||
if (searchQuery.isNotBlank() && !exactMatch) {
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().clickable {
|
||||
onCreateAndAssign(searchQuery)
|
||||
searchQuery = ""
|
||||
}.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.Add, null, tint = MaterialTheme.colorScheme.primary)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text("Create \"${searchQuery.trim()}\"", color = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(filteredTags, key = { it.id }) { tag ->
|
||||
var checkedCount = 0
|
||||
selectedBookIds.forEach { bookId ->
|
||||
val book = booksWithTags.find { it.bookId == bookId }
|
||||
if (book?.tags?.any { it.id == tag.id } == true) checkedCount++
|
||||
}
|
||||
|
||||
val state = when (checkedCount) {
|
||||
0 -> ToggleableState.Off
|
||||
selectedBookIds.size -> ToggleableState.On
|
||||
else -> ToggleableState.Indeterminate
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().clickable {
|
||||
val assign = state != ToggleableState.On
|
||||
onToggleTag(tag.id, assign)
|
||||
}.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TriStateCheckbox(state = state, onClick = null)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Surface(shape = androidx.compose.foundation.shape.CircleShape, color = Color(tag.color ?: 0xFF64B5F6.toInt()).copy(alpha = 0.2f), modifier = Modifier.size(24.dp)) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Icon(painterResource(id = R.drawable.tag), contentDescription = null, modifier = Modifier.size(12.dp), tint = Color(tag.color ?: 0xFF64B5F6.toInt()))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(tag.name, style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue