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