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 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))
|
- 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))
|
- 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
|
### Improved
|
||||||
- Significantly improve browsing speed (near instantaneous) ([@AntsyLich](https://github.com/AntsyLich)) ([#1946](https://github.com/mihonapp/mihon/pull/1946))
|
- 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")
|
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)
|
||||||
@ -271,6 +270,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
|
||||||
@ -94,8 +96,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,
|
||||||
@ -248,14 +248,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,
|
||||||
@ -559,10 +554,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,
|
||||||
@ -590,9 +590,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(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -604,11 +605,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,
|
||||||
|
@ -3,7 +3,6 @@ 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.0"
|
||||||
sqldelight = "2.0.2"
|
sqldelight = "2.0.2"
|
||||||
sqlite = "2.4.0"
|
sqlite = "2.4.0"
|
||||||
@ -11,6 +10,7 @@ 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"
|
||||||
@ -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"]
|
||||||
|
Reference in New Issue
Block a user