Add a one time popup asking for donation from long time users (#3203)

Also remove the empty "Updated to X.Y.Z" popup after app update
This commit is contained in:
AntsyLich
2026-04-10 02:08:20 +06:00
committed by GitHub
parent 02c6427823
commit e4b7002909
6 changed files with 172 additions and 33 deletions
+4
View File
@@ -13,6 +13,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
## [Unreleased]
### Added
- Add dedicated "Support Us" screen ([@AntsyLich](https://github.com/AntsyLich)) ([#3200](https://github.com/mihonapp/mihon/pull/3200))
- Add a one time popup asking for donation from long time users ([@AntsyLich](https://github.com/AntsyLich)) ([#3203](https://github.com/mihonapp/mihon/pull/3203))
### 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))
@@ -25,6 +26,9 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
### Fixed
- Fix app trying to split long strip when not needed ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3121](https://github.com/mihonapp/mihon/pull/3121))
### Removed
- Remove the empty "Updated to X.Y.Z" popup after app update ([@AntsyLich](https://github.com/AntsyLich)) ([#3203](https://github.com/mihonapp/mihon/pull/3203))
## [v0.19.7] - 2026-03-23
Same as v0.19.6
@@ -46,4 +46,9 @@ class BasePreferences(
)
val installationId: Preference<String> = preferenceStore.getString(Preference.appStateKey("installation_id"), "")
val donationCampaignShown: Preference<Boolean> = preferenceStore.getBoolean(
Preference.appStateKey("donation_campaign_shown"),
false,
)
}
@@ -28,7 +28,7 @@ fun NavigatorAdaptiveSheet(
content = { sheetNavigator ->
AdaptiveSheet(
onDismissRequest = onDismissRequest,
enableSwipeDismiss = enableSwipeDismiss(sheetNavigator),
enableImplicitDismiss = enableSwipeDismiss(sheetNavigator),
) {
ScreenTransition(
navigator = sheetNavigator,
@@ -63,7 +63,7 @@ fun NavigatorAdaptiveSheet(
fun AdaptiveSheet(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
enableSwipeDismiss: Boolean = true,
enableImplicitDismiss: Boolean = true,
content: @Composable () -> Unit,
) {
val isTabletUi = isTabletUi()
@@ -74,7 +74,7 @@ fun AdaptiveSheet(
) {
AdaptiveSheetImpl(
isTabletUi = isTabletUi,
enableSwipeDismiss = enableSwipeDismiss,
enableImplicitDismiss = enableImplicitDismiss,
onDismissRequest = onDismissRequest,
modifier = modifier,
) {
@@ -5,6 +5,7 @@ import android.app.SearchManager
import android.app.assist.AssistContent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Build
import android.os.Bundle
@@ -14,21 +15,32 @@ import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.AlertDialog
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.material.icons.filled.VolunteerActivism
import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -41,6 +53,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import androidx.core.animation.doOnEnd
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen
@@ -55,6 +69,7 @@ import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
import cafe.adriel.voyager.navigator.currentOrThrow
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.interactor.GetIncognitoState
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor
@@ -63,12 +78,10 @@ import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
import eu.kanade.presentation.util.AssistContentScreen
import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.RELEASE_URL
import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
@@ -80,7 +93,6 @@ import eu.kanade.tachiyomi.ui.more.NewUpdateScreen
import eu.kanade.tachiyomi.ui.more.OnboardingScreen
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.isNavigationBarNeedsScrim
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.updaterEnabled
import eu.kanade.tachiyomi.util.view.setComposeContent
import kotlinx.coroutines.channels.awaitClose
@@ -93,6 +105,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import logcat.LogPriority
import mihon.core.migration.Migrator
import mihon.feature.support.SupportUsScreen
import tachiyomi.core.common.Constants
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.logcat
@@ -100,9 +113,14 @@ import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.injectLazy
import kotlin.time.Clock
import kotlin.time.Duration.Companion.days
import kotlin.time.Instant
import kotlin.time.times
class MainActivity : BaseActivity() {
@@ -131,7 +149,7 @@ class MainActivity : BaseActivity() {
super.onCreate(savedInstanceState)
val didMigration = Migrator.awaitAndRelease()
Migrator.awaitAndRelease()
// Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079
if (!isTaskRoot) {
@@ -240,24 +258,7 @@ class MainActivity : BaseActivity() {
CheckForUpdates()
ShowOnboarding()
}
var showChangelog by remember { mutableStateOf(didMigration && !BuildConfig.DEBUG) }
if (showChangelog) {
AlertDialog(
onDismissRequest = { showChangelog = false },
title = { Text(text = stringResource(MR.strings.updated_version, BuildConfig.VERSION_NAME)) },
dismissButton = {
TextButton(onClick = { openInBrowser(RELEASE_URL) }) {
Text(text = stringResource(MR.strings.whats_new))
}
},
confirmButton = {
TextButton(onClick = { showChangelog = false }) {
Text(text = stringResource(MR.strings.action_ok))
}
},
)
ShowDonationCampaign()
}
}
@@ -343,6 +344,128 @@ class MainActivity : BaseActivity() {
}
}
@Composable
private fun ShowDonationCampaign() {
val navigator = LocalNavigator.currentOrThrow
var showCampaign by remember { mutableStateOf(false) }
if (showCampaign) {
val uriHandler = LocalUriHandler.current
val dismissSupportMessage = {
preferences.donationCampaignShown.set(true)
@Suppress("AssignedValueIsNeverRead")
showCampaign = false
}
AdaptiveSheet(
onDismissRequest = dismissSupportMessage,
enableImplicitDismiss = false,
) {
Column {
Spacer(modifier = Modifier.height(16.dp))
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(16.dp)
.weight(1f, fill = false)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = stringResource(MR.strings.donationCampaign_title),
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.headlineSmall,
)
Text(
text = stringResource(MR.strings.donationCampaign_paragraph1),
style = MaterialTheme.typography.bodyMedium,
)
Text(
text = stringResource(MR.strings.donationCampaign_paragraph2),
style = MaterialTheme.typography.bodyMedium,
)
Text(
text = stringResource(MR.strings.donationCampaign_paragraph3),
style = MaterialTheme.typography.bodyMedium,
)
}
HorizontalDivider()
Button(
modifier = Modifier
.padding(top = MaterialTheme.padding.small)
.padding(horizontal = MaterialTheme.padding.medium)
.fillMaxWidth(),
onClick = {
navigator.push(SupportUsScreen())
dismissSupportMessage()
},
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
Icon(
imageVector = Icons.Default.VolunteerActivism,
contentDescription = null,
)
Text(
text = stringResource(MR.strings.label_support_us),
color = MaterialTheme.colorScheme.onPrimary,
)
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
modifier = Modifier
.padding(bottom = MaterialTheme.padding.small)
.padding(horizontal = MaterialTheme.padding.medium),
) {
OutlinedButton(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
onClick = { uriHandler.openUri(Constants.URL_DISCORD) },
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
Text(
text = stringResource(MR.strings.donationCampaign_contactPlatform),
)
Icon(
imageVector = Icons.AutoMirrored.Default.OpenInNew,
contentDescription = null,
)
}
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
onClick = dismissSupportMessage,
) {
Text(
text = stringResource(MR.strings.donationCampaign_dismiss),
)
}
}
}
}
}
LaunchedEffect(Unit) {
try {
val firstInstallTime = packageManager.getPackageInfo(packageName, 0).firstInstallTime
val eligibleTime = Instant.fromEpochMilliseconds(firstInstallTime).plus(6 * 30.days)
@Suppress("AssignedValueIsNeverRead")
showCampaign = (Clock.System.now() >= eligibleTime && !preferences.donationCampaignShown.get())
} catch (_: PackageManager.NameNotFoundException) {
}
}
}
/**
* Sets custom splash screen exit animation on devices prior to Android 12.
*
@@ -648,7 +648,6 @@
<string name="licenses">Open source licenses</string>
<string name="privacy_policy">Privacy policy</string>
<string name="check_for_updates">Check for updates</string>
<string name="updated_version">Updated to v%1$s</string>
<!-- ACRA -->
<string name="pref_enable_acra">Send crash reports</string>
@@ -1054,4 +1053,11 @@
<string name="supportUsScreen.currentlySupportedBy">Mihon is currently backed by %d+ patrons. We want to thank each and everyone of you. Let's make Mihon even better together.</string>
<string name="supportUsScreen.contactForDetailsMessage">For alternative options or any related questions, join our Discord server.</string>
<string name="supportUsScreen.contactPlatform">Discord</string>
<string name="donationCampaign.title">Enjoying the app?</string>
<string name="donationCampaign.paragraph1">Hi! Im the main developer behind Mihon, and Id like to ask you to consider supporting its development financially. With enough support, Ill be able to work on Mihon full-time and focus on long-standing issues as well as highly requested features.</string>
<string name="donationCampaign.paragraph2">You can donate through either Patreon (preferred) or OpenCollective. If you have questions or would like to support using alternative methods, feel free to reach out to me through Mihon's Discord server.</string>
<string name="donationCampaign.paragraph3">Regardless, Mihon will continue to be maintained by me and the volunteers as well as stay open source.</string>
<string name="donationCampaign.contactPlatform">Discord</string>
<string name="donationCampaign.dismiss">Dismiss</string>
</resources>
@@ -52,7 +52,7 @@ import kotlin.math.roundToInt
@Composable
fun AdaptiveSheet(
isTabletUi: Boolean,
enableSwipeDismiss: Boolean,
enableImplicitDismiss: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
@@ -77,6 +77,7 @@ fun AdaptiveSheet(
.clickable(
interactionSource = null,
indication = null,
enabled = enableImplicitDismiss,
onClick = internalOnDismissRequest,
)
.fillMaxSize()
@@ -98,7 +99,7 @@ fun AdaptiveSheet(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
content = {
BackHandler(
enabled = remember { derivedStateOf { alpha > 0f } }.value,
enabled = enableImplicitDismiss && remember { derivedStateOf { alpha > 0f } }.value,
onBack = internalOnDismissRequest,
)
content()
@@ -149,7 +150,7 @@ fun AdaptiveSheet(
onClick = {},
)
.then(
if (enableSwipeDismiss) {
if (enableImplicitDismiss) {
Modifier.nestedScroll(
remember(anchoredDraggableState) {
anchoredDraggableState.preUpPostDownNestedScrollConnection {
@@ -174,7 +175,7 @@ fun AdaptiveSheet(
.anchoredDraggable(
state = anchoredDraggableState,
orientation = Orientation.Vertical,
enabled = enableSwipeDismiss,
enabled = enableImplicitDismiss,
flingBehavior = flingBehavior,
)
.navigationBarsPadding()
@@ -183,7 +184,7 @@ fun AdaptiveSheet(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
content = {
BackHandler(
enabled = anchoredDraggableState.targetValue == 0,
enabled = enableImplicitDismiss && anchoredDraggableState.targetValue == 0,
onBack = internalOnDismissRequest,
)
content()