From 2ef8ae11c9da02c24bf4d12bf22b41c5d19bada4 Mon Sep 17 00:00:00 2001 From: Secozzi <49240133+Secozzi@users.noreply.github.com> Date: Wed, 30 Jul 2025 06:20:43 +0200 Subject: [PATCH] Add option for rendering images in description (#2076) --- CHANGELOG.md | 1 + .../java/eu/kanade/domain/ui/UiPreferences.kt | 2 + .../manga/components/MangaInfoHeader.kt | 52 +++++++++++++++++-- .../manga/components/MarkdownRender.kt | 49 +++++++++++++++-- .../screen/SettingsAppearanceScreen.kt | 4 ++ .../moko-resources/base/strings.xml | 1 + 6 files changed, 101 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3765f6e57..1c1e6152d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Add markdown support for manga descriptions ([@Secozzi](https://github.com/Secozzi)) ([#1948](https://github.com/mihonapp/mihon/pull/1948)) - Use simpler markdown flavour ([@Secozzi](https://github.com/Secozzi)) ([#2000](https://github.com/mihonapp/mihon/pull/2000)) - Use Github markdown flavour for Github releases & fix bullet list alignment ([@Secozzi](https://github.com/Secozzi)) ([#2024](https://github.com/mihonapp/mihon/pull/2024)) + - Add option to toggle image loading ([@Secozzi](https://github.com/Secozzi)) ([#2076](https://github.com/mihonapp/mihon/pull/2076)) - Add Nord Theme ([@Riztard](https://github.com/Riztard)) ([#1951](https://github.com/mihonapp/mihon/pull/1951)) - Option to keep read manga when clearing database ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#1979](https://github.com/mihonapp/mihon/pull/1979)) - Add advanced option to always update manga title from source ([@FlaminSarge](https://github.com/FlaminSarge)) ([#1182](https://github.com/mihonapp/mihon/pull/1182)) diff --git a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt index a0a16251d..8d23145e6 100644 --- a/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt +++ b/app/src/main/java/eu/kanade/domain/ui/UiPreferences.kt @@ -34,6 +34,8 @@ class UiPreferences( fun tabletUiMode() = preferenceStore.getEnum("tablet_ui_mode", TabletUiMode.AUTOMATIC) + fun imagesInDescription() = preferenceStore.getBoolean("pref_render_images_description", true) + companion object { fun dateFormat(format: String): DateTimeFormatter = when (format) { "" -> DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT) diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt index daf8695da..1fb41c7e9 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaInfoHeader.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.appendInlineContent import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Brush @@ -68,8 +69,11 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withLink import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -79,10 +83,15 @@ import coil3.request.ImageRequest import coil3.request.crossfade import com.mikepenz.markdown.model.markdownAnnotator import com.mikepenz.markdown.model.markdownAnnotatorConfig +import com.mikepenz.markdown.utils.getUnescapedTextInNode +import eu.kanade.domain.ui.UiPreferences import eu.kanade.presentation.components.DropdownMenu import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.util.system.copyToClipboard +import org.intellij.markdown.MarkdownElementTypes +import org.intellij.markdown.MarkdownTokenTypes +import org.intellij.markdown.ast.findChildOfType import tachiyomi.domain.manga.model.Manga import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.DISABLED_ALPHA @@ -92,6 +101,8 @@ import tachiyomi.presentation.core.i18n.pluralStringResource import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.util.clickableNoIndication import tachiyomi.presentation.core.util.secondaryItemAlpha +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.time.Instant import java.time.temporal.ChronoUnit import kotlin.math.roundToInt @@ -554,8 +565,33 @@ private fun ColumnScope.MangaContentInfo( } } -private val descriptionAnnotator = markdownAnnotator( +private fun descriptionAnnotator(loadImages: Boolean, linkStyle: SpanStyle) = markdownAnnotator( annotate = { content, child -> + if (!loadImages && child.type == MarkdownElementTypes.IMAGE) { + val inlineLink = child.findChildOfType(MarkdownElementTypes.INLINE_LINK) + + val url = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_DESTINATION) + ?.getUnescapedTextInNode(content) + ?: inlineLink?.findChildOfType(MarkdownElementTypes.AUTOLINK) + ?.findChildOfType(MarkdownTokenTypes.AUTOLINK) + ?.getUnescapedTextInNode(content) + ?: return@markdownAnnotator false + + val textNode = inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TITLE) + ?: inlineLink?.findChildOfType(MarkdownElementTypes.LINK_TEXT) + val altText = textNode?.findChildOfType(MarkdownTokenTypes.TEXT) + ?.getUnescapedTextInNode(content).orEmpty() + + withLink(LinkAnnotation.Url(url = url)) { + pushStyle(linkStyle) + appendInlineContent(MARKDOWN_INLINE_IMAGE_TAG) + append(altText) + pop() + } + + return@markdownAnnotator true + } + if (child.type in DISALLOWED_MARKDOWN_TYPES) { append(content.substring(child.startOffset, child.endOffset)) return@markdownAnnotator true @@ -576,6 +612,8 @@ private fun MangaSummary( onEditNotesClicked: () -> Unit, modifier: Modifier = Modifier, ) { + val preferences = remember { Injekt.get() } + val loadImages = remember { preferences.imagesInDescription().get() } val animProgress by animateFloatAsState( targetValue = if (expanded) 1f else 0f, label = "summary", @@ -601,7 +639,11 @@ private fun MangaSummary( MarkdownRender( content = description, modifier = Modifier.secondaryItemAlpha(), - annotator = descriptionAnnotator, + annotator = descriptionAnnotator( + loadImages = loadImages, + linkStyle = getMarkdownLinkStyle().toSpanStyle(), + ), + loadImages = loadImages, ) } }, @@ -616,7 +658,11 @@ private fun MangaSummary( MarkdownRender( content = description, modifier = Modifier.secondaryItemAlpha(), - annotator = descriptionAnnotator, + annotator = descriptionAnnotator( + loadImages = loadImages, + linkStyle = getMarkdownLinkStyle().toSpanStyle(), + ), + loadImages = loadImages, ) } } diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MarkdownRender.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MarkdownRender.kt index c96c4b548..16f8efb46 100644 --- a/app/src/main/java/eu/kanade/presentation/manga/components/MarkdownRender.kt +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MarkdownRender.kt @@ -4,6 +4,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Image +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -12,6 +16,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.FirstBaseline +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextLinkStyles import androidx.compose.ui.text.font.FontFamily @@ -32,11 +38,13 @@ import com.mikepenz.markdown.compose.elements.MarkdownTableRow import com.mikepenz.markdown.compose.elements.MarkdownText import com.mikepenz.markdown.compose.elements.listDepth import com.mikepenz.markdown.model.DefaultMarkdownColors +import com.mikepenz.markdown.model.DefaultMarkdownInlineContent import com.mikepenz.markdown.model.DefaultMarkdownTypography import com.mikepenz.markdown.model.MarkdownAnnotator import com.mikepenz.markdown.model.MarkdownColors import com.mikepenz.markdown.model.MarkdownPadding import com.mikepenz.markdown.model.MarkdownTypography +import com.mikepenz.markdown.model.NoOpImageTransformerImpl import com.mikepenz.markdown.model.markdownAnnotator import com.mikepenz.markdown.model.rememberMarkdownState import org.intellij.markdown.MarkdownTokenTypes.Companion.HTML_TAG @@ -59,12 +67,15 @@ import org.intellij.markdown.parser.markerblocks.providers.ListMarkerProvider import org.intellij.markdown.parser.markerblocks.providers.SetextHeaderProvider import tachiyomi.presentation.core.components.material.padding +const val MARKDOWN_INLINE_IMAGE_TAG = "MARKDOWN_INLINE_IMAGE" + @Composable fun MarkdownRender( content: String, modifier: Modifier = Modifier, flavour: MarkdownFlavourDescriptor = SimpleMarkdownFlavourDescriptor, annotator: MarkdownAnnotator = remember { markdownAnnotator() }, + loadImages: Boolean = true, ) { Markdown( markdownState = rememberMarkdownState( @@ -77,7 +88,10 @@ fun MarkdownRender( typography = getMarkdownTypography(), padding = markdownPadding, components = markdownComponents, - imageTransformer = Coil3ImageTransformerImpl, + imageTransformer = remember(loadImages) { + if (loadImages) Coil3ImageTransformerImpl else NoOpImageTransformerImpl() + }, + inlineContent = getMarkdownInlineContent(), modifier = modifier, ) } @@ -99,13 +113,17 @@ private fun getMarkdownColors(): MarkdownColors { ) } +@Composable +@ReadOnlyComposable +fun getMarkdownLinkStyle() = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, +) + @Composable @ReadOnlyComposable private fun getMarkdownTypography(): MarkdownTypography { - val link = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Bold, - ) + val link = getMarkdownLinkStyle() return DefaultMarkdownTypography( h1 = MaterialTheme.typography.headlineMedium, h2 = MaterialTheme.typography.headlineSmall, @@ -216,6 +234,27 @@ private val markdownComponents = markdownComponents( }, ) +@Composable +@ReadOnlyComposable +private fun getMarkdownInlineContent() = DefaultMarkdownInlineContent( + inlineContent = mapOf( + MARKDOWN_INLINE_IMAGE_TAG to InlineTextContent( + placeholder = Placeholder( + width = MaterialTheme.typography.bodyMedium.fontSize * 1.25, + height = MaterialTheme.typography.bodyMedium.fontSize * 1.25, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter, + ), + children = { + Icon( + imageVector = Icons.Outlined.Image, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + }, + ), + ), +) + private object SimpleMarkdownFlavourDescriptor : CommonMarkFlavourDescriptor() { override val markerProcessorFactory: MarkerProcessorFactory = SimpleMarkdownProcessFactory } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt index da957ae70..1798f3614 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsAppearanceScreen.kt @@ -145,6 +145,10 @@ object SettingsAppearanceScreen : SearchableSettings { formattedNow, ), ), + Preference.PreferenceItem.SwitchPreference( + preference = uiPreferences.imagesInDescription(), + title = stringResource(MR.strings.pref_display_images_description), + ), ), ) } diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 07b0ea831..2dc4b9221 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -245,6 +245,7 @@ \"%1$s\" instead of \"%2$s\" Date format + Render images in manga descriptions Manage notifications App language