From 50b0ccb9289e0ee413ec44eeed0e8f6f13f63593 Mon Sep 17 00:00:00 2001
From: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
Date: Fri, 10 Apr 2026 01:56:24 +0600
Subject: [PATCH] Add dedicated "Support Us" screen (#3200)
---
CHANGELOG.md | 3 +
.../eu/kanade/presentation/more/MoreScreen.kt | 3 +-
.../more/settings/screen/about/AboutScreen.kt | 3 +-
.../eu/kanade/tachiyomi/ui/more/MoreTab.kt | 4 +-
.../mihon/feature/support/SupportUsScreen.kt | 143 ++++++++++++++++++
.../kotlin/tachiyomi/core/common/Constants.kt | 4 +-
.../moko-resources/base/strings.xml | 7 +
.../presentation/core/icons/OpenCollective.kt | 47 ++++++
.../presentation/core/icons/Patreon.kt | 39 +++++
9 files changed, 248 insertions(+), 5 deletions(-)
create mode 100644 app/src/main/java/mihon/feature/support/SupportUsScreen.kt
create mode 100644 presentation-core/src/main/java/tachiyomi/presentation/core/icons/OpenCollective.kt
create mode 100644 presentation-core/src/main/java/tachiyomi/presentation/core/icons/Patreon.kt
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b9b8c6c7..8cc6a1d80 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,9 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- `Other` - for technical stuff.
## [Unreleased]
+### Added
+- Add dedicated "Support Us" screen ([@AntsyLich](https://github.com/AntsyLich)) ([#3200](https://github.com/mihonapp/mihon/pull/3200))
+
### Changed
- Don't prompt users to enable Google Play services if disabled or unavailable ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3152](https://github.com/mihonapp/mihon/pull/3152))
- Make "Support Us" more prominent in More screen ([@AntsyLich](https://github.com/AntsyLich)) ([#3196](https://github.com/mihonapp/mihon/pull/3196))
diff --git a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt
index 2ec2d8f33..f96245ec4 100644
--- a/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt
@@ -40,6 +40,7 @@ fun MoreScreen(
onClickStats: () -> Unit,
onClickDataAndStorage: () -> Unit,
onClickSettings: () -> Unit,
+ onClickSupport: () -> Unit,
onClickAbout: () -> Unit,
) {
val uriHandler = LocalUriHandler.current
@@ -136,7 +137,7 @@ fun MoreScreen(
TextPreferenceWidget(
title = stringResource(MR.strings.label_support_us),
icon = Icons.Default.VolunteerActivism,
- onPreferenceClick = { uriHandler.openUri(Constants.URL_DONATE) },
+ onPreferenceClick = onClickSupport,
)
}
item {
diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt
index 91e0aaa3e..01e35c1ec 100644
--- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/about/AboutScreen.kt
@@ -41,6 +41,7 @@ import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.updaterEnabled
import kotlinx.coroutines.launch
import logcat.LogPriority
+import tachiyomi.core.common.Constants
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat
@@ -179,7 +180,7 @@ object AboutScreen : Screen() {
LinkIcon(
label = "Discord",
icon = CustomIcons.Discord,
- url = "https://discord.gg/mihon",
+ url = Constants.URL_DISCORD,
)
LinkIcon(
label = "X",
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt
index f673292de..f9cb561cc 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/more/MoreTab.kt
@@ -7,7 +7,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
-import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
@@ -31,6 +30,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
+import mihon.feature.support.SupportUsScreen
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
@@ -57,7 +57,6 @@ data object MoreTab : Tab {
@Composable
override fun Content() {
- val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val screenModel = rememberScreenModel { MoreScreenModel() }
val downloadQueueState by screenModel.downloadQueueState.collectAsState()
@@ -72,6 +71,7 @@ data object MoreTab : Tab {
onClickStats = { navigator.push(StatsScreen()) },
onClickDataAndStorage = { navigator.push(SettingsScreen(SettingsScreen.Destination.DataAndStorage)) },
onClickSettings = { navigator.push(SettingsScreen()) },
+ onClickSupport = { navigator.push(SupportUsScreen()) },
onClickAbout = { navigator.push(SettingsScreen(SettingsScreen.Destination.About)) },
)
}
diff --git a/app/src/main/java/mihon/feature/support/SupportUsScreen.kt b/app/src/main/java/mihon/feature/support/SupportUsScreen.kt
new file mode 100644
index 000000000..a9f9d219b
--- /dev/null
+++ b/app/src/main/java/mihon/feature/support/SupportUsScreen.kt
@@ -0,0 +1,143 @@
+package mihon.feature.support
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.OpenInNew
+import androidx.compose.material3.Card
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.currentOrThrow
+import eu.kanade.presentation.components.AppBar
+import eu.kanade.presentation.more.settings.widget.TextPreferenceWidget
+import eu.kanade.presentation.util.Screen
+import tachiyomi.core.common.Constants
+import tachiyomi.i18n.MR
+import tachiyomi.presentation.core.components.material.padding
+import tachiyomi.presentation.core.i18n.stringResource
+import tachiyomi.presentation.core.icons.CustomIcons
+import tachiyomi.presentation.core.icons.Discord
+import tachiyomi.presentation.core.icons.OpenCollective
+import tachiyomi.presentation.core.icons.Patreon
+
+class SupportUsScreen : Screen() {
+
+ @Composable
+ override fun Content() {
+ val navigator = LocalNavigator.currentOrThrow
+ val uriHandler = LocalUriHandler.current
+
+ Scaffold(
+ topBar = {
+ AppBar(
+ title = stringResource(MR.strings.label_support_us),
+ navigateUp = navigator::pop,
+ )
+ },
+ ) { paddingValues ->
+ Column(
+ verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
+ modifier = Modifier.verticalScroll(rememberScrollState()).padding(
+ remember(paddingValues) {
+ object : PaddingValues {
+ override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp {
+ return paddingValues.calculateLeftPadding(layoutDirection) +
+ MaterialTheme.padding.medium
+ }
+
+ override fun calculateTopPadding(): Dp {
+ return 0.dp
+ }
+
+ override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp {
+ return paddingValues.calculateRightPadding(layoutDirection) +
+ MaterialTheme.padding.medium
+ }
+
+ override fun calculateBottomPadding(): Dp {
+ return 0.dp
+ }
+ }
+ },
+ ),
+ ) {
+ Spacer(modifier = Modifier.height(paddingValues.calculateTopPadding()))
+
+ Text(
+ text = stringResource(MR.strings.supportUsScreen_perks),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
+ )
+
+ SupportItem(
+ icon = CustomIcons.Patreon,
+ title = stringResource(MR.strings.supportUsScreen_donationPlatform_patreon),
+ onClick = { uriHandler.openUri(Constants.URL_DONATE_PATREON) },
+ )
+ SupportItem(
+ icon = CustomIcons.OpenCollective,
+ title = stringResource(MR.strings.supportUsScreen_donationPlatform_opencollective),
+ onClick = { uriHandler.openUri(Constants.URL_DONATE_OPENCOLLECTIVE) },
+ )
+
+ Text(
+ text = stringResource(MR.strings.supportUsScreen_currentlySupportedBy, 200),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
+ )
+
+ Text(
+ text = stringResource(MR.strings.supportUsScreen_contactForDetailsMessage),
+ style = MaterialTheme.typography.bodyMedium,
+ modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
+ )
+
+ SupportItem(
+ icon = CustomIcons.Discord,
+ title = stringResource(MR.strings.supportUsScreen_contactPlatform),
+ onClick = { uriHandler.openUri(Constants.URL_DISCORD) },
+ )
+
+ Spacer(modifier = Modifier.height(paddingValues.calculateBottomPadding()))
+ }
+ }
+ }
+
+ @Composable
+ private fun SupportItem(
+ icon: ImageVector,
+ title: String,
+ onClick: () -> Unit,
+ ) {
+ Card {
+ TextPreferenceWidget(
+ title = title,
+ icon = icon,
+ widget = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.OpenInNew,
+ contentDescription = null,
+ )
+ },
+ onPreferenceClick = onClick,
+ )
+ }
+ }
+}
diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/Constants.kt b/core/common/src/main/kotlin/tachiyomi/core/common/Constants.kt
index 1e7b8b873..bfeff12d2 100644
--- a/core/common/src/main/kotlin/tachiyomi/core/common/Constants.kt
+++ b/core/common/src/main/kotlin/tachiyomi/core/common/Constants.kt
@@ -3,7 +3,9 @@ package tachiyomi.core.common
object Constants {
const val URL_HELP = "https://mihon.app/docs/guides/troubleshooting/"
const val URL_HELP_UPCOMING = "https://mihon.app/docs/faq/updates/upcoming"
- const val URL_DONATE = "https://patreon.com/mihon/membership"
+ const val URL_DONATE_PATREON = "https://patreon.com/mihon/membership"
+ const val URL_DONATE_OPENCOLLECTIVE = "https://opencollective.com/mihon/contribute"
+ const val URL_DISCORD = "https://discord.gg/mihon"
const val MANGA_EXTRA = "manga"
diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml
index c67a037f2..a58577486 100644
--- a/i18n/src/commonMain/moko-resources/base/strings.xml
+++ b/i18n/src/commonMain/moko-resources/base/strings.xml
@@ -1047,4 +1047,11 @@
Cancel
Cancel
No chapters found, this entry cannot be used for migration
+
+ Patrons get access to exclusive channels, with more perks to come.
+ Patreon (preferred)
+ OpenCollective
+ Mihon is currently backed by %d+ patrons. We want to thank each and everyone of you. Let's make Mihon even better together.
+ For alternative options or any related questions, join our Discord server.
+ Discord
diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/OpenCollective.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/OpenCollective.kt
new file mode 100644
index 000000000..de061b886
--- /dev/null
+++ b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/OpenCollective.kt
@@ -0,0 +1,47 @@
+package tachiyomi.presentation.core.icons
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+
+@Suppress("UnusedReceiverParameter")
+val CustomIcons.OpenCollective: ImageVector
+ get() {
+ if (_OpenCollective != null) {
+ return _OpenCollective!!
+ }
+ _OpenCollective = ImageVector.Builder(
+ name = "OpenCollective",
+ defaultWidth = 24.dp,
+ defaultHeight = 24.dp,
+ viewportWidth = 24f,
+ viewportHeight = 24f,
+ ).apply {
+ path(fill = SolidColor(Color.Black)) {
+ moveTo(12f, 0f)
+ curveTo(5.373f, 0f, 0f, 5.373f, 0f, 12f)
+ reflectiveCurveToRelative(5.373f, 12f, 12f, 12f)
+ curveToRelative(2.54f, 0f, 4.894f, -0.79f, 6.834f, -2.135f)
+ lineToRelative(-3.107f, -3.109f)
+ arcToRelative(7.715f, 7.715f, 0f, isMoreThanHalf = true, isPositiveArc = true, 0f, -13.512f)
+ lineToRelative(3.107f, -3.109f)
+ arcTo(11.943f, 11.943f, 0f, isMoreThanHalf = false, isPositiveArc = false, 12f, 0f)
+ close()
+ moveTo(21.865f, 5.166f)
+ lineToRelative(-3.109f, 3.107f)
+ arcTo(7.67f, 7.67f, 0f, isMoreThanHalf = false, isPositiveArc = true, 19.715f, 12f)
+ arcToRelative(7.682f, 7.682f, 0f, isMoreThanHalf = false, isPositiveArc = true, -0.959f, 3.727f)
+ lineToRelative(3.109f, 3.107f)
+ arcTo(11.943f, 11.943f, 0f, isMoreThanHalf = false, isPositiveArc = false, 24f, 12f)
+ curveToRelative(0f, -2.54f, -0.79f, -4.894f, -2.135f, -6.834f)
+ close()
+ }
+ }.build()
+
+ return _OpenCollective!!
+ }
+
+@Suppress("ObjectPropertyName")
+private var _OpenCollective: ImageVector? = null
diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Patreon.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Patreon.kt
new file mode 100644
index 000000000..af5fa55fa
--- /dev/null
+++ b/presentation-core/src/main/java/tachiyomi/presentation/core/icons/Patreon.kt
@@ -0,0 +1,39 @@
+package tachiyomi.presentation.core.icons
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+
+@Suppress("UnusedReceiverParameter")
+val CustomIcons.Patreon: ImageVector
+ get() {
+ if (_Patreon != null) {
+ return _Patreon!!
+ }
+ _Patreon = ImageVector.Builder(
+ name = "Patreon",
+ defaultWidth = 24.dp,
+ defaultHeight = 24.dp,
+ viewportWidth = 24f,
+ viewportHeight = 24f,
+ ).apply {
+ path(fill = SolidColor(Color.Black)) {
+ moveTo(22.957f, 7.21f)
+ curveToRelative(-0.004f, -3.064f, -2.391f, -5.576f, -5.191f, -6.482f)
+ curveToRelative(-3.478f, -1.125f, -8.064f, -0.962f, -11.384f, 0.604f)
+ curveTo(2.357f, 3.231f, 1.093f, 7.391f, 1.046f, 11.54f)
+ curveToRelative(-0.039f, 3.411f, 0.302f, 12.396f, 5.369f, 12.46f)
+ curveToRelative(3.765f, 0.047f, 4.326f, -4.804f, 6.068f, -7.141f)
+ curveToRelative(1.24f, -1.662f, 2.836f, -2.132f, 4.801f, -2.618f)
+ curveToRelative(3.376f, -0.836f, 5.678f, -3.501f, 5.673f, -7.031f)
+ close()
+ }
+ }.build()
+
+ return _Patreon!!
+ }
+
+@Suppress("ObjectPropertyName")
+private var _Patreon: ImageVector? = null