Desktop update (#362)
* Update documentation images * Introduce native vertical EPUB reader for Desktop * Add libsecret support for Linux secret storage * Update localized strings and plurals across multiple languages * Update PDF annotation UI and highlighter settings on desktop
This commit is contained in:
parent
e4634ed251
commit
6651169ce2
60 changed files with 14457 additions and 468 deletions
|
|
@ -194,7 +194,7 @@ object SharedPdfAnnotationDefaults {
|
|||
0xFFFFFFFF.toInt()
|
||||
)
|
||||
|
||||
val highlighterPalette: List<Int> = SharedPdfAndroidHighlightColors.palette.take(4)
|
||||
val highlighterPalette: List<Int> = SharedPdfAndroidHighlightColors.palette.take(5)
|
||||
|
||||
fun configFor(tool: PdfInkTool): PdfToolConfig {
|
||||
return when (tool) {
|
||||
|
|
@ -235,7 +235,7 @@ data class SharedPdfHighlighterPalette(
|
|||
|
||||
companion object {
|
||||
const val DefaultAlpha: Int = 0x8C
|
||||
const val MaxColors: Int = 4
|
||||
const val MaxColors: Int = 5
|
||||
val defaultColors: List<Int>
|
||||
get() = SharedPdfAnnotationDefaults.highlighterPalette
|
||||
.take(MaxColors)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
package com.aryan.reader.shared.reader
|
||||
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
||||
internal fun resolveSharedReaderTextAlign(
|
||||
cssTextAlign: TextAlign,
|
||||
fallbackTextAlign: TextAlign
|
||||
): TextAlign {
|
||||
return when {
|
||||
fallbackTextAlign.isExplicitSharedReaderTextAlign() -> fallbackTextAlign
|
||||
cssTextAlign == TextAlign.Unspecified -> fallbackTextAlign
|
||||
cssTextAlign == TextAlign.Justify -> TextAlign.Left
|
||||
else -> cssTextAlign
|
||||
}
|
||||
}
|
||||
|
||||
private fun TextAlign.isExplicitSharedReaderTextAlign(): Boolean {
|
||||
return this != TextAlign.Start &&
|
||||
this != TextAlign.Left &&
|
||||
this != TextAlign.Unspecified
|
||||
}
|
||||
|
||||
internal fun resolveSharedReaderFontFeatureSettings(
|
||||
existingSettings: String?,
|
||||
fontVariantNumeric: String?
|
||||
): String? {
|
||||
val existing = existingSettings
|
||||
?.trim()
|
||||
?.takeIf { it.isNotBlank() }
|
||||
val numericFeatures = fontVariantNumeric
|
||||
?.lowercase()
|
||||
?.split(Regex("\\s+"))
|
||||
?.mapNotNull { token ->
|
||||
when (token) {
|
||||
"lining-nums" -> "lnum"
|
||||
"oldstyle-nums" -> "onum"
|
||||
"proportional-nums" -> "pnum"
|
||||
"tabular-nums" -> "tnum"
|
||||
"diagonal-fractions" -> "frac"
|
||||
"stacked-fractions" -> "afrc"
|
||||
"ordinal" -> "ordn"
|
||||
"slashed-zero" -> "zero"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
?.distinct()
|
||||
?.map { feature -> "\"$feature\" on" }
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.joinToString(", ")
|
||||
|
||||
return listOfNotNull(existing, numericFeatures)
|
||||
.joinToString(", ")
|
||||
.takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.aryan.reader.shared.reader.ReaderPage
|
|||
import com.aryan.reader.shared.reader.ReaderReadingMode
|
||||
import com.aryan.reader.shared.reader.ReaderSearchOptions
|
||||
import com.aryan.reader.shared.reader.ReaderSettings
|
||||
import com.aryan.reader.shared.reader.SharedEpubBook
|
||||
|
||||
data class ReaderContentNavigationTarget(
|
||||
val locator: ReaderLocator?,
|
||||
|
|
@ -44,4 +45,18 @@ sealed interface ReaderContentRenderPlan {
|
|||
override val navigationTarget: ReaderContentNavigationTarget,
|
||||
override val highlights: List<UserHighlight>
|
||||
) : ReaderContentRenderPlan
|
||||
|
||||
data class NativeVerticalPages(
|
||||
val book: SharedEpubBook,
|
||||
val pages: List<ReaderPage>,
|
||||
val currentPageIndex: Int,
|
||||
val settings: ReaderSettings,
|
||||
val searchQuery: String,
|
||||
val searchOptions: ReaderSearchOptions,
|
||||
val highlightPalette: ReaderHighlightPalette,
|
||||
override val background: Color,
|
||||
override val foreground: Color,
|
||||
override val navigationTarget: ReaderContentNavigationTarget,
|
||||
override val highlights: List<UserHighlight>
|
||||
) : ReaderContentRenderPlan
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -63,17 +63,20 @@ import androidx.compose.ui.focus.focusRequester
|
|||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.RoundRect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathMeasure
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.StrokeJoin
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.graphics.drawscope.translate
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
|
|
@ -700,13 +703,11 @@ private fun SharedPdfAnnotationToolSettingsPanel(
|
|||
penPalette.ifEmpty { SharedPdfAnnotationDefaults.penPalette }
|
||||
}
|
||||
val selectedPaletteIndex = remember(activePalette, selectedColor, isHighlighter) {
|
||||
activePalette.indexOfFirst { paletteColor ->
|
||||
if (isHighlighter) {
|
||||
Color(paletteColor).copy(alpha = 1f) == Color(selectedColor).copy(alpha = 1f)
|
||||
} else {
|
||||
paletteColor == selectedColor
|
||||
}
|
||||
}
|
||||
sharedPdfSettingsSelectedPaletteIndex(
|
||||
activePalette = activePalette,
|
||||
selectedColor = selectedColor,
|
||||
matchRgbOnly = isHighlighter
|
||||
)
|
||||
}
|
||||
|
||||
fun colorPickerPalette(): List<Int> {
|
||||
|
|
@ -810,6 +811,8 @@ private fun SharedPdfAnnotationToolSettingsPanel(
|
|||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
if (isHighlighter) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
|
@ -978,6 +981,20 @@ private fun SharedPdfAnnotationToolSettingsPanel(
|
|||
}
|
||||
}
|
||||
|
||||
internal fun sharedPdfSettingsSelectedPaletteIndex(
|
||||
activePalette: List<Int>,
|
||||
selectedColor: Int,
|
||||
matchRgbOnly: Boolean
|
||||
): Int {
|
||||
return activePalette.indexOfFirst { paletteColor ->
|
||||
if (matchRgbOnly) {
|
||||
(paletteColor and 0x00FFFFFF) == (selectedColor and 0x00FFFFFF)
|
||||
} else {
|
||||
paletteColor == selectedColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SharedPdfHighlighterPalettePreview(
|
||||
colors: List<Int>,
|
||||
|
|
@ -1076,10 +1093,7 @@ private fun SharedPdfStyledPropertySlider(
|
|||
thumbColor: Color,
|
||||
activeColor: Color
|
||||
) {
|
||||
val displayValue = remember(value, valueRange) {
|
||||
val fraction = (value - valueRange.start) / (valueRange.endInclusive - valueRange.start)
|
||||
(fraction * 100f).roundToInt().coerceIn(1, 100)
|
||||
}
|
||||
val displayValue = remember(value, valueRange) { sharedPdfSettingsDisplayPercent(value, valueRange) }
|
||||
val onePercentDelta = (valueRange.endInclusive - valueRange.start) / 100f
|
||||
val canDecrease = value > valueRange.start + 0.0001f
|
||||
val canIncrease = value < valueRange.endInclusive - 0.0001f
|
||||
|
|
@ -1097,7 +1111,7 @@ private fun SharedPdfStyledPropertySlider(
|
|||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "-",
|
||||
text = "\u2014",
|
||||
color = if (canDecrease) Color.White else Color.White.copy(alpha = 0.3f),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
|
|
@ -1137,36 +1151,66 @@ private fun SharedPdfStyledPropertySlider(
|
|||
}
|
||||
}
|
||||
},
|
||||
track = { sliderState ->
|
||||
val range = sliderState.valueRange.endInclusive - sliderState.valueRange.start
|
||||
val fraction = if (range == 0f) {
|
||||
0f
|
||||
} else {
|
||||
((sliderState.value - sliderState.valueRange.start) / range).coerceIn(0f, 1f)
|
||||
}
|
||||
val activeTrackColor = when {
|
||||
isOpacity -> activeColor.copy(alpha = 1f)
|
||||
activeColor.luminance() < 0.18f -> Color.White.copy(alpha = 0.88f)
|
||||
else -> activeColor.copy(alpha = 0.95f)
|
||||
}
|
||||
val inactiveTrackColor = if (isOpacity) {
|
||||
Color.White.copy(alpha = 0.24f)
|
||||
} else {
|
||||
trackColor.copy(alpha = 0.85f)
|
||||
}
|
||||
Box(
|
||||
track = { _ ->
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
.background(inactiveTrackColor, RoundedCornerShape(2.dp)),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
.height(16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(fraction)
|
||||
.height(4.dp)
|
||||
.background(activeTrackColor, RoundedCornerShape(2.dp))
|
||||
)
|
||||
val trackHeight = size.height
|
||||
val cornerRadius = CornerRadius(trackHeight / 2f)
|
||||
if (isOpacity) {
|
||||
drawRoundRect(
|
||||
color = Color.Gray,
|
||||
size = size,
|
||||
cornerRadius = cornerRadius
|
||||
)
|
||||
val roundedClipPath = Path().apply {
|
||||
addRoundRect(
|
||||
RoundRect(
|
||||
rect = Rect(Offset.Zero, size),
|
||||
cornerRadius = cornerRadius
|
||||
)
|
||||
)
|
||||
}
|
||||
clipPath(roundedClipPath) {
|
||||
val boxSize = 12f
|
||||
val columns = (size.width / boxSize).toInt() + 1
|
||||
val rows = (size.height / boxSize).toInt() + 1
|
||||
for (column in 0 until columns) {
|
||||
for (row in 0 until rows) {
|
||||
drawRect(
|
||||
color = if ((column + row) % 2 == 0) {
|
||||
Color(0xFF555555)
|
||||
} else {
|
||||
Color(0xFF333333)
|
||||
},
|
||||
topLeft = Offset(column * boxSize, row * boxSize),
|
||||
size = Size(boxSize, boxSize)
|
||||
)
|
||||
}
|
||||
}
|
||||
drawRect(color = activeColor)
|
||||
}
|
||||
} else {
|
||||
drawRoundRect(
|
||||
color = trackColor.copy(alpha = 0.5f),
|
||||
size = size,
|
||||
cornerRadius = cornerRadius
|
||||
)
|
||||
val dotRadius = 1.5.dp.toPx()
|
||||
val padding = trackHeight / 2f
|
||||
val availableWidth = size.width - (padding * 2f)
|
||||
val dotCount = 8
|
||||
val spacing = availableWidth / (dotCount - 1)
|
||||
for (index in 0 until dotCount) {
|
||||
drawCircle(
|
||||
color = Color.White.copy(alpha = 0.2f),
|
||||
radius = dotRadius,
|
||||
center = Offset(padding + (index * spacing), size.height / 2f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -1192,6 +1236,15 @@ private fun SharedPdfStyledPropertySlider(
|
|||
}
|
||||
}
|
||||
|
||||
internal fun sharedPdfSettingsDisplayPercent(
|
||||
value: Float,
|
||||
valueRange: ClosedFloatingPointRange<Float>
|
||||
): Int {
|
||||
val range = valueRange.endInclusive - valueRange.start
|
||||
val fraction = if (range == 0f) 0f else (value - valueRange.start) / range
|
||||
return (fraction * 100f).roundToInt().coerceIn(1, 100)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SharedPdfInkColorPalette(
|
||||
colors: List<Int>,
|
||||
|
|
@ -2174,7 +2227,7 @@ private fun SharedPdfPenIcon(
|
|||
val animatedInkColor by animateColorAsState(targetValue = inkColor, label = "shared_ink_color")
|
||||
val inkProgress by animateFloatAsState(
|
||||
targetValue = if (isSelected) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 450, easing = LinearEasing),
|
||||
animationSpec = tween(durationMillis = 600, easing = LinearEasing),
|
||||
label = "shared_ink_progress"
|
||||
)
|
||||
|
||||
|
|
@ -2211,12 +2264,18 @@ private fun SharedPdfPenIcon(
|
|||
}
|
||||
|
||||
if (inkProgress > 0.01f) {
|
||||
val tipY = when (tool) {
|
||||
PdfInkTool.HIGHLIGHTER -> topPadding
|
||||
PdfInkTool.HIGHLIGHTER_ROUND -> topPadding + tipHeight * 0.15f
|
||||
else -> topPadding
|
||||
}
|
||||
drawInkPreview(
|
||||
tool = tool,
|
||||
color = animatedInkColor,
|
||||
progress = inkProgress,
|
||||
startPoint = Offset(size.width / 2f, topPadding - 1f),
|
||||
strokeWidth = strokeWidth
|
||||
startPoint = Offset(size.width / 2f, tipY),
|
||||
strokeWidth = strokeWidth,
|
||||
isStraight = showHighlighterSnap
|
||||
)
|
||||
}
|
||||
if (showHighlighterSnap && tool.isDesktopHighlighter) {
|
||||
|
|
@ -2545,23 +2604,59 @@ private fun DrawScope.drawInkPreview(
|
|||
color: Color,
|
||||
progress: Float,
|
||||
startPoint: Offset,
|
||||
strokeWidth: Float
|
||||
strokeWidth: Float,
|
||||
isStraight: Boolean = false
|
||||
) {
|
||||
val x = startPoint.x
|
||||
val y = startPoint.y - 2f
|
||||
val path = Path().apply {
|
||||
moveTo(startPoint.x, startPoint.y)
|
||||
moveTo(x, y)
|
||||
if (tool.isHighlighter) {
|
||||
val waveWidth = 46f
|
||||
cubicTo(startPoint.x + waveWidth * 0.35f, startPoint.y - 12f, startPoint.x + waveWidth * 0.65f, startPoint.y + 12f, startPoint.x + waveWidth, startPoint.y)
|
||||
val waveWidth = 70f
|
||||
if (isStraight) {
|
||||
lineTo(x + waveWidth, y)
|
||||
} else {
|
||||
val amplitude = 20f
|
||||
cubicTo(
|
||||
x + waveWidth * 0.35f,
|
||||
y - amplitude,
|
||||
x + waveWidth * 0.65f,
|
||||
y + amplitude,
|
||||
x + waveWidth,
|
||||
y
|
||||
)
|
||||
}
|
||||
} else {
|
||||
cubicTo(startPoint.x + 22f, startPoint.y - 24f, startPoint.x - 22f, startPoint.y - 52f, startPoint.x - 9f, startPoint.y - 28f)
|
||||
cubicTo(startPoint.x - 3f, startPoint.y - 8f, startPoint.x + 32f, startPoint.y - 16f, startPoint.x + 44f, startPoint.y - 34f)
|
||||
cubicTo(
|
||||
x + 16f,
|
||||
y - 18f,
|
||||
x - 18f,
|
||||
y - 34f,
|
||||
x - 7f,
|
||||
y - 21f
|
||||
)
|
||||
cubicTo(
|
||||
x - 2f,
|
||||
y - 5f,
|
||||
x + 22f,
|
||||
y - 11f,
|
||||
x + 31f,
|
||||
y - 26f
|
||||
)
|
||||
}
|
||||
}
|
||||
val revealProgress = sharedPdfInkPreviewRevealProgress(progress)
|
||||
val pathMeasure = PathMeasure()
|
||||
pathMeasure.setPath(path, false)
|
||||
val revealedPath = Path()
|
||||
val targetLength = pathMeasure.length * revealProgress
|
||||
if (targetLength <= 0f || !pathMeasure.getSegment(0f, targetLength, revealedPath, true)) return
|
||||
|
||||
val width = SharedPdfInkRenderer.effectiveStrokeWidthPx(strokeWidth, pageWidthPx = 700f)
|
||||
.coerceIn(if (tool.isHighlighter) 5f else 1.2f, if (tool.isHighlighter) 16f else 5f)
|
||||
drawPath(
|
||||
path = path,
|
||||
color = color.copy(alpha = color.alpha * progress),
|
||||
path = revealedPath,
|
||||
color = color,
|
||||
style = Stroke(
|
||||
width = width,
|
||||
cap = if (tool == PdfInkTool.HIGHLIGHTER) StrokeCap.Butt else StrokeCap.Round,
|
||||
|
|
@ -2571,6 +2666,10 @@ private fun DrawScope.drawInkPreview(
|
|||
)
|
||||
}
|
||||
|
||||
internal fun sharedPdfInkPreviewRevealProgress(progress: Float): Float {
|
||||
return progress.coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
private val PdfInkTool.isDesktopPenTool: Boolean
|
||||
get() = this == PdfInkTool.FOUNTAIN_PEN || this == PdfInkTool.PEN || this == PdfInkTool.PENCIL
|
||||
|
||||
|
|
|
|||
|
|
@ -242,6 +242,7 @@ fun SharedReaderScreen(
|
|||
readerTexturePreviewContent: (@Composable (String, Modifier) -> Unit)? = null,
|
||||
readerCustomTextureIds: List<String> = emptyList(),
|
||||
onImportReaderTexture: ((ReaderSettings) -> ReaderSettings?)? = null,
|
||||
preferNativeVerticalReader: Boolean = false,
|
||||
bottomChromeExtraContent: @Composable ColumnScope.() -> Unit = {},
|
||||
useDetachedChromeLayer: Boolean = true,
|
||||
useDetachedPanelLayer: Boolean = true,
|
||||
|
|
@ -597,6 +598,21 @@ fun SharedReaderScreen(
|
|||
ttsRequestId = ttsRequestId
|
||||
)
|
||||
val renderPlan = if (settings.readingMode == ReaderReadingMode.VERTICAL) {
|
||||
if (preferNativeVerticalReader) {
|
||||
ReaderContentRenderPlan.NativeVerticalPages(
|
||||
book = readerState.book,
|
||||
pages = readerState.pages,
|
||||
currentPageIndex = readerState.currentPageIndex,
|
||||
settings = settings,
|
||||
searchQuery = session.searchQuery,
|
||||
searchOptions = session.searchOptions,
|
||||
highlightPalette = highlightPalette,
|
||||
background = background,
|
||||
foreground = foreground,
|
||||
navigationTarget = navigationTarget,
|
||||
highlights = session.highlights
|
||||
)
|
||||
} else {
|
||||
val lastChapterIndex = readerState.book.chapters.lastIndex
|
||||
val activeChapterIndex = if (lastChapterIndex >= 0) {
|
||||
readerState.currentPage?.chapterIndex?.takeIf { it in 0..lastChapterIndex }
|
||||
|
|
@ -761,6 +777,7 @@ fun SharedReaderScreen(
|
|||
navigationTarget = navigationTarget,
|
||||
highlights = session.highlights
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ReaderContentRenderPlan.NativePaginatedPages(
|
||||
visiblePages = readerState.visiblePages,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue