mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-28 12:07:52 +02:00
Add markdown support for manga descriptions (#1948)
This commit is contained in:
@ -15,6 +15,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
||||
- Add more Kaomoji for empty/error screens ([@ianfhunter](https://github.com/ianfhunter/)) ([#1909](https://github.com/mihonapp/mihon/pull/1909))
|
||||
- Add user manga notes ([@imkunet](https://github.com/imkunet), [@AntsyLich](https://github.com/AntsyLich)) ([#428](https://github.com/mihonapp/mihon/pull/428))
|
||||
- Fix user notes not restoring when manga doesn't exist in DB ([@AntsyLich](https://github.com/AntsyLich)) ([#1945](https://github.com/mihonapp/mihon/pull/1945))
|
||||
- Add markdown support for manga descriptions ([@Secozzi](https://github.com/Secozzi)) ([#1948](https://github.com/mihonapp/mihon/pull/1948))
|
||||
|
||||
### Improved
|
||||
- Significantly improve browsing speed (near instantaneous) ([@AntsyLich](https://github.com/AntsyLich)) ([#1946](https://github.com/mihonapp/mihon/pull/1946))
|
||||
|
@ -262,7 +262,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)
|
||||
@ -271,6 +270,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
|
||||
@ -94,8 +96,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,
|
||||
@ -248,14 +248,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,
|
||||
@ -559,10 +554,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,
|
||||
@ -590,9 +590,10 @@ private fun MangaSummary(
|
||||
expanded = true,
|
||||
onEditNotes = onEditNotesClicked,
|
||||
)
|
||||
Text(
|
||||
text = expandedDescription,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
MarkdownRender(
|
||||
content = description,
|
||||
annotator = descriptionAnnotator,
|
||||
modifier = Modifier.secondaryItemAlpha(),
|
||||
)
|
||||
}
|
||||
},
|
||||
@ -604,11 +605,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,
|
||||
|
@ -3,7 +3,6 @@ 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.0"
|
||||
sqldelight = "2.0.2"
|
||||
sqlite = "2.4.0"
|
||||
@ -11,6 +10,7 @@ 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"
|
||||
@ -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"]
|
||||
|
Reference in New Issue
Block a user