* Enhance Cloud TTS with navigation controls and shared UI overlay in desktop app * Refactor Cloud TTS voice settings and remove standalone settings overlay on desktop app * Persist reader window state and improve slider interaction in desktop app * Improve PDF page transitions and refine focus management in desktop app * Refactor scrollbar interaction and adjust desktop modal focus handling * Improve PDF sidecar synchronization and cross-platform metadata compatibility * Refactor PDF annotation comment logic to shared module and implement Desktop UI * Refactor reader screen to use tap-to-toggle and full-width styling in desktop app * Refactor reader workspace layout and chrome-panel interactions * Implement global search keyboard shortcuts and focusable chrome layers * Implement flavor-specific legal links and update the About UI * Refactor reader UI controls on desktop app * Enhance desktop folder sync with background metadata extraction and improved error handling * Refactor Library UI and remove redundant Home tab in desktop app * Add custom tooltips to reader icon buttons in desktop app * Integrate app theme controls into reader interfaces on desktop * Add right-to-left pagination support and improve focus restoration on desktop app * Improve EPUB pagination geometry and diagnostic logging for layout cutoffs on desktop app * Update desktop reader defaults and implement settings migration * Implement block-based position tracking in ReaderLocator * Enhance EPUB highlighting reliability in desktop app * Add support for custom reader themes and update highlight palette logic in desktop app * Replace the Tools panel with a "More" dropdown menu and refactor account UI * Implement account profile header in desktop sidebar * Implement cloud sync reliability improvements and sidebar toggle on desktop app * Improve EPUB annotation synchronization and highlight mapping accuracy in desktop app * Integrate WebView2 for EPUB vertical rendering on Windows * Refactor reader layout logic and enhance WebView2 diagnostics * Improve vertical reading layout and WebView2 resizing on Desktop * Refine vertical reading mode layout and margin handling * Enhance reader locator precision and Desktop mode-switching reliability * Implement chapter-level caching and warm-start pagination in desktop app * Replace bundled KCEF with native system webviews via SWT * Refactor EPUB page info bar visibility and layout logic * Improve PDF toolbar persistence and fix tab reactivation logic * Enable multi-selection and bulk operations for custom fonts * Refactor instrumentation tests * Add EPUB UI test fixture and initial instrumentation tests * Expand EpubReader UI tests and improve accessibility * Add instrumentation tests and test tags for library and reader screens * Enhance OPDS parser logic and catalog integration * Add support for toggling local synchronization on a per-folder basis. * Implement tri-state sizing for the TTS overlay * Persist TTS overlay size across sessions * Refactor reader brightness control and add incremental step buttons * Improve CSS support, pagination control, and style-aware semantic caching * Improve link handling, interaction, and diagnostics in the paginated reader * crash fixes * Implement persistent pending removal for external files * Implement book-specific word replacements * Add native vertical reading mode with custom renderer * Implement text selection and navigation improvements for the native vertical reader * Implement locator-based navigation and improved vertical scrolling in native vertical mode in epub * Implement lazy loading and chapter prefetching for native vertical reader * Improve window lifecycle and disposal handling on Desktop * Optimize vertical reading performance in desktop app * Enhance TTS start accuracy and diagnostic logging on desktop * Refactor AI settings visibility on desktop * Improve pagination height measurement and enhance cutoff diagnostics * Implement lifecycle management and improve justified text splitting for pagination * Refine AI usage tracking and force AI feature visibility on Desktop * Add descriptive context comments and usage examples to string and plural resources. * Optimize performance and memory usage in search and state mapping * Replace reader page sliders with minimal slider and navigation controls * Add support for CBT comic archives * Harden file path validation and XML parsing to prevent security vulnerabilities * Implement local account profile caching and optimize desktop performance * Improve desktop persistence reliability and add Linux secure storage support * Improved PDF zoom stability and layout prediction during zoom commits * Improved PDF spread layout prediction, reader focus restoration, and account profile caching * Enhance highlight precision and scoping using block-local offsets and CFIs * Enhance cloud book content synchronization and background downloads * Implement granular timestamp tracking for reading positions and PDF annotations * Restrict diagnostic logging and stack traces to debug builds * Refine PDF page gaps and reader chrome interaction logic * Refactor PDF highlight rendering and overhaul Desktop sidebar UI * Implement a new interaction dock and undo/redo history for PDF annotations in desktop * Enhance PDF color picker and improve navigation scroll restoration * Add highlight palette customization and improve selection menu UI in desktop app epub reader * Enhance desktop shelf management and library organization
448 lines
22 KiB
Kotlin
448 lines
22 KiB
Kotlin
/*
|
|
* Episteme Reader - A native Android document reader.
|
|
* Copyright (C) 2026 Episteme
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* mail: epistemereader@gmail.com
|
|
*/
|
|
@file:OptIn(ExperimentalSerializationApi::class)
|
|
package com.aryan.reader.paginatedreader
|
|
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.graphics.isSpecified
|
|
import androidx.compose.ui.text.AnnotatedString
|
|
import androidx.compose.ui.text.ParagraphStyle
|
|
import androidx.compose.ui.text.SpanStyle
|
|
import androidx.compose.ui.text.font.FontStyle
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.text.style.TextAlign
|
|
import androidx.compose.ui.unit.Dp
|
|
import androidx.compose.ui.unit.TextUnit
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.unit.isSpecified
|
|
import com.aryan.reader.paginatedreader.serialization.AnnotatedStringSerializer
|
|
import com.aryan.reader.paginatedreader.serialization.ColorSerializer
|
|
import com.aryan.reader.paginatedreader.serialization.DpSerializer
|
|
import com.aryan.reader.paginatedreader.serialization.ParagraphStyleSerializer
|
|
import com.aryan.reader.paginatedreader.serialization.SpanStyleSerializer
|
|
import com.aryan.reader.paginatedreader.serialization.TextAlignSerializer
|
|
import com.aryan.reader.paginatedreader.serialization.TextUnitSerializer
|
|
import kotlinx.serialization.ExperimentalSerializationApi
|
|
import kotlinx.serialization.Serializable
|
|
import kotlinx.serialization.protobuf.ProtoNumber
|
|
|
|
@Serializable
|
|
data class BlockStyle(
|
|
@ProtoNumber(1) val margin: BoxBorders = BoxBorders(),
|
|
@ProtoNumber(2) val padding: BoxBorders = BoxBorders(),
|
|
@ProtoNumber(3) @Serializable(with = DpSerializer::class) val width: Dp = Dp.Unspecified,
|
|
@ProtoNumber(4) @Serializable(with = DpSerializer::class) val maxWidth: Dp = Dp.Unspecified,
|
|
@ProtoNumber(5) @Serializable(with = DpSerializer::class) val height: Dp = Dp.Unspecified,
|
|
@ProtoNumber(6) @Serializable(with = ColorSerializer::class) val backgroundColor: Color = Color.Unspecified,
|
|
@ProtoNumber(7) val borderTop: BorderStyle? = null,
|
|
@ProtoNumber(8) val borderRight: BorderStyle? = null,
|
|
@ProtoNumber(9) val borderBottom: BorderStyle? = null,
|
|
@ProtoNumber(10) val borderLeft: BorderStyle? = null,
|
|
@ProtoNumber(11) val listStyleType: String? = null,
|
|
@ProtoNumber(12) val listStyleImage: String? = null,
|
|
@ProtoNumber(13) val pageBreakInsideAvoid: Boolean = false,
|
|
@ProtoNumber(14) val pageBreakAfterAvoid: Boolean = false,
|
|
@ProtoNumber(15) val boxSizing: String? = null,
|
|
@ProtoNumber(16) val float: String? = null,
|
|
@ProtoNumber(17) val clear: String? = null,
|
|
@ProtoNumber(18) val position: String? = null,
|
|
@ProtoNumber(19) @Serializable(with = DpSerializer::class) val top: Dp = Dp.Unspecified,
|
|
@ProtoNumber(20) @Serializable(with = DpSerializer::class) val right: Dp = Dp.Unspecified,
|
|
@ProtoNumber(21) @Serializable(with = DpSerializer::class) val bottom: Dp = Dp.Unspecified,
|
|
@ProtoNumber(22) @Serializable(with = DpSerializer::class) val left: Dp = Dp.Unspecified,
|
|
@ProtoNumber(23) val display: String? = null,
|
|
@ProtoNumber(24) val flexDirection: String? = null,
|
|
@ProtoNumber(25) val justifyContent: String? = null,
|
|
@ProtoNumber(26) val alignItems: String? = null,
|
|
@ProtoNumber(27) val horizontalAlign: String? = null,
|
|
@ProtoNumber(28) val filter: String? = null,
|
|
@ProtoNumber(29) val borderCollapse: String? = null,
|
|
@ProtoNumber(30) @Serializable(with = DpSerializer::class) val borderTopLeftRadius: Dp = 0.dp,
|
|
@ProtoNumber(31) @Serializable(with = DpSerializer::class) val borderTopRightRadius: Dp = 0.dp,
|
|
@ProtoNumber(32) @Serializable(with = DpSerializer::class) val borderBottomRightRadius: Dp = 0.dp,
|
|
@ProtoNumber(33) @Serializable(with = DpSerializer::class) val borderBottomLeftRadius: Dp = 0.dp,
|
|
@ProtoNumber(34) @Serializable(with = DpSerializer::class) val borderSpacing: Dp = 0.dp,
|
|
@ProtoNumber(35) @Serializable(with = DpSerializer::class) val minWidth: Dp = Dp.Unspecified,
|
|
@ProtoNumber(36) @Serializable(with = DpSerializer::class) val minHeight: Dp = Dp.Unspecified,
|
|
@ProtoNumber(37) @Serializable(with = DpSerializer::class) val maxHeight: Dp = Dp.Unspecified,
|
|
@ProtoNumber(38) val overflow: String? = null,
|
|
@ProtoNumber(39) val breakBefore: String? = null,
|
|
@ProtoNumber(40) val breakAfter: String? = null,
|
|
@ProtoNumber(41) val breakInside: String? = null,
|
|
@ProtoNumber(42) val widows: Int = 2,
|
|
@ProtoNumber(43) val orphans: Int = 2,
|
|
@ProtoNumber(44) val visibility: String? = null,
|
|
@ProtoNumber(45) val objectFit: String? = null,
|
|
@ProtoNumber(46) val objectPosition: String? = null,
|
|
@ProtoNumber(47) val backgroundImage: String? = null
|
|
) {
|
|
fun merge(other: BlockStyle): BlockStyle {
|
|
return BlockStyle(
|
|
margin = BoxBorders(
|
|
top = if (other.margin.top != 0.dp) other.margin.top else this.margin.top,
|
|
bottom = if (other.margin.bottom != 0.dp) other.margin.bottom else this.margin.bottom,
|
|
left = if (other.margin.left != 0.dp) other.margin.left else this.margin.left,
|
|
right = if (other.margin.right != 0.dp) other.margin.right else this.margin.right
|
|
),
|
|
padding = BoxBorders(
|
|
top = if (other.padding.top != 0.dp) other.padding.top else this.padding.top,
|
|
bottom = if (other.padding.bottom != 0.dp) other.padding.bottom else this.padding.bottom,
|
|
left = if (other.padding.left != 0.dp) other.padding.left else this.padding.left,
|
|
right = if (other.padding.right != 0.dp) other.padding.right else this.padding.right
|
|
),
|
|
width = if (other.width != Dp.Unspecified) other.width else this.width,
|
|
maxWidth = if (other.maxWidth != Dp.Unspecified) other.maxWidth else this.maxWidth,
|
|
height = if (other.height != Dp.Unspecified) other.height else this.height,
|
|
backgroundColor = if (other.backgroundColor.isSpecified) other.backgroundColor else this.backgroundColor,
|
|
borderTop = other.borderTop ?: this.borderTop,
|
|
borderRight = other.borderRight ?: this.borderRight,
|
|
borderBottom = other.borderBottom ?: this.borderBottom,
|
|
borderLeft = other.borderLeft ?: this.borderLeft,
|
|
borderTopLeftRadius = if (other.borderTopLeftRadius != 0.dp) other.borderTopLeftRadius else this.borderTopLeftRadius,
|
|
borderTopRightRadius = if (other.borderTopRightRadius != 0.dp) other.borderTopRightRadius else this.borderTopRightRadius,
|
|
borderBottomRightRadius = if (other.borderBottomRightRadius != 0.dp) other.borderBottomRightRadius else this.borderBottomRightRadius,
|
|
borderBottomLeftRadius = if (other.borderBottomLeftRadius != 0.dp) other.borderBottomLeftRadius else this.borderBottomLeftRadius,
|
|
listStyleType = other.listStyleType ?: this.listStyleType,
|
|
listStyleImage = other.listStyleImage ?: this.listStyleImage,
|
|
pageBreakInsideAvoid = this.pageBreakInsideAvoid || other.pageBreakInsideAvoid,
|
|
pageBreakAfterAvoid = this.pageBreakAfterAvoid || other.pageBreakAfterAvoid,
|
|
boxSizing = other.boxSizing ?: this.boxSizing,
|
|
float = other.float ?: this.float,
|
|
clear = other.clear ?: this.clear,
|
|
position = other.position ?: this.position,
|
|
top = if (other.top.isSpecified) other.top else this.top,
|
|
right = if (other.right.isSpecified) other.right else this.right,
|
|
bottom = if (other.bottom.isSpecified) other.bottom else this.bottom,
|
|
left = if (other.left.isSpecified) other.left else this.left,
|
|
display = other.display ?: this.display,
|
|
flexDirection = other.flexDirection ?: this.flexDirection,
|
|
justifyContent = other.justifyContent ?: this.justifyContent,
|
|
alignItems = other.alignItems ?: this.alignItems,
|
|
horizontalAlign = other.horizontalAlign ?: this.horizontalAlign,
|
|
filter = other.filter ?: this.filter,
|
|
borderCollapse = other.borderCollapse ?: this.borderCollapse,
|
|
borderSpacing = if (other.borderSpacing != 0.dp) other.borderSpacing else this.borderSpacing,
|
|
minWidth = if (other.minWidth.isSpecified) other.minWidth else this.minWidth,
|
|
minHeight = if (other.minHeight.isSpecified) other.minHeight else this.minHeight,
|
|
maxHeight = if (other.maxHeight.isSpecified) other.maxHeight else this.maxHeight,
|
|
overflow = other.overflow ?: this.overflow,
|
|
breakBefore = other.breakBefore ?: this.breakBefore,
|
|
breakAfter = other.breakAfter ?: this.breakAfter,
|
|
breakInside = other.breakInside ?: this.breakInside,
|
|
widows = if (other.widows != 2) other.widows else this.widows,
|
|
orphans = if (other.orphans != 2) other.orphans else this.orphans,
|
|
visibility = other.visibility ?: this.visibility,
|
|
objectFit = other.objectFit ?: this.objectFit,
|
|
objectPosition = other.objectPosition ?: this.objectPosition,
|
|
backgroundImage = other.backgroundImage ?: this.backgroundImage
|
|
)
|
|
}
|
|
}
|
|
|
|
@Serializable
|
|
data class BoxBorders(
|
|
@ProtoNumber(1) @Serializable(with = DpSerializer::class) val top: Dp = 0.dp,
|
|
@ProtoNumber(2) @Serializable(with = DpSerializer::class) val right: Dp = 0.dp,
|
|
@ProtoNumber(3) @Serializable(with = DpSerializer::class) val bottom: Dp = 0.dp,
|
|
@ProtoNumber(4) @Serializable(with = DpSerializer::class) val left: Dp = 0.dp
|
|
)
|
|
|
|
@Serializable
|
|
data class BorderStyle(
|
|
@ProtoNumber(1) @Serializable(with = DpSerializer::class) val width: Dp = 0.dp,
|
|
@ProtoNumber(2) @Serializable(with = ColorSerializer::class) val color: Color = Color.Transparent,
|
|
@ProtoNumber(3) val style: String = "solid"
|
|
)
|
|
|
|
@Serializable
|
|
sealed interface ContentBlock {
|
|
val style: BlockStyle
|
|
val elementId: String?
|
|
val cfi: String?
|
|
val blockIndex: Int
|
|
val expectedHeight: Int
|
|
}
|
|
|
|
sealed interface TextContentBlock : ContentBlock {
|
|
val content: AnnotatedString
|
|
val startCharOffsetInSource: Int
|
|
val endCharOffsetInSource: Int
|
|
}
|
|
|
|
@Serializable
|
|
data class ParagraphBlock(
|
|
@ProtoNumber(1) @Serializable(with = AnnotatedStringSerializer::class) override val content: AnnotatedString,
|
|
@ProtoNumber(2) @Serializable(with = TextAlignSerializer::class) val textAlign: TextAlign? = null,
|
|
@ProtoNumber(3) override val style: BlockStyle = BlockStyle(),
|
|
@ProtoNumber(4) override val elementId: String? = null,
|
|
@ProtoNumber(5) override val cfi: String? = null,
|
|
@ProtoNumber(6) override val startCharOffsetInSource: Int = 0,
|
|
@ProtoNumber(7) override val endCharOffsetInSource: Int = -1,
|
|
@ProtoNumber(8) override val blockIndex: Int,
|
|
@ProtoNumber(9) override val expectedHeight: Int = 0
|
|
) : TextContentBlock
|
|
|
|
@Serializable
|
|
data class ImageBlock(
|
|
@ProtoNumber(1) val path: String,
|
|
@ProtoNumber(2) val altText: String?,
|
|
@ProtoNumber(3) val intrinsicWidth: Float? = null,
|
|
@ProtoNumber(4) val intrinsicHeight: Float? = null,
|
|
@ProtoNumber(5) override val style: BlockStyle = BlockStyle(),
|
|
@ProtoNumber(6) override val elementId: String? = null,
|
|
@ProtoNumber(7) override val cfi: String? = null,
|
|
@ProtoNumber(8) val invertOnDarkTheme: Boolean = false,
|
|
@ProtoNumber(9) override val blockIndex: Int,
|
|
@ProtoNumber(10) override val expectedHeight: Int = 0
|
|
) : ContentBlock
|
|
|
|
@Serializable
|
|
data class HeaderBlock(
|
|
@ProtoNumber(1) val level: Int,
|
|
@ProtoNumber(2) @Serializable(with = AnnotatedStringSerializer::class) override val content: AnnotatedString,
|
|
@ProtoNumber(3) @Serializable(with = TextAlignSerializer::class) val textAlign: TextAlign? = null,
|
|
@ProtoNumber(4) override val style: BlockStyle = BlockStyle(),
|
|
@ProtoNumber(5) override val elementId: String? = null,
|
|
@ProtoNumber(6) override val cfi: String? = null,
|
|
@ProtoNumber(7) override val startCharOffsetInSource: Int = 0,
|
|
@ProtoNumber(8) override val endCharOffsetInSource: Int = -1,
|
|
@ProtoNumber(9) override val blockIndex: Int,
|
|
@ProtoNumber(10) override val expectedHeight: Int = 0
|
|
) : TextContentBlock
|
|
|
|
@Serializable
|
|
data class SpacerBlock(
|
|
@ProtoNumber(1) @Serializable(with = DpSerializer::class) val height: Dp = 8.dp,
|
|
@ProtoNumber(2) override val style: BlockStyle = BlockStyle(),
|
|
@ProtoNumber(3) override val elementId: String? = null,
|
|
@ProtoNumber(4) override val cfi: String? = null,
|
|
@ProtoNumber(5) override val blockIndex: Int,
|
|
@ProtoNumber(6) override val expectedHeight: Int = 0
|
|
) : ContentBlock
|
|
|
|
@Serializable
|
|
data class QuoteBlock(
|
|
@ProtoNumber(1) @Serializable(with = AnnotatedStringSerializer::class) override val content: AnnotatedString,
|
|
@ProtoNumber(2) @Serializable(with = TextAlignSerializer::class) val textAlign: TextAlign? = null,
|
|
@ProtoNumber(3) override val style: BlockStyle = BlockStyle(),
|
|
@ProtoNumber(4) override val elementId: String? = null,
|
|
@ProtoNumber(5) override val cfi: String? = null,
|
|
@ProtoNumber(6) override val startCharOffsetInSource: Int = 0,
|
|
@ProtoNumber(7) override val endCharOffsetInSource: Int = -1,
|
|
@ProtoNumber(8) override val blockIndex: Int,
|
|
@ProtoNumber(9) override val expectedHeight: Int = 0
|
|
) : TextContentBlock
|
|
|
|
@Serializable
|
|
data class ListItemBlock(
|
|
@ProtoNumber(1) @Serializable(with = AnnotatedStringSerializer::class) override val content: AnnotatedString,
|
|
@ProtoNumber(2) val itemMarker: String?,
|
|
@ProtoNumber(3) val itemMarkerImage: String? = null,
|
|
@ProtoNumber(4) override val style: BlockStyle,
|
|
@ProtoNumber(5) override val elementId: String? = null,
|
|
@ProtoNumber(6) override val cfi: String? = null,
|
|
@ProtoNumber(7) override val startCharOffsetInSource: Int = 0,
|
|
@ProtoNumber(8) override val endCharOffsetInSource: Int = -1,
|
|
@ProtoNumber(9) override val blockIndex: Int,
|
|
@ProtoNumber(10) override val expectedHeight: Int = 0
|
|
) : TextContentBlock
|
|
|
|
@Serializable
|
|
data class TableCell(
|
|
@ProtoNumber(1) val content: List<ContentBlock>,
|
|
@ProtoNumber(2) val isHeader: Boolean = false,
|
|
@ProtoNumber(3) val style: CssStyle = CssStyle(),
|
|
@ProtoNumber(4) val colspan: Int = 1
|
|
)
|
|
|
|
@Serializable
|
|
data class TableBlock(
|
|
@ProtoNumber(1) val rows: List<List<TableCell>>,
|
|
@ProtoNumber(2) override val style: BlockStyle = BlockStyle(),
|
|
@ProtoNumber(3) override val elementId: String? = null,
|
|
@ProtoNumber(4) override val cfi: String? = null,
|
|
@ProtoNumber(5) override val blockIndex: Int,
|
|
@ProtoNumber(6) override val expectedHeight: Int = 0
|
|
) : ContentBlock
|
|
|
|
@Serializable
|
|
data class MathBlock(
|
|
@ProtoNumber(1) val svgContent: String?,
|
|
@ProtoNumber(2) val altText: String?,
|
|
@ProtoNumber(3) override val style: BlockStyle,
|
|
@ProtoNumber(4) override val elementId: String?,
|
|
@ProtoNumber(5) override val cfi: String?,
|
|
@ProtoNumber(6) val svgWidth: String? = null,
|
|
@ProtoNumber(7) val svgHeight: String? = null,
|
|
@ProtoNumber(8) val svgViewBox: String? = null,
|
|
@ProtoNumber(9) val isFromMathJax: Boolean = false,
|
|
@ProtoNumber(10) override val blockIndex: Int,
|
|
@ProtoNumber(11) override val expectedHeight: Int = 0
|
|
) : ContentBlock
|
|
|
|
@Serializable
|
|
data class WrappingContentBlock(
|
|
@ProtoNumber(1) val floatedImage: ImageBlock,
|
|
@ProtoNumber(2) val paragraphsToWrap: List<ParagraphBlock>,
|
|
@ProtoNumber(3) override val style: BlockStyle = BlockStyle(),
|
|
@ProtoNumber(4) override val elementId: String? = null,
|
|
@ProtoNumber(5) override val cfi: String? = null,
|
|
@ProtoNumber(6) override val blockIndex: Int,
|
|
@ProtoNumber(7) override val expectedHeight: Int = 0
|
|
) : ContentBlock
|
|
|
|
@Serializable
|
|
data class TextEmphasis(
|
|
@ProtoNumber(1) val style: String? = null,
|
|
@ProtoNumber(2) val fill: String? = null,
|
|
@ProtoNumber(3) @Serializable(with = ColorSerializer::class) val color: Color = Color.Unspecified,
|
|
@ProtoNumber(4) val position: String? = null
|
|
)
|
|
|
|
@Serializable
|
|
data class CssStyle(
|
|
@ProtoNumber(1) @Serializable(with = SpanStyleSerializer::class) val spanStyle: SpanStyle = SpanStyle(),
|
|
@ProtoNumber(2) @Serializable(with = ParagraphStyleSerializer::class) val paragraphStyle: ParagraphStyle = ParagraphStyle(),
|
|
@ProtoNumber(3) val blockStyle: BlockStyle = BlockStyle(),
|
|
@ProtoNumber(4) val fontFamilies: List<String> = emptyList(),
|
|
@ProtoNumber(5) val display: String? = null,
|
|
@ProtoNumber(6) @Serializable(with = TextUnitSerializer::class) val fontSize: TextUnit = TextUnit.Unspecified,
|
|
@ProtoNumber(7) val textTransform: String? = null,
|
|
@ProtoNumber(8) val boxSizing: String? = null,
|
|
@ProtoNumber(9) val content: String? = null,
|
|
@ProtoNumber(10) val hyphens: String? = null,
|
|
@ProtoNumber(11) val fontVariantNumeric: String? = null,
|
|
@ProtoNumber(12) val textEmphasis: TextEmphasis? = null,
|
|
@ProtoNumber(13) @Serializable(with = TextUnitSerializer::class) val wordSpacing: TextUnit = TextUnit.Unspecified,
|
|
@ProtoNumber(14) val textDecorationStyle: String? = null,
|
|
@ProtoNumber(15) @Serializable(with = ColorSerializer::class) val textDecorationColor: Color = Color.Unspecified,
|
|
@ProtoNumber(16) @Serializable(with = DpSerializer::class) val textUnderlineOffset: Dp = Dp.Unspecified,
|
|
@ProtoNumber(17) val whiteSpace: String? = null,
|
|
@ProtoNumber(18) val verticalAlign: String? = null,
|
|
@ProtoNumber(19) val customProperties: Map<String, String> = emptyMap()
|
|
) {
|
|
fun merge(other: CssStyle): CssStyle {
|
|
return CssStyle(
|
|
spanStyle = this.spanStyle.merge(other.spanStyle),
|
|
paragraphStyle = this.paragraphStyle.merge(other.paragraphStyle),
|
|
blockStyle = this.blockStyle.merge(other.blockStyle),
|
|
fontFamilies = other.fontFamilies.takeIf { it.isNotEmpty() } ?: this.fontFamilies,
|
|
display = other.display ?: this.display,
|
|
fontSize = if (other.fontSize.isSpecified) other.fontSize else this.fontSize,
|
|
textTransform = other.textTransform ?: this.textTransform,
|
|
boxSizing = other.boxSizing ?: this.boxSizing,
|
|
content = other.content ?: this.content,
|
|
hyphens = other.hyphens ?: this.hyphens,
|
|
fontVariantNumeric = other.fontVariantNumeric ?: this.fontVariantNumeric,
|
|
textEmphasis = other.textEmphasis ?: this.textEmphasis,
|
|
wordSpacing = if (other.wordSpacing.isSpecified) other.wordSpacing else this.wordSpacing,
|
|
textDecorationStyle = other.textDecorationStyle ?: this.textDecorationStyle,
|
|
textDecorationColor = if (other.textDecorationColor.isSpecified) other.textDecorationColor else this.textDecorationColor,
|
|
textUnderlineOffset = if (other.textUnderlineOffset.isSpecified) other.textUnderlineOffset else this.textUnderlineOffset,
|
|
whiteSpace = other.whiteSpace ?: this.whiteSpace,
|
|
verticalAlign = other.verticalAlign ?: this.verticalAlign,
|
|
customProperties = this.customProperties + other.customProperties
|
|
)
|
|
}
|
|
}
|
|
|
|
@Serializable
|
|
data class CssSelector(
|
|
@ProtoNumber(1) val selector: String,
|
|
@ProtoNumber(2) val specificity: Int
|
|
)
|
|
|
|
@Serializable
|
|
data class CssRule(
|
|
@ProtoNumber(1) val selector: CssSelector,
|
|
@ProtoNumber(2) val style: CssStyle,
|
|
@ProtoNumber(3) val pseudoElement: String? = null,
|
|
@ProtoNumber(4) val sourceOrder: Int = 0
|
|
)
|
|
|
|
@Serializable
|
|
data class FontFaceInfo(
|
|
@ProtoNumber(1) val fontFamily: String,
|
|
@ProtoNumber(2) val src: String,
|
|
@ProtoNumber(3) @Serializable(with = com.aryan.reader.paginatedreader.serialization.FontWeightSerializer::class) val fontWeight: FontWeight?,
|
|
@ProtoNumber(4) @Serializable(with = com.aryan.reader.paginatedreader.serialization.FontStyleSerializer::class) val fontStyle: FontStyle?
|
|
)
|
|
|
|
@Serializable
|
|
data class Page(
|
|
@ProtoNumber(1) val content: List<ContentBlock>
|
|
)
|
|
|
|
@Serializable
|
|
data class FlexContainerBlock(
|
|
@ProtoNumber(1) val children: List<ContentBlock>,
|
|
@ProtoNumber(2) override val style: BlockStyle = BlockStyle(),
|
|
@ProtoNumber(3) override val elementId: String? = null,
|
|
@ProtoNumber(4) override val cfi: String? = null,
|
|
@ProtoNumber(5) override val blockIndex: Int,
|
|
@ProtoNumber(6) override val expectedHeight: Int = 0
|
|
) : ContentBlock
|
|
|
|
@Serializable
|
|
data class OptimizedCssRules(
|
|
@ProtoNumber(1) val byTag: Map<String, List<CssRule>> = emptyMap(),
|
|
@ProtoNumber(2) val byClass: Map<String, List<CssRule>> = emptyMap(),
|
|
@ProtoNumber(3) val byId: Map<String, List<CssRule>> = emptyMap(),
|
|
@ProtoNumber(4) val otherComplex: List<CssRule> = emptyList(),
|
|
@ProtoNumber(5) val allRules: List<CssRule> = emptyList()
|
|
) {
|
|
fun merge(other: OptimizedCssRules): OptimizedCssRules {
|
|
fun mergeMap(
|
|
m1: Map<String, List<CssRule>>,
|
|
m2: Map<String, List<CssRule>>
|
|
): Map<String, List<CssRule>> {
|
|
if (m1.isEmpty()) return m2
|
|
if (m2.isEmpty()) return m1
|
|
|
|
val result = LinkedHashMap(m1)
|
|
for ((key, value) in m2) {
|
|
val existing = result[key]
|
|
if (existing != null) {
|
|
result[key] = existing + value
|
|
} else {
|
|
result[key] = value
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
return OptimizedCssRules(
|
|
byTag = mergeMap(this.byTag, other.byTag),
|
|
byClass = mergeMap(this.byClass, other.byClass),
|
|
byId = mergeMap(this.byId, other.byId),
|
|
otherComplex = this.otherComplex + other.otherComplex,
|
|
allRules = this.toFlatList() + other.toFlatList()
|
|
)
|
|
}
|
|
|
|
fun toFlatList(): List<CssRule> {
|
|
return allRules.ifEmpty { byTag.values.flatten() + byClass.values.flatten() + byId.values.flatten() + otherComplex }
|
|
}
|
|
}
|
|
|
|
data class OptimizedCssParseResult(
|
|
val rules: OptimizedCssRules,
|
|
val fontFaces: List<FontFaceInfo>
|
|
)
|