Add markdown support for manga descriptions (#1948)
(cherry picked from commit 4e68339783b47b0780e1b9aee643404339d35ed1) # Conflicts: # CHANGELOG.md # gradle/libs.versions.toml
This commit is contained in:
parent
c8039739d5
commit
ad53c0de83
@ -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)
|
||||
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
@ -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,
|
||||
|
@ -57,7 +57,7 @@ dependencies {
|
||||
|
||||
// SY -->
|
||||
implementation(sylibs.xlog)
|
||||
implementation(libs.injekt.core)
|
||||
implementation(libs.injekt)
|
||||
implementation(sylibs.exifinterface)
|
||||
// SY <--
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ dependencies {
|
||||
compileOnly(libs.compose.stablemarker)
|
||||
|
||||
// SY -->
|
||||
implementation(libs.injekt.core)
|
||||
implementation(libs.injekt)
|
||||
// SY <--
|
||||
|
||||
testImplementation(libs.bundles.test)
|
||||
|
@ -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"]
|
||||
|
@ -31,5 +31,5 @@ dependencies {
|
||||
implementation(libs.material)
|
||||
// SY <--
|
||||
|
||||
api(libs.injekt.core)
|
||||
api(libs.injekt)
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user