mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
feat(upgrade): Replace upgrade dialog with full-screen upgrade screen
Add dedicated Compose upgrade screens for both FOSS and gplay flavors, presenting pro benefits before the purchase/sponsor action. Migrate all upgrade dialog callers to navigate to the new screen instead.
This commit is contained in:
@@ -116,7 +116,7 @@ internal fun AddProfileContent() = PreviewWrapper {
|
||||
@Composable
|
||||
internal fun SettingsIndexContent() = PreviewWrapper {
|
||||
SettingsScreen(
|
||||
state = SettingsViewModel.State(sponsorUrl = "https://example.com"),
|
||||
state = SettingsViewModel.State(isPro = true, sponsorUrl = "https://example.com"),
|
||||
onNavigateUp = {},
|
||||
onGeneralSettings = {},
|
||||
onDeviceManager = {},
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package eu.darken.capod.common.upgrade.core
|
||||
|
||||
import android.app.Activity
|
||||
import android.widget.Toast
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.WebpageTool
|
||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
@@ -15,7 +10,6 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class UpgradeControlFoss @Inject constructor(
|
||||
private val fossCache: FossCache,
|
||||
private val webpageTool: WebpageTool,
|
||||
) : UpgradeRepo {
|
||||
|
||||
override val upgradeInfo: Flow<UpgradeRepo.Info> = fossCache.upgrade.flow.map { data ->
|
||||
@@ -30,34 +24,11 @@ class UpgradeControlFoss @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun launchBillingFlow(activity: Activity) {
|
||||
MaterialAlertDialogBuilder(activity).apply {
|
||||
setIcon(R.drawable.ic_heart)
|
||||
setTitle(R.string.upgrade_capod_label)
|
||||
setMessage(R.string.upgrade_capod_description)
|
||||
setPositiveButton(R.string.foss_upgrade_donate_label) { _, _ ->
|
||||
fossCache.upgrade.value = FossUpgrade(
|
||||
upgradedAt = Instant.now(),
|
||||
reason = FossUpgrade.Reason.DONATED
|
||||
)
|
||||
webpageTool.open("https://github.com/sponsors/d4rken")
|
||||
Toast.makeText(activity, R.string.general_thank_you_label, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
setNegativeButton(R.string.foss_upgrade_alreadydonated_label) { _, _ ->
|
||||
fossCache.upgrade.value = FossUpgrade(
|
||||
upgradedAt = Instant.now(),
|
||||
reason = FossUpgrade.Reason.ALREADY_DONATED
|
||||
)
|
||||
Toast.makeText(activity, R.string.general_thank_you_label, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
setNeutralButton(R.string.foss_upgrade_no_money_label) { _, _ ->
|
||||
fossCache.upgrade.value = FossUpgrade(
|
||||
upgradedAt = Instant.now(),
|
||||
reason = FossUpgrade.Reason.NO_MONEY
|
||||
)
|
||||
Toast.makeText(activity, "¯\\_(ツ)_/¯", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}.show()
|
||||
fun upgrade(reason: FossUpgrade.Reason) {
|
||||
fossCache.upgrade.value = FossUpgrade(
|
||||
upgradedAt = Instant.now(),
|
||||
reason = reason
|
||||
)
|
||||
}
|
||||
|
||||
data class Info(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package eu.darken.capod.upgrade.ui
|
||||
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import eu.darken.capod.common.navigation.Nav
|
||||
import eu.darken.capod.common.navigation.NavigationEntry
|
||||
import javax.inject.Inject
|
||||
|
||||
class UpgradeNavigation @Inject constructor() : NavigationEntry {
|
||||
override fun EntryProviderScope<NavKey>.setup() {
|
||||
entry<Nav.Main.Upgrade> { UpgradeScreenHost() }
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class Mod {
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bind(entry: UpgradeNavigation): NavigationEntry
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package eu.darken.capod.upgrade.ui
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.twotone.Message
|
||||
import androidx.compose.material.icons.twotone.BluetoothConnected
|
||||
import androidx.compose.material.icons.twotone.Favorite
|
||||
import androidx.compose.material.icons.twotone.Palette
|
||||
import androidx.compose.material.icons.twotone.PlayCircle
|
||||
import androidx.compose.material.icons.twotone.Widgets
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.compose.Preview2
|
||||
import eu.darken.capod.common.compose.PreviewWrapper
|
||||
import eu.darken.capod.common.error.ErrorEventHandler
|
||||
import eu.darken.capod.common.navigation.NavigationEventHandler
|
||||
|
||||
@Composable
|
||||
fun UpgradeScreenHost(vm: UpgradeViewModel = hiltViewModel()) {
|
||||
ErrorEventHandler(vm)
|
||||
NavigationEventHandler(vm)
|
||||
|
||||
UpgradeScreen(
|
||||
onNavigateUp = { vm.navUp() },
|
||||
onSponsor = { vm.sponsor() },
|
||||
)
|
||||
}
|
||||
|
||||
private data class Benefit(val icon: ImageVector, val textRes: Int)
|
||||
|
||||
@Composable
|
||||
fun UpgradeScreen(
|
||||
onNavigateUp: () -> Unit,
|
||||
onSponsor: () -> Unit,
|
||||
) {
|
||||
val benefits = listOf(
|
||||
Benefit(Icons.TwoTone.Palette, R.string.upgrade_benefit_themes),
|
||||
Benefit(Icons.TwoTone.PlayCircle, R.string.upgrade_benefit_autoplay),
|
||||
Benefit(Icons.TwoTone.BluetoothConnected, R.string.upgrade_benefit_autoconnect),
|
||||
Benefit(Icons.AutoMirrored.TwoTone.Message, R.string.upgrade_benefit_popups),
|
||||
Benefit(Icons.TwoTone.Widgets, R.string.upgrade_benefit_widgets),
|
||||
Benefit(Icons.TwoTone.Favorite, R.string.upgrade_benefit_support),
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Surface(
|
||||
modifier = Modifier.size(120.dp),
|
||||
shape = CircleShape,
|
||||
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f),
|
||||
) {}
|
||||
Image(
|
||||
painter = painterResource(R.drawable.splash_graphic2),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(80.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
append("CAPod ")
|
||||
withStyle(SpanStyle(color = colorResource(R.color.brand_secondary), fontWeight = FontWeight.Bold)) {
|
||||
append("FOSS")
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.upgrade_capod_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.upgrade_foss_preamble),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 4.dp)) {
|
||||
benefits.forEach { benefit ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
modifier = Modifier.size(28.dp),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
imageVector = benefit.icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = stringResource(benefit.textRes),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Button(
|
||||
onClick = onSponsor,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.Favorite,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.upgrade_foss_sponsor_action),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = onNavigateUp,
|
||||
modifier = Modifier.padding(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.TwoTone.ArrowBack,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun UpgradeScreenPreview() = PreviewWrapper {
|
||||
UpgradeScreen(
|
||||
onNavigateUp = {},
|
||||
onSponsor = {},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package eu.darken.capod.upgrade.ui
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.darken.capod.common.WebpageTool
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.uix.ViewModel4
|
||||
import eu.darken.capod.common.upgrade.core.FossUpgrade
|
||||
import eu.darken.capod.common.upgrade.core.UpgradeControlFoss
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class UpgradeViewModel @Inject constructor(
|
||||
dispatcherProvider: DispatcherProvider,
|
||||
private val upgradeControlFoss: UpgradeControlFoss,
|
||||
private val webpageTool: WebpageTool,
|
||||
) : ViewModel4(dispatcherProvider) {
|
||||
|
||||
fun sponsor() {
|
||||
upgradeControlFoss.upgrade(FossUpgrade.Reason.DONATED)
|
||||
webpageTool.open("https://github.com/sponsors/d4rken")
|
||||
navUp()
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,6 @@
|
||||
<string name="foss_upgrade_donate_label">Donate</string>
|
||||
<string name="foss_upgrade_alreadydonated_label">I already donated</string>
|
||||
<string name="foss_upgrade_no_money_label">I spend all my money on AirPods</string>
|
||||
<string name="upgrade_foss_preamble">CAPod FOSS is free and open source. If you find it useful, consider sponsoring development to help keep the project going.</string>
|
||||
<string name="upgrade_foss_sponsor_action">Sponsor development</string>
|
||||
</resources>
|
||||
@@ -1,16 +1,9 @@
|
||||
package eu.darken.capod.common.upgrade.core
|
||||
|
||||
import android.app.Activity
|
||||
import android.widget.Toast
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.error.asErrorDialogBuilder
|
||||
import eu.darken.capod.common.flow.replayingShare
|
||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||
import eu.darken.capod.common.upgrade.core.data.BillingData
|
||||
@@ -21,8 +14,6 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.retryWhen
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -31,7 +22,6 @@ import kotlin.math.pow
|
||||
@Singleton
|
||||
class UpgradeRepoGplay @Inject constructor(
|
||||
@AppScope private val scope: CoroutineScope,
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
private val billingDataRepo: BillingDataRepo,
|
||||
private val billingCache: BillingCache,
|
||||
) : UpgradeRepo {
|
||||
@@ -76,49 +66,6 @@ class UpgradeRepoGplay @Inject constructor(
|
||||
}
|
||||
.replayingShare(scope)
|
||||
|
||||
override fun launchBillingFlow(activity: Activity) {
|
||||
MaterialAlertDialogBuilder(activity).apply {
|
||||
setIcon(R.drawable.ic_heart)
|
||||
setTitle(R.string.upgrade_capod_label)
|
||||
setMessage(R.string.upgrade_capod_description)
|
||||
setPositiveButton(R.string.general_upgrade_action) { _, _ ->
|
||||
scope.launch {
|
||||
try {
|
||||
billingDataRepo.startIapFlow(activity, CapodSku.PRO_UPGRADE.sku)
|
||||
} catch (e: Exception) {
|
||||
log(TAG) { "startIapFlow failed:${e.asLog()}" }
|
||||
withContext(dispatcherProvider.Main) {
|
||||
e.asErrorDialogBuilder(activity).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setNeutralButton(R.string.general_check_action) { dialog, _ ->
|
||||
log(TAG) { "recheck()" }
|
||||
scope.launch {
|
||||
try {
|
||||
val data = billingDataRepo.getIapData()
|
||||
log(TAG) { "Recheck successful: $data" }
|
||||
withContext(dispatcherProvider.Main) {
|
||||
if (data.purchases.isEmpty()) {
|
||||
Toast.makeText(
|
||||
activity,
|
||||
R.string.upgrades_no_purchases_found_check_account,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log(TAG) { "Recheck failed:${e.asLog()}" }
|
||||
withContext(dispatcherProvider.Main) {
|
||||
e.asErrorDialogBuilder(activity).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
data class Info(
|
||||
private val gracePeriod: Boolean = false,
|
||||
private val billingData: BillingData?,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package eu.darken.capod.upgrade.ui
|
||||
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import eu.darken.capod.common.navigation.Nav
|
||||
import eu.darken.capod.common.navigation.NavigationEntry
|
||||
import javax.inject.Inject
|
||||
|
||||
class UpgradeNavigation @Inject constructor() : NavigationEntry {
|
||||
override fun EntryProviderScope<NavKey>.setup() {
|
||||
entry<Nav.Main.Upgrade> { UpgradeScreenHost() }
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class Mod {
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bind(entry: UpgradeNavigation): NavigationEntry
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
package eu.darken.capod.upgrade.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.twotone.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.twotone.Message
|
||||
import androidx.compose.material.icons.twotone.BluetoothConnected
|
||||
import androidx.compose.material.icons.twotone.Favorite
|
||||
import androidx.compose.material.icons.twotone.Palette
|
||||
import androidx.compose.material.icons.twotone.PlayCircle
|
||||
import androidx.compose.material.icons.twotone.Stars
|
||||
import androidx.compose.material.icons.twotone.Widgets
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.compose.Preview2
|
||||
import eu.darken.capod.common.compose.PreviewWrapper
|
||||
import eu.darken.capod.common.error.ErrorEventHandler
|
||||
import eu.darken.capod.common.navigation.NavigationEventHandler
|
||||
|
||||
@Composable
|
||||
fun UpgradeScreenHost(vm: UpgradeViewModel = hiltViewModel()) {
|
||||
ErrorEventHandler(vm)
|
||||
NavigationEventHandler(vm)
|
||||
|
||||
val context = LocalContext.current
|
||||
val activity = context as? Activity
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
vm.events.collect { event ->
|
||||
when (event) {
|
||||
UpgradeViewModel.UpgradeEvent.RestoreFailed -> {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.upgrades_no_purchases_found_check_account,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UpgradeScreen(
|
||||
onNavigateUp = { vm.navUp() },
|
||||
onUpgrade = { activity?.let { vm.startPurchase(it) } },
|
||||
onRestore = { vm.restorePurchase() },
|
||||
)
|
||||
}
|
||||
|
||||
private data class Benefit(val icon: ImageVector, val textRes: Int)
|
||||
|
||||
@Composable
|
||||
fun UpgradeScreen(
|
||||
onNavigateUp: () -> Unit,
|
||||
onUpgrade: () -> Unit,
|
||||
onRestore: () -> Unit,
|
||||
) {
|
||||
val benefits = listOf(
|
||||
Benefit(Icons.TwoTone.Palette, R.string.upgrade_benefit_themes),
|
||||
Benefit(Icons.TwoTone.PlayCircle, R.string.upgrade_benefit_autoplay),
|
||||
Benefit(Icons.TwoTone.BluetoothConnected, R.string.upgrade_benefit_autoconnect),
|
||||
Benefit(Icons.AutoMirrored.TwoTone.Message, R.string.upgrade_benefit_popups),
|
||||
Benefit(Icons.TwoTone.Widgets, R.string.upgrade_benefit_widgets),
|
||||
Benefit(Icons.TwoTone.Favorite, R.string.upgrade_benefit_support),
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Surface(
|
||||
modifier = Modifier.size(120.dp),
|
||||
shape = CircleShape,
|
||||
color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f),
|
||||
) {}
|
||||
Image(
|
||||
painter = painterResource(R.drawable.splash_graphic2),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(80.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
append("CAPod ")
|
||||
withStyle(SpanStyle(color = colorResource(R.color.brand_tertiary), fontWeight = FontWeight.Bold)) {
|
||||
append("Pro")
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.upgrade_capod_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.upgrade_preamble),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 4.dp)) {
|
||||
benefits.forEach { benefit ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
modifier = Modifier.size(28.dp),
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
imageVector = benefit.icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = stringResource(benefit.textRes),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
Button(
|
||||
onClick = onUpgrade,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.Stars,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.general_upgrade_action),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onRestore,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
) {
|
||||
Text(text = stringResource(R.string.upgrade_restore_action))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = onNavigateUp,
|
||||
modifier = Modifier.padding(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.TwoTone.ArrowBack,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun UpgradeScreenPreview() = PreviewWrapper {
|
||||
UpgradeScreen(
|
||||
onNavigateUp = {},
|
||||
onUpgrade = {},
|
||||
onRestore = {},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package eu.darken.capod.upgrade.ui
|
||||
|
||||
import android.app.Activity
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.SingleEventFlow
|
||||
import eu.darken.capod.common.uix.ViewModel4
|
||||
import eu.darken.capod.common.upgrade.core.CapodSku
|
||||
import eu.darken.capod.common.upgrade.core.UpgradeRepoGplay
|
||||
import eu.darken.capod.common.upgrade.core.data.BillingDataRepo
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class UpgradeViewModel @Inject constructor(
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
private val upgradeRepo: UpgradeRepoGplay,
|
||||
private val billingDataRepo: BillingDataRepo,
|
||||
) : ViewModel4(dispatcherProvider) {
|
||||
|
||||
sealed interface UpgradeEvent {
|
||||
data object RestoreFailed : UpgradeEvent
|
||||
}
|
||||
|
||||
val events = SingleEventFlow<UpgradeEvent>()
|
||||
|
||||
init {
|
||||
upgradeRepo.upgradeInfo
|
||||
.onEach { info ->
|
||||
if (info.isPro) {
|
||||
log(TAG) { "User is now pro, navigating back" }
|
||||
navUp()
|
||||
}
|
||||
}
|
||||
.launchIn(vmScope)
|
||||
}
|
||||
|
||||
fun startPurchase(activity: Activity) = launch {
|
||||
try {
|
||||
withContext(dispatcherProvider.Main) {
|
||||
billingDataRepo.startIapFlow(activity, CapodSku.PRO_UPGRADE.sku)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log(TAG) { "startIapFlow failed: $e" }
|
||||
errorEvents.emitBlocking(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun restorePurchase() = launch {
|
||||
try {
|
||||
val data = billingDataRepo.getIapData()
|
||||
log(TAG) { "Restore check: $data" }
|
||||
if (data.purchasedSkus.any { it.sku == CapodSku.PRO_UPGRADE.sku }) {
|
||||
log(TAG) { "Pro purchase found" }
|
||||
} else {
|
||||
log(TAG) { "No pro purchase found" }
|
||||
events.tryEmit(UpgradeEvent.RestoreFailed)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log(TAG) { "Restore failed: $e" }
|
||||
errorEvents.emitBlocking(e)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Upgrade", "VM")
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,5 @@
|
||||
<string name="upgrades_gplay_billing_error_description">There was an error in Google Play. Please try again later or reboot your phone.\n\nError: %s</string>
|
||||
<string name="upgrades_gplay_billing_result_error_label">Google Play Billing Error</string>
|
||||
<string name="upgrades_gplay_billing_result_error_description">There was an error when asking Google Play for your purchase details. Clear Google Play cache and reboot your phone.\n\nError %s</string>
|
||||
<string name="upgrade_restore_action">Restore purchase</string>
|
||||
</resources>
|
||||
@@ -18,6 +18,9 @@ object Nav {
|
||||
|
||||
@Serializable
|
||||
data object TroubleShooter : Main
|
||||
|
||||
@Serializable
|
||||
data object Upgrade : Main
|
||||
}
|
||||
|
||||
sealed interface Settings : NavigationDestination {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package eu.darken.capod.common.upgrade
|
||||
|
||||
import android.app.Activity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.time.Instant
|
||||
|
||||
interface UpgradeRepo {
|
||||
val upgradeInfo: Flow<Info>
|
||||
|
||||
fun launchBillingFlow(activity: Activity)
|
||||
|
||||
fun getSponsorUrl(): String? = null
|
||||
|
||||
interface Info {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package eu.darken.capod.main.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -53,6 +55,10 @@ class MainActivity : Activity2() {
|
||||
val backStack = rememberNavBackStack(startDestination)
|
||||
navCtrl.setup(backStack)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
consumeUpgradeExtra(intent)
|
||||
}
|
||||
|
||||
CapodTheme(state = themeState) {
|
||||
val backgroundColor = MaterialTheme.colorScheme.background
|
||||
val useDarkIcons = backgroundColor.luminance() > 0.5f
|
||||
@@ -85,7 +91,22 @@ class MainActivity : Activity2() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
consumeUpgradeExtra(intent)
|
||||
}
|
||||
|
||||
private fun consumeUpgradeExtra(intent: Intent?) {
|
||||
if (intent?.getBooleanExtra(EXTRA_NAVIGATE_TO_UPGRADE, false) == true) {
|
||||
intent.removeExtra(EXTRA_NAVIGATE_TO_UPGRADE)
|
||||
if (generalSettings.isOnboardingDone.value) {
|
||||
navCtrl.goTo(Nav.Main.Upgrade)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_NAVIGATE_TO_UPGRADE = "navigate_to_upgrade"
|
||||
private val TAG = logTag("MainActivity")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.darken.capod.main.ui.overview
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
@@ -67,7 +66,6 @@ fun OverviewScreenHost(vm: OverviewViewModel = hiltViewModel()) {
|
||||
NavigationEventHandler(vm)
|
||||
|
||||
val context = LocalContext.current
|
||||
val activity = context as? Activity
|
||||
|
||||
// Collect workerAutolaunch passively to keep it active
|
||||
LaunchedEffect(Unit) {
|
||||
@@ -122,13 +120,6 @@ fun OverviewScreenHost(vm: OverviewViewModel = hiltViewModel()) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle upgrade flow events
|
||||
LaunchedEffect(Unit) {
|
||||
vm.launchUpgradeFlow.collect { action ->
|
||||
activity?.let { action(it) }
|
||||
}
|
||||
}
|
||||
|
||||
val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
|
||||
val currentState = state ?: return
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.darken.capod.main.ui.overview
|
||||
|
||||
import android.app.Activity
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
@@ -50,7 +49,6 @@ class OverviewViewModel @Inject constructor(
|
||||
) : ViewModel4(dispatcherProvider) {
|
||||
|
||||
val requestPermissionEvent = SingleEventFlow<Permission>()
|
||||
val launchUpgradeFlow = SingleEventFlow<(Activity) -> Unit>()
|
||||
|
||||
private val showUnmatchedDevices = MutableStateFlow(false)
|
||||
|
||||
@@ -141,8 +139,8 @@ class OverviewViewModel @Inject constructor(
|
||||
navTo(Nav.Main.DeviceManager)
|
||||
}
|
||||
|
||||
fun onUpgrade() = launch {
|
||||
launchUpgradeFlow.tryEmit { upgradeRepo.launchBillingFlow(it) }
|
||||
fun onUpgrade() {
|
||||
navTo(Nav.Main.Upgrade)
|
||||
}
|
||||
|
||||
fun toggleUnmatchedDevices() {
|
||||
|
||||
@@ -94,11 +94,11 @@ fun SettingsScreen(
|
||||
},
|
||||
actions = {
|
||||
val sponsorUrl = state.sponsorUrl
|
||||
if (sponsorUrl != null) {
|
||||
if (state.isPro && sponsorUrl != null) {
|
||||
IconButton(onClick = { onSponsor(sponsorUrl) }) {
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.Favorite,
|
||||
contentDescription = "Sponsor development",
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -190,7 +190,7 @@ fun SettingsScreen(
|
||||
@Composable
|
||||
private fun SettingsScreenPreview() = PreviewWrapper {
|
||||
SettingsScreen(
|
||||
state = SettingsViewModel.State(sponsorUrl = "https://example.com"),
|
||||
state = SettingsViewModel.State(isPro = true, sponsorUrl = "https://example.com"),
|
||||
onNavigateUp = {},
|
||||
onGeneralSettings = {},
|
||||
onDeviceManager = {},
|
||||
|
||||
@@ -18,11 +18,12 @@ class SettingsViewModel @Inject constructor(
|
||||
) : ViewModel4(dispatcherProvider) {
|
||||
|
||||
data class State(
|
||||
val isPro: Boolean,
|
||||
val sponsorUrl: String?,
|
||||
)
|
||||
|
||||
val state = upgradeRepo.upgradeInfo
|
||||
.map { State(sponsorUrl = upgradeRepo.getSponsorUrl()) }
|
||||
.map { State(isPro = it.isPro, sponsorUrl = upgradeRepo.getSponsorUrl()) }
|
||||
.asLiveState()
|
||||
|
||||
fun openUrl(url: String) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.darken.capod.main.ui.settings.general
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -35,14 +34,12 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -69,11 +66,6 @@ fun GeneralSettingsScreenHost(vm: GeneralSettingsViewModel = hiltViewModel()) {
|
||||
ErrorEventHandler(vm)
|
||||
NavigationEventHandler(vm)
|
||||
|
||||
val activity = LocalContext.current as? Activity
|
||||
LaunchedEffect(Unit) {
|
||||
vm.launchUpgradeFlow.collect { action -> activity?.let { action(it) } }
|
||||
}
|
||||
|
||||
val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
|
||||
state?.let {
|
||||
GeneralSettingsScreen(
|
||||
|
||||
+5
-9
@@ -1,11 +1,9 @@
|
||||
package eu.darken.capod.main.ui.settings.general
|
||||
|
||||
import android.app.Activity
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.SingleEventFlow
|
||||
import eu.darken.capod.common.navigation.Nav
|
||||
import eu.darken.capod.common.theming.ThemeColor
|
||||
import eu.darken.capod.common.theming.ThemeMode
|
||||
@@ -42,8 +40,6 @@ class GeneralSettingsViewModel @Inject constructor(
|
||||
|
||||
private val isPro = upgradeRepo.upgradeInfo.map { it.isPro }.asLiveState()
|
||||
|
||||
val launchUpgradeFlow = SingleEventFlow<(Activity) -> Unit>()
|
||||
|
||||
val state = combine(
|
||||
combine(
|
||||
generalSettings.monitorMode.flow,
|
||||
@@ -110,7 +106,7 @@ class GeneralSettingsViewModel @Inject constructor(
|
||||
if (isPro.first()) {
|
||||
generalSettings.themeMode.value = mode
|
||||
} else {
|
||||
launchUpgradeFlow.tryEmit { upgradeRepo.launchBillingFlow(it) }
|
||||
navTo(Nav.Main.Upgrade)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +114,7 @@ class GeneralSettingsViewModel @Inject constructor(
|
||||
if (isPro.first()) {
|
||||
generalSettings.themeStyle.value = style
|
||||
} else {
|
||||
launchUpgradeFlow.tryEmit { upgradeRepo.launchBillingFlow(it) }
|
||||
navTo(Nav.Main.Upgrade)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,12 +122,12 @@ class GeneralSettingsViewModel @Inject constructor(
|
||||
if (isPro.first()) {
|
||||
generalSettings.themeColor.value = color
|
||||
} else {
|
||||
launchUpgradeFlow.tryEmit { upgradeRepo.launchBillingFlow(it) }
|
||||
navTo(Nav.Main.Upgrade)
|
||||
}
|
||||
}
|
||||
|
||||
fun launchUpgrade() = launch {
|
||||
launchUpgradeFlow.tryEmit { upgradeRepo.launchBillingFlow(it) }
|
||||
fun launchUpgrade() {
|
||||
navTo(Nav.Main.Upgrade)
|
||||
}
|
||||
|
||||
fun goToDebugSettings() {
|
||||
|
||||
@@ -22,7 +22,7 @@ import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.theming.CapodTheme
|
||||
import eu.darken.capod.common.uix.Activity2
|
||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||
import eu.darken.capod.main.ui.MainActivity
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.currentThemeState
|
||||
import eu.darken.capod.main.core.themeState
|
||||
@@ -34,7 +34,6 @@ class WidgetConfigurationActivity : Activity2() {
|
||||
|
||||
private val vm: WidgetConfigurationViewModel by viewModels()
|
||||
|
||||
@Inject lateinit var upgradeRepo: UpgradeRepo
|
||||
@Inject lateinit var generalSettings: GeneralSettings
|
||||
@ApplicationContext @Inject lateinit var appContext: Context
|
||||
|
||||
@@ -87,7 +86,11 @@ class WidgetConfigurationActivity : Activity2() {
|
||||
if (currentState.isPro) {
|
||||
confirmSelection()
|
||||
} else {
|
||||
upgradeRepo.launchBillingFlow(this@WidgetConfigurationActivity)
|
||||
startActivity(
|
||||
Intent(this@WidgetConfigurationActivity, MainActivity::class.java).apply {
|
||||
putExtra(MainActivity.EXTRA_NAVIGATE_TO_UPGRADE, true)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
onCancel = { finish() },
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package eu.darken.capod.reaction.ui
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -16,6 +15,8 @@ import androidx.compose.material.icons.automirrored.twotone.Message
|
||||
import androidx.compose.material.icons.twotone.PauseCircle
|
||||
import androidx.compose.material.icons.twotone.PlayCircle
|
||||
import androidx.compose.material.icons.twotone.QuestionMark
|
||||
import androidx.compose.material.icons.twotone.Stars
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.twotone.Workspaces
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -35,7 +36,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -56,19 +56,18 @@ fun ReactionSettingsScreenHost(vm: ReactionSettingsViewModel = hiltViewModel())
|
||||
NavigationEventHandler(vm)
|
||||
|
||||
val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
|
||||
val activity = LocalContext.current as Activity
|
||||
|
||||
state?.let {
|
||||
ReactionSettingsScreen(
|
||||
state = it,
|
||||
onNavigateUp = { vm.navUp() },
|
||||
onOnePodModeChanged = { enabled -> vm.setOnePodMode(enabled) },
|
||||
onAutoPlayChanged = { enabled -> vm.setAutoPlay(enabled, activity) },
|
||||
onAutoPauseChanged = { enabled -> vm.setAutoPause(enabled, activity) },
|
||||
onAutoConnectChanged = { enabled -> vm.setAutoConnect(enabled, activity) },
|
||||
onAutoPlayChanged = { enabled -> vm.setAutoPlay(enabled) },
|
||||
onAutoPauseChanged = { enabled -> vm.setAutoPause(enabled) },
|
||||
onAutoConnectChanged = { enabled -> vm.setAutoConnect(enabled) },
|
||||
onAutoConnectConditionSelected = { condition -> vm.setAutoConnectCondition(condition) },
|
||||
onShowPopUpOnCaseOpenChanged = { enabled -> vm.setShowPopUpOnCaseOpen(enabled, activity) },
|
||||
onShowPopUpOnConnectionChanged = { enabled -> vm.setShowPopUpOnConnection(enabled, activity) },
|
||||
onShowPopUpOnCaseOpenChanged = { enabled -> vm.setShowPopUpOnCaseOpen(enabled) },
|
||||
onShowPopUpOnConnectionChanged = { enabled -> vm.setShowPopUpOnConnection(enabled) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -131,11 +130,7 @@ fun ReactionSettingsScreen(
|
||||
icon = Icons.TwoTone.PlayCircle,
|
||||
onClick = { onAutoPlayChanged(!state.autoPlay) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = state.autoPlay,
|
||||
onCheckedChange = onAutoPlayChanged,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
ProGatedToggle(isPro = state.isPro, checked = state.autoPlay, onCheckedChange = onAutoPlayChanged)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -146,11 +141,7 @@ fun ReactionSettingsScreen(
|
||||
icon = Icons.TwoTone.PauseCircle,
|
||||
onClick = { onAutoPauseChanged(!state.autoPause) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = state.autoPause,
|
||||
onCheckedChange = onAutoPauseChanged,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
ProGatedToggle(isPro = state.isPro, checked = state.autoPause, onCheckedChange = onAutoPauseChanged)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -173,11 +164,7 @@ fun ReactionSettingsScreen(
|
||||
icon = Icons.TwoTone.BluetoothConnected,
|
||||
onClick = { onAutoConnectChanged(!state.autoConnect) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = state.autoConnect,
|
||||
onCheckedChange = onAutoConnectChanged,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
ProGatedToggle(isPro = state.isPro, checked = state.autoConnect, onCheckedChange = onAutoConnectChanged)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -200,11 +187,7 @@ fun ReactionSettingsScreen(
|
||||
icon = Icons.AutoMirrored.TwoTone.Message,
|
||||
onClick = { onShowPopUpOnCaseOpenChanged(!state.showPopUpOnCaseOpen) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = state.showPopUpOnCaseOpen,
|
||||
onCheckedChange = onShowPopUpOnCaseOpenChanged,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
ProGatedToggle(isPro = state.isPro, checked = state.showPopUpOnCaseOpen, onCheckedChange = onShowPopUpOnCaseOpenChanged)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -215,11 +198,7 @@ fun ReactionSettingsScreen(
|
||||
icon = Icons.AutoMirrored.TwoTone.Message,
|
||||
onClick = { onShowPopUpOnConnectionChanged(!state.showPopUpOnConnection) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = state.showPopUpOnConnection,
|
||||
onCheckedChange = onShowPopUpOnConnectionChanged,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
ProGatedToggle(isPro = state.isPro, checked = state.showPopUpOnConnection, onCheckedChange = onShowPopUpOnConnectionChanged)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -270,6 +249,28 @@ fun ReactionSettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProGatedToggle(
|
||||
isPro: Boolean,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
if (isPro) {
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.Stars,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 16.dp).size(24.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun ReactionSettingsScreenPreview() = PreviewWrapper {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package eu.darken.capod.reaction.ui
|
||||
|
||||
import android.app.Activity
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
|
||||
import eu.darken.capod.common.navigation.Nav
|
||||
import eu.darken.capod.common.uix.ViewModel4
|
||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
@@ -63,7 +62,7 @@ class ReactionSettingsViewModel @Inject constructor(
|
||||
reactionSettings.onePodMode.value = enabled
|
||||
}
|
||||
|
||||
fun setAutoPlay(enabled: Boolean, activity: Activity) = launch {
|
||||
fun setAutoPlay(enabled: Boolean) = launch {
|
||||
if (!enabled) {
|
||||
reactionSettings.autoPlay.value = false
|
||||
return@launch
|
||||
@@ -71,11 +70,11 @@ class ReactionSettingsViewModel @Inject constructor(
|
||||
if (isPro.first()) {
|
||||
reactionSettings.autoPlay.value = true
|
||||
} else {
|
||||
upgradeRepo.launchBillingFlow(activity)
|
||||
navTo(Nav.Main.Upgrade)
|
||||
}
|
||||
}
|
||||
|
||||
fun setAutoPause(enabled: Boolean, activity: Activity) = launch {
|
||||
fun setAutoPause(enabled: Boolean) = launch {
|
||||
if (!enabled) {
|
||||
reactionSettings.autoPause.value = false
|
||||
return@launch
|
||||
@@ -83,11 +82,11 @@ class ReactionSettingsViewModel @Inject constructor(
|
||||
if (isPro.first()) {
|
||||
reactionSettings.autoPause.value = true
|
||||
} else {
|
||||
upgradeRepo.launchBillingFlow(activity)
|
||||
navTo(Nav.Main.Upgrade)
|
||||
}
|
||||
}
|
||||
|
||||
fun setAutoConnect(enabled: Boolean, activity: Activity) = launch {
|
||||
fun setAutoConnect(enabled: Boolean) = launch {
|
||||
if (!enabled) {
|
||||
reactionSettings.autoConnect.value = false
|
||||
return@launch
|
||||
@@ -96,7 +95,7 @@ class ReactionSettingsViewModel @Inject constructor(
|
||||
reactionSettings.autoConnect.value = true
|
||||
generalSettings.monitorMode.value = MonitorMode.ALWAYS
|
||||
} else {
|
||||
upgradeRepo.launchBillingFlow(activity)
|
||||
navTo(Nav.Main.Upgrade)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +103,7 @@ class ReactionSettingsViewModel @Inject constructor(
|
||||
reactionSettings.autoConnectCondition.value = condition
|
||||
}
|
||||
|
||||
fun setShowPopUpOnCaseOpen(enabled: Boolean, activity: Activity) = launch {
|
||||
fun setShowPopUpOnCaseOpen(enabled: Boolean) = launch {
|
||||
if (!enabled) {
|
||||
reactionSettings.showPopUpOnCaseOpen.value = false
|
||||
return@launch
|
||||
@@ -112,11 +111,11 @@ class ReactionSettingsViewModel @Inject constructor(
|
||||
if (isPro.first()) {
|
||||
reactionSettings.showPopUpOnCaseOpen.value = true
|
||||
} else {
|
||||
upgradeRepo.launchBillingFlow(activity)
|
||||
navTo(Nav.Main.Upgrade)
|
||||
}
|
||||
}
|
||||
|
||||
fun setShowPopUpOnConnection(enabled: Boolean, activity: Activity) = launch {
|
||||
fun setShowPopUpOnConnection(enabled: Boolean) = launch {
|
||||
if (!enabled) {
|
||||
reactionSettings.showPopUpOnConnection.value = false
|
||||
return@launch
|
||||
@@ -124,7 +123,7 @@ class ReactionSettingsViewModel @Inject constructor(
|
||||
if (isPro.first()) {
|
||||
reactionSettings.showPopUpOnConnection.value = true
|
||||
} else {
|
||||
upgradeRepo.launchBillingFlow(activity)
|
||||
navTo(Nav.Main.Upgrade)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,13 @@
|
||||
|
||||
<string name="upgrade_capod_label">Upgrade CAPod</string>
|
||||
<string name="upgrade_capod_description">Get additional features and support the developer.</string>
|
||||
<string name="upgrade_preamble">CAPod is developed by a single person. Upgrading unlocks extra features and helps keep the app alive.</string>
|
||||
<string name="upgrade_benefit_themes">Theme customization</string>
|
||||
<string name="upgrade_benefit_autoplay">Auto-play & auto-pause</string>
|
||||
<string name="upgrade_benefit_autoconnect">Auto-connect</string>
|
||||
<string name="upgrade_benefit_popups">Popup on case open & connection</string>
|
||||
<string name="upgrade_benefit_widgets">Home screen widgets</string>
|
||||
<string name="upgrade_benefit_support">Support the developer</string>
|
||||
|
||||
<string name="settings_monitor_mode_label">Monitor mode</string>
|
||||
<string name="settings_monitor_mode_description">Under which circumstances this app monitors Bluetooth data.</string>
|
||||
|
||||
Reference in New Issue
Block a user