From e4b700290968cdc3462d42e511f6d26e266801a6 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Fri, 10 Apr 2026 02:08:20 +0600 Subject: [PATCH] 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 --- CHANGELOG.md | 4 + .../eu/kanade/domain/base/BasePreferences.kt | 5 + .../presentation/components/AdaptiveSheet.kt | 6 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 171 +++++++++++++++--- .../moko-resources/base/strings.xml | 8 +- .../core/components/AdaptiveSheet.kt | 11 +- 6 files changed, 172 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cc6a1d80..43d1a190f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt index 45ed21530..d38c55b2b 100644 --- a/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt +++ b/app/src/main/java/eu/kanade/domain/base/BasePreferences.kt @@ -46,4 +46,9 @@ class BasePreferences( ) val installationId: Preference = preferenceStore.getString(Preference.appStateKey("installation_id"), "") + + val donationCampaignShown: Preference = preferenceStore.getBoolean( + Preference.appStateKey("donation_campaign_shown"), + false, + ) } diff --git a/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt b/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt index a5916d268..4d51bd33e 100644 --- a/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt +++ b/app/src/main/java/eu/kanade/presentation/components/AdaptiveSheet.kt @@ -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, ) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 3c1beda74..e80e5c947 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -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. * diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index a58577486..705fd2882 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -648,7 +648,6 @@ Open source licenses Privacy policy Check for updates - Updated to v%1$s Send crash reports @@ -1054,4 +1053,11 @@ 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 + + Enjoying the app? + Hi! I’m the main developer behind Mihon, and I’d like to ask you to consider supporting its development financially. With enough support, I’ll be able to work on Mihon full-time and focus on long-standing issues as well as highly requested features. + 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. + Regardless, Mihon will continue to be maintained by me and the volunteers as well as stay open source. + Discord + Dismiss diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt index 3d2fcd73a..1f1175167 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/AdaptiveSheet.kt @@ -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()