Add markdown support for manga descriptions (#1948)

(cherry picked from commit 4e68339783b47b0780e1b9aee643404339d35ed1)

# Conflicts:
#	CHANGELOG.md
#	gradle/libs.versions.toml
This commit is contained in:
Secozzi 2025-04-01 08:52:15 +02:00 committed by Jobobby04
parent c8039739d5
commit ad53c0de83
9 changed files with 165 additions and 44 deletions

View File

@ -239,7 +239,7 @@ dependencies {
implementation(libs.preferencektx)
// Dependency injection
implementation(libs.injekt.core)
implementation(libs.injekt)
// Image loading
implementation(platform(libs.coil.bom))
@ -257,7 +257,6 @@ dependencies {
exclude(group = "androidx.viewpager", module = "viewpager")
}
implementation(libs.insetter)
implementation(libs.bundles.richtext)
implementation(libs.richeditor.compose)
implementation(libs.aboutLibraries.compose)
implementation(libs.bundles.voyager)
@ -266,6 +265,7 @@ dependencies {
implementation(libs.compose.webview)
implementation(libs.compose.grid)
implementation(libs.reorderable)
implementation(libs.bundles.markdown)
// Logging
implementation(libs.logcat)

View File

@ -77,6 +77,8 @@ import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import com.mikepenz.markdown.model.markdownAnnotator
import com.mikepenz.markdown.model.markdownAnnotatorConfig
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.SManga
@ -95,8 +97,6 @@ import java.time.Instant
import java.time.temporal.ChronoUnit
import kotlin.math.roundToInt
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
@Composable
fun MangaInfoBox(
isTabletUi: Boolean,
@ -266,14 +266,9 @@ fun ExpandableMangaDescription(
}
val desc =
description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder)
val trimmedDescription = remember(desc) {
desc
.replace(whitespaceLineRegex, "\n")
.trimEnd()
}
MangaSummary(
expandedDescription = desc,
shrunkDescription = trimmedDescription,
description = desc,
expanded = expanded,
notes = notes,
onEditNotesClicked = onEditNotes,
@ -598,10 +593,15 @@ private fun ColumnScope.MangaContentInfo(
}
}
private val descriptionAnnotator = markdownAnnotator(
config = markdownAnnotatorConfig(
eolAsNewLine = true,
),
)
@Composable
private fun MangaSummary(
expandedDescription: String,
shrunkDescription: String,
description: String,
notes: String,
expanded: Boolean,
onEditNotesClicked: () -> Unit,
@ -629,9 +629,10 @@ private fun MangaSummary(
expanded = true,
onEditNotes = onEditNotesClicked,
)
Text(
text = expandedDescription,
style = MaterialTheme.typography.bodyMedium,
MarkdownRender(
content = description,
annotator = descriptionAnnotator,
modifier = Modifier.secondaryItemAlpha(),
)
}
},
@ -643,11 +644,9 @@ private fun MangaSummary(
onEditNotes = onEditNotesClicked,
)
SelectionContainer {
Text(
text = if (expanded) expandedDescription else shrunkDescription,
maxLines = Int.MAX_VALUE,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
MarkdownRender(
content = description,
annotator = descriptionAnnotator,
modifier = Modifier.secondaryItemAlpha(),
)
}

View File

@ -0,0 +1,130 @@
package eu.kanade.presentation.manga.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl
import com.mikepenz.markdown.compose.LocalBulletListHandler
import com.mikepenz.markdown.compose.components.markdownComponents
import com.mikepenz.markdown.compose.elements.MarkdownBulletList
import com.mikepenz.markdown.compose.elements.MarkdownDivider
import com.mikepenz.markdown.compose.elements.MarkdownOrderedList
import com.mikepenz.markdown.compose.elements.MarkdownTable
import com.mikepenz.markdown.compose.elements.MarkdownTableHeader
import com.mikepenz.markdown.compose.elements.MarkdownTableRow
import com.mikepenz.markdown.compose.elements.listDepth
import com.mikepenz.markdown.m3.Markdown
import com.mikepenz.markdown.m3.markdownTypography
import com.mikepenz.markdown.model.MarkdownAnnotator
import com.mikepenz.markdown.model.markdownAnnotator
import com.mikepenz.markdown.model.markdownPadding
import tachiyomi.presentation.core.components.material.padding
@Composable
fun MarkdownRender(
content: String,
annotator: MarkdownAnnotator = markdownAnnotator(),
modifier: Modifier = Modifier,
) {
Markdown(
content = content,
annotator = annotator,
typography = mihonMarkdownTypography(),
padding = mihonMarkdownPadding(),
components = mihonMarkdownComponents(),
imageTransformer = Coil3ImageTransformerImpl,
modifier = modifier,
)
}
@Composable
private fun mihonMarkdownPadding() = markdownPadding(
list = 0.dp,
listItemTop = 2.dp,
listItemBottom = 2.dp,
)
@Composable
private fun mihonMarkdownTypography() = markdownTypography(
h1 = MaterialTheme.typography.headlineMedium,
h2 = MaterialTheme.typography.headlineSmall,
h3 = MaterialTheme.typography.titleLarge,
h4 = MaterialTheme.typography.titleMedium,
h5 = MaterialTheme.typography.titleSmall,
h6 = MaterialTheme.typography.bodyLarge,
paragraph = MaterialTheme.typography.bodyMedium,
text = MaterialTheme.typography.bodyMedium,
ordered = MaterialTheme.typography.bodyMedium,
bullet = MaterialTheme.typography.bodyMedium,
list = MaterialTheme.typography.bodyMedium,
link = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
),
)
@Composable
private fun mihonMarkdownComponents() = markdownComponents(
horizontalRule = {
MarkdownDivider(
modifier = Modifier
.padding(vertical = MaterialTheme.padding.extraSmall)
.fillMaxWidth(),
)
},
orderedList = { ol ->
Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) {
MarkdownOrderedList(
content = ol.content,
node = ol.node,
style = ol.typography.ordered,
depth = ol.listDepth,
markerModifier = { Modifier.alignBy(FirstBaseline) },
listModifier = { Modifier.alignBy(FirstBaseline) },
)
}
},
unorderedList = { ul ->
val markers = listOf("", "", "", "")
CompositionLocalProvider(
LocalBulletListHandler provides { _, _, _, _ -> "${markers[ul.listDepth % markers.size]} " },
) {
Column(modifier = Modifier.padding(start = MaterialTheme.padding.small)) {
MarkdownBulletList(ul.content, ul.node, style = ul.typography.bullet)
}
}
},
table = { t ->
MarkdownTable(
content = t.content,
node = t.node,
style = t.typography.text,
headerBlock = { content, header, tableWidth, style ->
MarkdownTableHeader(
content = content,
header = header,
tableWidth = tableWidth,
style = style,
maxLines = Int.MAX_VALUE,
)
},
rowBlock = { content, header, tableWidth, style ->
MarkdownTableRow(
content = content,
header = header,
tableWidth = tableWidth,
style = style,
maxLines = Int.MAX_VALUE,
)
},
)
},
)

View File

@ -1,5 +1,6 @@
package eu.kanade.presentation.more
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -13,12 +14,8 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.tooling.preview.PreviewLightDark
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material3.RichText
import com.halilibo.richtext.ui.string.RichTextStringStyle
import eu.kanade.presentation.manga.components.MarkdownRender
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
@ -42,17 +39,12 @@ fun NewUpdateScreen(
rejectText = stringResource(MR.strings.action_not_now),
onRejectClick = onRejectUpdate,
) {
RichText(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = MaterialTheme.padding.large),
style = RichTextStyle(
stringStyle = RichTextStringStyle(
linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary),
),
),
) {
Markdown(content = changelogInfo)
MarkdownRender(content = changelogInfo)
TextButton(
onClick = onOpenInBrowser,

View File

@ -57,7 +57,7 @@ dependencies {
// SY -->
implementation(sylibs.xlog)
implementation(libs.injekt.core)
implementation(libs.injekt)
implementation(sylibs.exifinterface)
// SY <--
}

View File

@ -34,7 +34,7 @@ dependencies {
compileOnly(libs.compose.stablemarker)
// SY -->
implementation(libs.injekt.core)
implementation(libs.injekt)
// SY <--
testImplementation(libs.bundles.test)

View File

@ -3,14 +3,14 @@ aboutlib_version = "11.6.3"
leakcanary = "2.14"
moko = "0.24.5"
okhttp_version = "5.0.0-alpha.14"
richtext = "0.20.0"
shizuku_version = "13.1.5"
shizuku_version = "13.1.0"
sqldelight = "2.0.2"
sqlite = "2.4.0"
voyager = "1.0.1"
spotless = "7.0.2"
ktlint-core = "1.5.0"
firebase-bom = "33.11.0"
markdown = "0.33.0-b05"
[libraries]
desugar = "com.android.tools:desugar_jdk_libs:2.1.5"
@ -40,7 +40,7 @@ sqlite-android = "com.github.requery:sqlite-android:3.45.0"
preferencektx = "androidx.preference:preference-ktx:1.2.1"
injekt-core = "com.github.null2264:injekt-koin:ee267b2e27"
injekt = "com.github.null2264:injekt-koin:ee267b2e27"
coil-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.1.0" }
coil-core = { module = "io.coil-kt.coil3:coil" }
@ -53,9 +53,6 @@ image-decoder = "com.github.tachiyomiorg:image-decoder:41c059e540"
natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"
richtext-commonmark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" }
richtext-m3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", version.ref = "richtext" }
richeditor-compose = "com.mohamedrejeb.richeditor:richeditor-compose:1.0.0-rc10"
material = "com.google.android.material:material:1.12.0"
@ -104,6 +101,9 @@ voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", vers
spotless-gradle = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotless" }
ktlint-core = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint-core" }
markdown-m3 = { module = "com.mikepenz:multiplatform-markdown-renderer-m3", version.ref = "markdown" }
markdown-coil = { module = "com.mikepenz:multiplatform-markdown-renderer-coil3", version.ref = "markdown" }
[plugins]
google-services = { id = "com.google.gms.google-services", version = "4.4.2" }
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlib_version" }
@ -119,5 +119,5 @@ coil = ["coil-core", "coil-gif", "coil-compose", "coil-network-okhttp"]
shizuku = ["shizuku-api", "shizuku-provider"]
sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-android-paging"]
voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"]
richtext = ["richtext-commonmark", "richtext-m3"]
test = ["junit", "kotest-assertions", "mockk"]
markdown = ["markdown-m3", "markdown-coil"]

View File

@ -31,5 +31,5 @@ dependencies {
implementation(libs.material)
// SY <--
api(libs.injekt.core)
api(libs.injekt)
}

View File

@ -13,7 +13,7 @@ kotlin {
val commonMain by getting {
dependencies {
api(kotlinx.serialization.json)
api(libs.injekt.core)
api(libs.injekt)
api(libs.rxjava)
api(libs.jsoup)