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)
|
implementation(libs.preferencektx)
|
||||||
|
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
implementation(libs.injekt.core)
|
implementation(libs.injekt)
|
||||||
|
|
||||||
// Image loading
|
// Image loading
|
||||||
implementation(platform(libs.coil.bom))
|
implementation(platform(libs.coil.bom))
|
||||||
@ -257,7 +257,6 @@ dependencies {
|
|||||||
exclude(group = "androidx.viewpager", module = "viewpager")
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
||||||
}
|
}
|
||||||
implementation(libs.insetter)
|
implementation(libs.insetter)
|
||||||
implementation(libs.bundles.richtext)
|
|
||||||
implementation(libs.richeditor.compose)
|
implementation(libs.richeditor.compose)
|
||||||
implementation(libs.aboutLibraries.compose)
|
implementation(libs.aboutLibraries.compose)
|
||||||
implementation(libs.bundles.voyager)
|
implementation(libs.bundles.voyager)
|
||||||
@ -266,6 +265,7 @@ dependencies {
|
|||||||
implementation(libs.compose.webview)
|
implementation(libs.compose.webview)
|
||||||
implementation(libs.compose.grid)
|
implementation(libs.compose.grid)
|
||||||
implementation(libs.reorderable)
|
implementation(libs.reorderable)
|
||||||
|
implementation(libs.bundles.markdown)
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation(libs.logcat)
|
implementation(libs.logcat)
|
||||||
|
@ -77,6 +77,8 @@ import androidx.compose.ui.unit.sp
|
|||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import coil3.request.crossfade
|
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.presentation.components.DropdownMenu
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
@ -95,8 +97,6 @@ import java.time.Instant
|
|||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private val whitespaceLineRegex = Regex("[\\r\\n]{2,}", setOf(RegexOption.MULTILINE))
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaInfoBox(
|
fun MangaInfoBox(
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
@ -266,14 +266,9 @@ fun ExpandableMangaDescription(
|
|||||||
}
|
}
|
||||||
val desc =
|
val desc =
|
||||||
description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder)
|
description.takeIf { !it.isNullOrBlank() } ?: stringResource(MR.strings.description_placeholder)
|
||||||
val trimmedDescription = remember(desc) {
|
|
||||||
desc
|
|
||||||
.replace(whitespaceLineRegex, "\n")
|
|
||||||
.trimEnd()
|
|
||||||
}
|
|
||||||
MangaSummary(
|
MangaSummary(
|
||||||
expandedDescription = desc,
|
description = desc,
|
||||||
shrunkDescription = trimmedDescription,
|
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
notes = notes,
|
notes = notes,
|
||||||
onEditNotesClicked = onEditNotes,
|
onEditNotesClicked = onEditNotes,
|
||||||
@ -598,10 +593,15 @@ private fun ColumnScope.MangaContentInfo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val descriptionAnnotator = markdownAnnotator(
|
||||||
|
config = markdownAnnotatorConfig(
|
||||||
|
eolAsNewLine = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MangaSummary(
|
private fun MangaSummary(
|
||||||
expandedDescription: String,
|
description: String,
|
||||||
shrunkDescription: String,
|
|
||||||
notes: String,
|
notes: String,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
onEditNotesClicked: () -> Unit,
|
onEditNotesClicked: () -> Unit,
|
||||||
@ -629,9 +629,10 @@ private fun MangaSummary(
|
|||||||
expanded = true,
|
expanded = true,
|
||||||
onEditNotes = onEditNotesClicked,
|
onEditNotes = onEditNotesClicked,
|
||||||
)
|
)
|
||||||
Text(
|
MarkdownRender(
|
||||||
text = expandedDescription,
|
content = description,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
annotator = descriptionAnnotator,
|
||||||
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -643,11 +644,9 @@ private fun MangaSummary(
|
|||||||
onEditNotes = onEditNotesClicked,
|
onEditNotes = onEditNotesClicked,
|
||||||
)
|
)
|
||||||
SelectionContainer {
|
SelectionContainer {
|
||||||
Text(
|
MarkdownRender(
|
||||||
text = if (expanded) expandedDescription else shrunkDescription,
|
content = description,
|
||||||
maxLines = Int.MAX_VALUE,
|
annotator = descriptionAnnotator,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
modifier = Modifier.secondaryItemAlpha(),
|
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
|
package eu.kanade.presentation.more
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@ -13,12 +14,8 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.SpanStyle
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import com.halilibo.richtext.markdown.Markdown
|
import eu.kanade.presentation.manga.components.MarkdownRender
|
||||||
import com.halilibo.richtext.ui.RichTextStyle
|
|
||||||
import com.halilibo.richtext.ui.material3.RichText
|
|
||||||
import com.halilibo.richtext.ui.string.RichTextStringStyle
|
|
||||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
@ -42,17 +39,12 @@ fun NewUpdateScreen(
|
|||||||
rejectText = stringResource(MR.strings.action_not_now),
|
rejectText = stringResource(MR.strings.action_not_now),
|
||||||
onRejectClick = onRejectUpdate,
|
onRejectClick = onRejectUpdate,
|
||||||
) {
|
) {
|
||||||
RichText(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = MaterialTheme.padding.large),
|
.padding(vertical = MaterialTheme.padding.large),
|
||||||
style = RichTextStyle(
|
|
||||||
stringStyle = RichTextStringStyle(
|
|
||||||
linkStyle = SpanStyle(color = MaterialTheme.colorScheme.primary),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
) {
|
) {
|
||||||
Markdown(content = changelogInfo)
|
MarkdownRender(content = changelogInfo)
|
||||||
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onOpenInBrowser,
|
onClick = onOpenInBrowser,
|
||||||
|
@ -57,7 +57,7 @@ dependencies {
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
implementation(sylibs.xlog)
|
implementation(sylibs.xlog)
|
||||||
implementation(libs.injekt.core)
|
implementation(libs.injekt)
|
||||||
implementation(sylibs.exifinterface)
|
implementation(sylibs.exifinterface)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ dependencies {
|
|||||||
compileOnly(libs.compose.stablemarker)
|
compileOnly(libs.compose.stablemarker)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
implementation(libs.injekt.core)
|
implementation(libs.injekt)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
testImplementation(libs.bundles.test)
|
testImplementation(libs.bundles.test)
|
||||||
|
@ -3,14 +3,14 @@ aboutlib_version = "11.6.3"
|
|||||||
leakcanary = "2.14"
|
leakcanary = "2.14"
|
||||||
moko = "0.24.5"
|
moko = "0.24.5"
|
||||||
okhttp_version = "5.0.0-alpha.14"
|
okhttp_version = "5.0.0-alpha.14"
|
||||||
richtext = "0.20.0"
|
shizuku_version = "13.1.0"
|
||||||
shizuku_version = "13.1.5"
|
|
||||||
sqldelight = "2.0.2"
|
sqldelight = "2.0.2"
|
||||||
sqlite = "2.4.0"
|
sqlite = "2.4.0"
|
||||||
voyager = "1.0.1"
|
voyager = "1.0.1"
|
||||||
spotless = "7.0.2"
|
spotless = "7.0.2"
|
||||||
ktlint-core = "1.5.0"
|
ktlint-core = "1.5.0"
|
||||||
firebase-bom = "33.11.0"
|
firebase-bom = "33.11.0"
|
||||||
|
markdown = "0.33.0-b05"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
desugar = "com.android.tools:desugar_jdk_libs:2.1.5"
|
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"
|
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-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.1.0" }
|
||||||
coil-core = { module = "io.coil-kt.coil3:coil" }
|
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"
|
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"
|
richeditor-compose = "com.mohamedrejeb.richeditor:richeditor-compose:1.0.0-rc10"
|
||||||
|
|
||||||
material = "com.google.android.material:material:1.12.0"
|
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" }
|
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" }
|
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]
|
[plugins]
|
||||||
google-services = { id = "com.google.gms.google-services", version = "4.4.2" }
|
google-services = { id = "com.google.gms.google-services", version = "4.4.2" }
|
||||||
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlib_version" }
|
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"]
|
shizuku = ["shizuku-api", "shizuku-provider"]
|
||||||
sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-android-paging"]
|
sqldelight = ["sqldelight-android-driver", "sqldelight-coroutines", "sqldelight-android-paging"]
|
||||||
voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"]
|
voyager = ["voyager-navigator", "voyager-screenmodel", "voyager-tab-navigator", "voyager-transitions"]
|
||||||
richtext = ["richtext-commonmark", "richtext-m3"]
|
|
||||||
test = ["junit", "kotest-assertions", "mockk"]
|
test = ["junit", "kotest-assertions", "mockk"]
|
||||||
|
markdown = ["markdown-m3", "markdown-coil"]
|
||||||
|
@ -31,5 +31,5 @@ dependencies {
|
|||||||
implementation(libs.material)
|
implementation(libs.material)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
api(libs.injekt.core)
|
api(libs.injekt)
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ kotlin {
|
|||||||
val commonMain by getting {
|
val commonMain by getting {
|
||||||
dependencies {
|
dependencies {
|
||||||
api(kotlinx.serialization.json)
|
api(kotlinx.serialization.json)
|
||||||
api(libs.injekt.core)
|
api(libs.injekt)
|
||||||
api(libs.rxjava)
|
api(libs.rxjava)
|
||||||
api(libs.jsoup)
|
api(libs.jsoup)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user