Linux support (#381)

* Add desktop release CI and support for Arch Linux packaging

* Make Gradle wrapper executable in desktop-release workflow

* Make Gradle wrapper executable in desktop-release workflow

* Make Gradle wrapper executable in desktop-release workflow

* Configure Gradle and update Java environment in desktop-release workflow

* Update Java setup and AUR packaging in desktop release workflow

* Update Java setup and AUR packaging in desktop release workflow

* Add MSIX packaging support for Windows desktop distribution

* Update AUR packaging metadata and validation

* Use spine toc attribute for NCX resolution

* crash fixes

* Implement automatic discovery and injection of EPUB font face siblings

* Enhance custom font support with family grouping and variable font handling

* Optimize metadata loading and improve TTS highlighting

* Add keyboard navigation support for EPUB reader

* Refine PDF spread page sizing to respect aspect ratios

* Implement responsive maximum height for reader popups and sheets

* Handle TTS generation failures by skipping problematic chunks

* Refactor PDF tile rendering logic and zoom indicator behavior

* Prefer block and offset locators over page index in native vertical flow

* Implement save and share actions for original book files

* Add Estonian language support

* Implement temporary viewing mode for external files

* Implement direct opening for temporary external files without library persistence

* fix failing tests

* Import SharedFileCapabilities in DesktopLibraryUi

* Improve native vertical reader progress, persistence, and image support

* Center target in viewport for native vertical reader and support animated scrolling
This commit is contained in:
Aryan 2026-06-14 13:43:49 +05:30 committed by GitHub
parent a13d6599d1
commit 625a4d5d2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
102 changed files with 6012 additions and 687 deletions

View file

@ -1,5 +1,8 @@
package com.aryan.reader.shared
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
data class CustomFontItem(
val id: String,
val displayName: String,
@ -10,3 +13,76 @@ data class CustomFontItem(
val isDeleted: Boolean = false
)
data class CustomFontVariantItem(
val font: CustomFontItem,
val variant: FontVariant?
)
data class CustomFontFamilyItem(
val familyName: String,
val variants: List<CustomFontVariantItem>
)
fun List<CustomFontItem>.groupByFamily(): List<CustomFontFamilyItem> {
val families = this.groupBy {
it.displayName.familyFilenameSignature().takeIf { s -> s.isNotBlank() } ?: it.displayName
}
return families.map { (familyName, fonts) ->
val variants = fonts.map { font ->
CustomFontVariantItem(
font = font,
variant = font.displayName.detectFontVariant()
)
}
CustomFontFamilyItem(
familyName = familyName.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() },
variants = variants
)
}.sortedBy { it.familyName }
}
fun CustomFontVariantItem.fontFaceLabel(): String {
val variant = variant ?: return "Regular"
return when {
variant.weight.weight >= FontWeight.Bold.weight && variant.style == FontStyle.Italic -> "Bold Italic"
variant.weight.weight >= FontWeight.Bold.weight -> "Bold"
variant.style == FontStyle.Italic -> "Italic"
variant.weight == FontWeight.Normal -> "Regular"
variant.weight.weight < FontWeight.Normal.weight -> variant.weight.fontWeightLabel()
else -> variant.weight.fontWeightLabel()
}
}
fun CustomFontFamilyItem.fontFaceSummary(): String {
return variants
.sortedWith(compareBy<CustomFontVariantItem> {
it.variant?.style == FontStyle.Italic
}.thenBy {
it.variant?.weight?.weight ?: FontWeight.Normal.weight
})
.map { it.fontFaceLabel() }
.distinct()
.joinToString()
}
fun CustomFontFamilyItem.hasVariableWeightFace(): Boolean {
return variants.any { variant ->
variant.font.displayName.supportsVariableWeightAxis() || variant.font.fileName.supportsVariableWeightAxis()
}
}
private fun FontWeight.fontWeightLabel(): String {
return when (this) {
FontWeight.Thin -> "Thin"
FontWeight.ExtraLight -> "Extra Light"
FontWeight.Light -> "Light"
FontWeight.Normal -> "Regular"
FontWeight.Medium -> "Medium"
FontWeight.SemiBold -> "Semi Bold"
FontWeight.Bold -> "Bold"
FontWeight.ExtraBold -> "Extra Bold"
FontWeight.Black -> "Black"
else -> weight.toString()
}
}

View file

@ -0,0 +1,149 @@
package com.aryan.reader.shared
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
data class FontVariant(
val weight: FontWeight,
val style: FontStyle
)
private val filenameSeparatorsRegex = Regex("""[\s._,-]+""")
fun String.familyFilenameSignature(): String {
val tokens = filenameTokens()
val variantIndexes = tokens.variantTokenIndexes()
val signatureTokens = tokens.filterIndexed { index, token ->
token.isNotBlank() && index !in variantIndexes
}
return signatureTokens.joinToString(separator = " ")
}
fun String.supportsVariableWeightAxis(): Boolean {
return filenameTokens().any { it == "wght" }
}
fun String.fontWeightCssDescriptor(fallbackWeight: FontWeight = FontWeight.Normal): String {
return if (supportsVariableWeightAxis()) {
"100 900"
} else {
fallbackWeight.weight.toString()
}
}
private fun List<String>.variantTokenIndexes(): Set<Int> {
val indexes = mutableSetOf<Int>()
forEachIndexed { index, token ->
if (token in singleVariantTokens || token.toIntOrNull()?.isCssFontWeight() == true) {
indexes += index
}
val next = getOrNull(index + 1) ?: return@forEachIndexed
if ("$token$next" in compoundVariantTokens) {
indexes += index
indexes += index + 1
}
}
return indexes
}
private fun Int.isCssFontWeight(): Boolean = this in 100..900 && this % 100 == 0
private fun List<String>.containsCompoundToken(compoundTokens: Set<String>): Boolean {
return windowed(size = 2, step = 1, partialWindows = false)
.any { (first, second) -> "$first$second" in compoundTokens }
}
private fun List<String>.detectedWeight(): FontWeight {
val compactTokens = buildList {
addAll(this@detectedWeight)
this@detectedWeight.windowed(size = 2, step = 1, partialWindows = false)
.forEach { (first, second) -> add("$first$second") }
}
return compactTokens.asSequence()
.mapNotNull { token ->
tokenWeightMap[token]
?: compoundTokenWeightMap[token]
?: token.toIntOrNull()?.takeIf { it.isCssFontWeight() }?.let(::FontWeight)
}
.maxByOrNull { it.weight }
?: FontWeight.Normal
}
private fun List<String>.detectedStyle(): FontStyle {
return if (any { it in italicTokens } || containsCompoundToken(compoundItalicTokens)) {
FontStyle.Italic
} else {
FontStyle.Normal
}
}
fun String.detectFontVariant(): FontVariant? {
val tokens = filenameTokens()
.filter { it.isNotBlank() }
if (tokens.isEmpty()) return null
return FontVariant(weight = tokens.detectedWeight(), style = tokens.detectedStyle())
}
private fun String.filenameTokens(): List<String> {
return this
.replace(Regex("""(?i)variablefont"""), " variablefont ")
.replace(Regex("""(?<=[a-z])(?=[A-Z])"""), "-")
.lowercase()
.split(filenameSeparatorsRegex)
}
private val italicTokens = setOf("italic", "ital", "oblique", "obliq", "it", "itallic", "italics", "slanted", "slant")
private val tokenWeightMap = mapOf(
"thin" to FontWeight.Thin,
"hairline" to FontWeight.Thin,
"extralight" to FontWeight.ExtraLight,
"ultralight" to FontWeight.ExtraLight,
"light" to FontWeight.Light,
"regular" to FontWeight.Normal,
"normal" to FontWeight.Normal,
"roman" to FontWeight.Normal,
"book" to FontWeight.Normal,
"medium" to FontWeight.Medium,
"semibold" to FontWeight.SemiBold,
"demibold" to FontWeight.SemiBold,
"bold" to FontWeight.Bold,
"extrabold" to FontWeight.ExtraBold,
"ultrabold" to FontWeight.ExtraBold,
"black" to FontWeight.Black,
"heavy" to FontWeight.Black
)
private val variableFontTokens = setOf(
"variablefont",
"vf",
"variable",
"wght",
"wdth",
"opsz",
"slnt",
"grad",
"xtra",
"xopq",
"yopq",
"ytlc",
"ytuc",
"ytas",
"ytde"
)
private val compoundItalicTokens = setOf("bolditalic", "boldital", "boldoblique", "boldobliq")
private val compoundWeightTokens = mapOf(
"extralight" to FontWeight.ExtraLight,
"ultralight" to FontWeight.ExtraLight,
"semibold" to FontWeight.SemiBold,
"demibold" to FontWeight.SemiBold,
"extrabold" to FontWeight.ExtraBold,
"ultrabold" to FontWeight.ExtraBold
)
private val compoundVariableFontTokens = setOf("variablefont")
private val compoundTokenWeightMap = compoundItalicTokens.associateWith { FontWeight.Bold } + compoundWeightTokens
private val singleVariantTokens = italicTokens + tokenWeightMap.keys + variableFontTokens
private val compoundVariantTokens = compoundItalicTokens + compoundWeightTokens.keys + compoundVariableFontTokens

View file

@ -239,7 +239,9 @@ internal enum class NonReaderLibraryPrimaryAction {
}
internal enum class NonReaderBookOverflowAction {
ADD_TO_SHELF
ADD_TO_SHELF,
SAVE_ORIGINAL,
SHARE_ORIGINAL
}
internal fun visibleNonReaderLibraryTabs(
@ -266,8 +268,14 @@ internal fun bookOverflowActionsForPlatform(
platform: ReaderPlatform = ReaderPlatform.ANDROID
): Set<NonReaderBookOverflowAction> {
return when (platform) {
ReaderPlatform.DESKTOP -> setOf(NonReaderBookOverflowAction.ADD_TO_SHELF)
ReaderPlatform.ANDROID -> emptySet()
ReaderPlatform.DESKTOP -> setOf(
NonReaderBookOverflowAction.ADD_TO_SHELF,
NonReaderBookOverflowAction.SAVE_ORIGINAL
)
ReaderPlatform.ANDROID -> setOf(
NonReaderBookOverflowAction.SAVE_ORIGINAL,
NonReaderBookOverflowAction.SHARE_ORIGINAL
)
}
}

View file

@ -56,6 +56,8 @@ import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Save
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Sync
import androidx.compose.material.icons.filled.Tag
import androidx.compose.material3.AssistChip
@ -165,6 +167,8 @@ fun SharedHomeScreen(
onRemoveSelected: () -> Unit,
onShowBookInfo: (BookItem) -> Unit = {},
onEditBook: (BookItem) -> Unit = {},
onSaveOriginalFile: (BookItem) -> Unit = {},
onShareOriginalFile: (BookItem) -> Unit = {},
onTagSelectedBooks: () -> Unit = {},
onAddSelectedBooksToShelf: () -> Unit = {},
onOpenTab: (BookItem) -> Unit = onOpenBook,
@ -173,6 +177,7 @@ fun SharedHomeScreen(
onRecentLimitChange: (Int) -> Unit = {},
onTogglePinned: (BookItem) -> Unit = {},
onOpenSettings: () -> Unit = {},
platform: ReaderPlatform = ReaderPlatform.ANDROID,
showActiveTabs: Boolean = true,
modifier: Modifier = Modifier
) {
@ -188,6 +193,16 @@ fun SharedHomeScreen(
) {
state.toNonReaderHomeLayoutModel()
}
val saveOriginalFileAction = if (NonReaderBookOverflowAction.SAVE_ORIGINAL in bookOverflowActionsForPlatform(platform)) {
onSaveOriginalFile
} else {
null
}
val shareOriginalFileAction = if (NonReaderBookOverflowAction.SHARE_ORIGINAL in bookOverflowActionsForPlatform(platform)) {
onShareOriginalFile
} else {
null
}
NonReaderScreenScaffold(
title = readerString("nav_home", "Home"),
subtitle = readerString("desktop_home_subtitle", "Continue reading and recent books"),
@ -268,6 +283,8 @@ fun SharedHomeScreen(
onOpenBook = { onOpenBook(book) },
onShowBookInfo = { onShowBookInfo(book) },
onEditBook = { onEditBook(book) },
onSaveOriginalFile = saveOriginalFileAction?.let { save -> { save(book) } },
onShareOriginalFile = shareOriginalFileAction?.let { share -> { share(book) } },
onTogglePinned = { onTogglePinned(book) }
)
}
@ -294,6 +311,8 @@ fun SharedHomeScreen(
onToggleSelection = onToggleSelection,
onShowBookInfo = onShowBookInfo,
onEditBook = onEditBook,
onSaveOriginalFile = saveOriginalFileAction,
onShareOriginalFile = shareOriginalFileAction,
onTogglePinned = onTogglePinned
)
}
@ -309,6 +328,8 @@ fun SharedHomeScreen(
onToggleSelection = onToggleSelection,
onShowBookInfo = onShowBookInfo,
onEditBook = onEditBook,
onSaveOriginalFile = saveOriginalFileAction,
onShareOriginalFile = shareOriginalFileAction,
onTogglePinned = onTogglePinned
)
}
@ -331,6 +352,8 @@ fun SharedLibraryScreen(
onRemoveSelected: () -> Unit,
onShowBookInfo: (BookItem) -> Unit = {},
onEditBook: (BookItem) -> Unit = {},
onSaveOriginalFile: (BookItem) -> Unit = {},
onShareOriginalFile: (BookItem) -> Unit = {},
onCreateShelf: () -> Unit = {},
onCreateShelfWithBooks: (String, Set<String>) -> Unit = { _, _ -> },
onCreateSmartShelf: () -> Unit = {},
@ -446,6 +469,8 @@ fun SharedLibraryScreen(
onToggleSelection = onToggleSelection,
onShowBookInfo = onShowBookInfo,
onEditBook = onEditBook,
onSaveOriginalFile = onSaveOriginalFile,
onShareOriginalFile = onShareOriginalFile,
onTogglePinned = onTogglePinned,
onAddBooksToShelf = onAddBooksToShelf,
onManageShelfBooks = onManageShelfBooks,
@ -495,6 +520,8 @@ fun SharedLibraryScreen(
onToggleSelection = onToggleSelection,
onShowBookInfo = onShowBookInfo,
onEditBook = onEditBook,
onSaveOriginalFile = onSaveOriginalFile,
onShareOriginalFile = onShareOriginalFile,
onTogglePinned = onTogglePinned,
onAddBooksToShelf = onAddBooksToShelf,
onManageShelfBooks = onManageShelfBooks,
@ -601,8 +628,11 @@ private fun ContinueReadingCard(
onOpenBook: () -> Unit,
onShowBookInfo: () -> Unit,
onEditBook: () -> Unit,
onSaveOriginalFile: (() -> Unit)?,
onShareOriginalFile: (() -> Unit)?,
onTogglePinned: () -> Unit
) {
val canUseOriginalFileActions = !book.isOpdsStream() && book.path != null
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(SharedUiTokens.surfaceRadius),
@ -643,6 +673,16 @@ private fun ContinueReadingCard(
IconButton(onClick = onEditBook) {
Icon(Icons.Default.Edit, contentDescription = readerString("action_edit", "Edit"))
}
if (canUseOriginalFileActions && onSaveOriginalFile != null) {
IconButton(onClick = onSaveOriginalFile) {
Icon(Icons.Default.Save, contentDescription = readerString("action_save_copy_to_device", "Save copy to device"))
}
}
if (canUseOriginalFileActions && onShareOriginalFile != null) {
IconButton(onClick = onShareOriginalFile) {
Icon(Icons.Default.Share, contentDescription = readerString("action_share", "Share"))
}
}
}
}
}
@ -659,6 +699,8 @@ private fun HomeBookShelf(
onToggleSelection: (String) -> Unit,
onShowBookInfo: (BookItem) -> Unit,
onEditBook: (BookItem) -> Unit,
onSaveOriginalFile: ((BookItem) -> Unit)?,
onShareOriginalFile: ((BookItem) -> Unit)?,
onTogglePinned: (BookItem) -> Unit
) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
@ -674,6 +716,8 @@ private fun HomeBookShelf(
onToggleSelection = { onToggleSelection(book.id) },
onShowInfo = { onShowBookInfo(book) },
onEdit = { onEditBook(book) },
onSaveOriginalFile = onSaveOriginalFile?.let { save -> { save(book) } },
onShareOriginalFile = onShareOriginalFile?.let { share -> { share(book) } },
onTogglePinned = { onTogglePinned(book) },
modifier = Modifier.width(168.dp)
)
@ -1226,6 +1270,8 @@ private fun LibraryContent(
onToggleSelection: (String) -> Unit,
onShowBookInfo: (BookItem) -> Unit,
onEditBook: (BookItem) -> Unit,
onSaveOriginalFile: (BookItem) -> Unit,
onShareOriginalFile: (BookItem) -> Unit,
onTogglePinned: (BookItem) -> Unit,
onAddBooksToShelf: (Set<String>) -> Unit,
onManageShelfBooks: ((Shelf) -> Unit)?,
@ -1262,6 +1308,16 @@ private fun LibraryContent(
} else {
null
}
val saveOriginalFileAction = if (NonReaderBookOverflowAction.SAVE_ORIGINAL in bookOverflowActionsForPlatform(platform)) {
onSaveOriginalFile
} else {
null
}
val shareOriginalFileAction = if (NonReaderBookOverflowAction.SHARE_ORIGINAL in bookOverflowActionsForPlatform(platform)) {
onShareOriginalFile
} else {
null
}
val manageShelfBooksAction = if (platform == ReaderPlatform.DESKTOP) onManageShelfBooks else null
val showNewShelfPrimaryAction = NonReaderLibraryPrimaryAction.NEW_SHELF in
primaryLibraryActionsForTab(selectedTab, platform)
@ -1314,6 +1370,8 @@ private fun LibraryContent(
onToggleSelection = onToggleSelection,
onShowBookInfo = onShowBookInfo,
onEditBook = onEditBook,
onSaveOriginalFile = saveOriginalFileAction,
onShareOriginalFile = shareOriginalFileAction,
onTogglePinned = onTogglePinned,
onAddToShelf = addToShelfFromBookAction?.let { addToShelf -> { book -> addToShelf(setOf(book.id)) } },
modifier = Modifier.weight(1f)
@ -1347,6 +1405,8 @@ private fun LibraryContent(
onToggleSelection = onToggleSelection,
onShowBookInfo = onShowBookInfo,
onEditBook = onEditBook,
onSaveOriginalFile = saveOriginalFileAction,
onShareOriginalFile = shareOriginalFileAction,
onTogglePinned = onTogglePinned,
onAddBooksToShelf = addToShelfFromBookAction,
onManageShelfBooks = manageShelfBooksAction,
@ -1370,6 +1430,8 @@ private fun LibraryContent(
onToggleSelection = onToggleSelection,
onShowBookInfo = onShowBookInfo,
onEditBook = onEditBook,
onSaveOriginalFile = saveOriginalFileAction,
onShareOriginalFile = shareOriginalFileAction,
onTogglePinned = onTogglePinned,
onAddBooksToShelf = addToShelfFromBookAction,
onRenameShelf = onRenameShelf,
@ -1387,6 +1449,8 @@ private fun LibraryContent(
onToggleSelection = onToggleSelection,
onShowBookInfo = onShowBookInfo,
onEditBook = onEditBook,
onSaveOriginalFile = saveOriginalFileAction,
onShareOriginalFile = shareOriginalFileAction,
onTogglePinned = onTogglePinned,
onAddBooksToShelf = addToShelfFromBookAction,
emptyTitle = readerString("desktop_no_tags_yet", "No tags yet"),
@ -1409,6 +1473,8 @@ private fun LibraryContent(
onToggleSelection = onToggleSelection,
onShowBookInfo = onShowBookInfo,
onEditBook = onEditBook,
onSaveOriginalFile = saveOriginalFileAction,
onShareOriginalFile = shareOriginalFileAction,
onTogglePinned = onTogglePinned,
onAddBooksToShelf = addToShelfFromBookAction,
onOpenShelf = { shelf -> onStateChange(state.copy(viewingShelfId = shelf.id)) },
@ -1431,6 +1497,8 @@ private fun LibraryContent(
onToggleSelection = onToggleSelection,
onShowBookInfo = onShowBookInfo,
onEditBook = onEditBook,
onSaveOriginalFile = saveOriginalFileAction,
onShareOriginalFile = shareOriginalFileAction,
onTogglePinned = onTogglePinned,
onAddBooksToShelf = addToShelfFromBookAction,
onRemoveFolder = onRemoveFolder,
@ -1690,6 +1758,8 @@ private fun BookGrid(
onShowBookInfo: (BookItem) -> Unit,
onEditBook: (BookItem) -> Unit,
onTogglePinned: (BookItem) -> Unit,
onSaveOriginalFile: ((BookItem) -> Unit)? = null,
onShareOriginalFile: ((BookItem) -> Unit)? = null,
onAddToShelf: ((BookItem) -> Unit)? = null,
modifier: Modifier = Modifier
) {
@ -1710,6 +1780,8 @@ private fun BookGrid(
onShowInfo = { onShowBookInfo(book) },
onEdit = { onEditBook(book) },
onTogglePinned = { onTogglePinned(book) },
onSaveOriginalFile = onSaveOriginalFile?.let { save -> { save(book) } },
onShareOriginalFile = onShareOriginalFile?.let { share -> { share(book) } },
onAddToShelf = onAddToShelf?.let { addToShelf -> { addToShelf(book) } }
)
}
@ -1733,6 +1805,8 @@ private fun BookGrid(
onShowInfo = { onShowBookInfo(book) },
onEdit = { onEditBook(book) },
onTogglePinned = { onTogglePinned(book) },
onSaveOriginalFile = onSaveOriginalFile?.let { save -> { save(book) } },
onShareOriginalFile = onShareOriginalFile?.let { share -> { share(book) } },
onAddToShelf = onAddToShelf?.let { addToShelf -> { addToShelf(book) } }
)
}
@ -1752,6 +1826,8 @@ private fun BookTile(
onShowInfo: () -> Unit,
onEdit: () -> Unit,
onTogglePinned: () -> Unit,
onSaveOriginalFile: (() -> Unit)? = null,
onShareOriginalFile: (() -> Unit)? = null,
onAddToShelf: (() -> Unit)? = null,
modifier: Modifier = Modifier
) {
@ -1803,6 +1879,8 @@ private fun BookTile(
onShowInfo = onShowInfo,
onEdit = onEdit,
onToggleSelection = onToggleSelection,
onSaveOriginalFile = onSaveOriginalFile.takeIf { !book.isOpdsStream() && book.path != null },
onShareOriginalFile = onShareOriginalFile.takeIf { !book.isOpdsStream() && book.path != null },
onAddToShelf = onAddToShelf
)
}
@ -1839,6 +1917,8 @@ private fun BookListItem(
onShowInfo: () -> Unit,
onEdit: () -> Unit,
onTogglePinned: () -> Unit,
onSaveOriginalFile: (() -> Unit)? = null,
onShareOriginalFile: (() -> Unit)? = null,
onAddToShelf: (() -> Unit)? = null
) {
var menuExpanded by remember { mutableStateOf(false) }
@ -1881,6 +1961,8 @@ private fun BookListItem(
onShowInfo = onShowInfo,
onEdit = onEdit,
onToggleSelection = onToggleSelection,
onSaveOriginalFile = onSaveOriginalFile.takeIf { !book.isOpdsStream() && book.path != null },
onShareOriginalFile = onShareOriginalFile.takeIf { !book.isOpdsStream() && book.path != null },
onAddToShelf = onAddToShelf
)
}
@ -1898,6 +1980,8 @@ private fun BookActionMenu(
onShowInfo: () -> Unit,
onEdit: () -> Unit,
onToggleSelection: () -> Unit,
onSaveOriginalFile: (() -> Unit)? = null,
onShareOriginalFile: (() -> Unit)? = null,
onAddToShelf: (() -> Unit)? = null
) {
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) {
@ -1925,6 +2009,26 @@ private fun BookActionMenu(
onEdit()
}
)
if (onSaveOriginalFile != null) {
DropdownMenuItem(
leadingIcon = { Icon(Icons.Default.Save, contentDescription = null) },
text = { Text(readerString("action_save_copy_to_device", "Save copy to device")) },
onClick = {
onDismiss()
onSaveOriginalFile()
}
)
}
if (onShareOriginalFile != null) {
DropdownMenuItem(
leadingIcon = { Icon(Icons.Default.Share, contentDescription = null) },
text = { Text(readerString("action_share", "Share")) },
onClick = {
onDismiss()
onShareOriginalFile()
}
)
}
if (onAddToShelf != null) {
DropdownMenuItem(
leadingIcon = { Icon(Icons.Default.Folder, contentDescription = null) },
@ -2112,6 +2216,8 @@ private fun ShelfCollection(
onShowBookInfo: (BookItem) -> Unit,
onEditBook: (BookItem) -> Unit,
onTogglePinned: (BookItem) -> Unit,
onSaveOriginalFile: ((BookItem) -> Unit)? = null,
onShareOriginalFile: ((BookItem) -> Unit)? = null,
onAddBooksToShelf: ((Set<String>) -> Unit)? = null,
onManageShelfBooks: ((Shelf) -> Unit)? = null,
onRenameShelf: (Shelf) -> Unit = {},
@ -2151,6 +2257,8 @@ private fun ShelfCollection(
onShowBookInfo = onShowBookInfo,
onEditBook = onEditBook,
onTogglePinned = onTogglePinned,
onSaveOriginalFile = onSaveOriginalFile,
onShareOriginalFile = onShareOriginalFile,
onAddBooksToShelf = onAddBooksToShelf,
onManageShelfBooks = onManageShelfBooks,
onRenameShelf = onRenameShelf,
@ -2172,6 +2280,8 @@ private fun ShelfSection(
onShowBookInfo: (BookItem) -> Unit,
onEditBook: (BookItem) -> Unit,
onTogglePinned: (BookItem) -> Unit,
onSaveOriginalFile: ((BookItem) -> Unit)?,
onShareOriginalFile: ((BookItem) -> Unit)?,
onAddBooksToShelf: ((Set<String>) -> Unit)?,
onManageShelfBooks: ((Shelf) -> Unit)?,
onRenameShelf: (Shelf) -> Unit,
@ -2258,6 +2368,8 @@ private fun ShelfSection(
onShowInfo = { onShowBookInfo(book) },
onEdit = { onEditBook(book) },
onTogglePinned = { onTogglePinned(book) },
onSaveOriginalFile = onSaveOriginalFile?.let { save -> { save(book) } },
onShareOriginalFile = onShareOriginalFile?.let { share -> { share(book) } },
onAddToShelf = onAddBooksToShelf?.let { addToShelf -> { addToShelf(setOf(book.id)) } },
modifier = Modifier.width(148.dp)
)
@ -2279,6 +2391,8 @@ private fun FolderShelfDetail(
onShowBookInfo: (BookItem) -> Unit,
onEditBook: (BookItem) -> Unit,
onTogglePinned: (BookItem) -> Unit,
onSaveOriginalFile: ((BookItem) -> Unit)? = null,
onShareOriginalFile: ((BookItem) -> Unit)? = null,
onAddBooksToShelf: ((Set<String>) -> Unit)? = null,
onOpenShelf: (Shelf) -> Unit,
onBack: () -> Unit,
@ -2345,6 +2459,8 @@ private fun FolderShelfDetail(
onShowInfo = { onShowBookInfo(book) },
onEdit = { onEditBook(book) },
onTogglePinned = { onTogglePinned(book) },
onSaveOriginalFile = onSaveOriginalFile?.let { save -> { save(book) } },
onShareOriginalFile = onShareOriginalFile?.let { share -> { share(book) } },
onAddToShelf = onAddBooksToShelf?.let { addToShelf -> { addToShelf(setOf(book.id)) } }
)
}

View file

@ -1663,13 +1663,9 @@ private fun List<ReaderPage>.findSharedNativeVerticalPageIndexForBlock(block: Se
return firstOrNull()?.pageIndex
}
private fun List<SharedNativeVerticalFlowItem>.sharedNativeVerticalItemIndexForLocator(
internal fun List<SharedNativeVerticalFlowItem>.sharedNativeVerticalItemIndexForLocator(
locator: ReaderLocator
): Int? {
locator.pageIndex?.let { pageIndex ->
val samePage = indexOfFirst { item -> item.page.pageIndex == pageIndex }
if (samePage >= 0) return samePage
}
val chapterIndex = locator.chapterIndex
if (chapterIndex != null) {
locator.blockIndex?.let { blockIndex ->
@ -1687,6 +1683,12 @@ private fun List<SharedNativeVerticalFlowItem>.sharedNativeVerticalItemIndexForL
}
if (sameOffset >= 0) return sameOffset
}
}
locator.pageIndex?.let { pageIndex ->
val samePage = indexOfFirst { item -> item.page.pageIndex == pageIndex }
if (samePage >= 0) return samePage
}
if (chapterIndex != null) {
val sameChapter = indexOfFirst { item -> item.page.chapterIndex == chapterIndex }
if (sameChapter >= 0) return sameChapter
}
@ -3170,7 +3172,7 @@ private fun SemanticTextBlock.renderedTextStyle(
).takeIf { it.isSpecified } ?: foreground,
fontSize = fontSize,
lineHeight = lineHeight,
fontFamily = fallbackFontFamily,
fontFamily = style.spanStyle.fontFamily ?: fallbackFontFamily,
fontWeight = fontWeight
?: style.spanStyle.fontWeight
?: if (this is SemanticHeader) FontWeight.Bold else MaterialTheme.typography.bodyLarge.fontWeight,

View file

@ -117,6 +117,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import com.aryan.reader.shared.BuiltInReaderThemes
import com.aryan.reader.shared.CustomFontItem
import com.aryan.reader.shared.fontFaceSummary
import com.aryan.reader.shared.groupByFamily
import com.aryan.reader.shared.hasVariableWeightFace
import com.aryan.reader.shared.HighlightColor
import com.aryan.reader.shared.PageInfoMode
import com.aryan.reader.shared.PageInfoPosition
@ -1740,28 +1743,46 @@ fun SharedReaderFormatControls(
}
}
val activeCustomFonts = customFonts.filterNot { it.isDeleted }.sortedBy { it.displayName.lowercase() }
if (activeCustomFonts.isNotEmpty()) {
val activeCustomFontFamilies = customFonts.filterNot { it.isDeleted }.groupByFamily()
if (activeCustomFontFamilies.isNotEmpty()) {
Text(
readerString("desktop_imported_fonts", "Imported fonts"),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
SharedReaderChoiceRow {
activeCustomFonts.forEach { font ->
activeCustomFontFamilies.forEach { family ->
val isSelected = family.variants.any { it.font.path == settings.customFontPath }
FilterChip(
selected = settings.customFontPath == font.path,
selected = isSelected,
onClick = {
val baseFont = family.variants.firstOrNull { it.variant?.weight == FontWeight.Normal && it.variant?.style == androidx.compose.ui.text.font.FontStyle.Normal }?.font ?: family.variants.first().font
onReaderAction(
ReaderAction.SettingsChanged(
settings.copy(
fontFamily = font.displayName,
customFontPath = font.path
fontFamily = family.familyName,
customFontPath = baseFont.path
)
)
)
},
label = { Text(font.displayName, maxLines = 1, overflow = TextOverflow.Ellipsis) }
label = {
Row(horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically) {
Text(family.familyName, maxLines = 1, overflow = TextOverflow.Ellipsis)
val variantsStr = buildString {
append(family.fontFaceSummary())
if (family.hasVariableWeightFace()) append(" - Variable weight")
}
if (variantsStr.isNotBlank()) {
Text(
"($variantsStr)",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
maxLines = 1
)
}
}
}
)
}
}