book-reader/shared/src/commonMain/kotlin/com/aryan/reader/paginatedreader/PaginatedReaderData.kt
Aryan 83dcafa4b6
Windows ga (#358)
* 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
2026-06-02 00:51:42 +05:30

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>
)