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
|
@Composable
|
||||||
internal fun SettingsIndexContent() = PreviewWrapper {
|
internal fun SettingsIndexContent() = PreviewWrapper {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
state = SettingsViewModel.State(sponsorUrl = "https://example.com"),
|
state = SettingsViewModel.State(isPro = true, sponsorUrl = "https://example.com"),
|
||||||
onNavigateUp = {},
|
onNavigateUp = {},
|
||||||
onGeneralSettings = {},
|
onGeneralSettings = {},
|
||||||
onDeviceManager = {},
|
onDeviceManager = {},
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
package eu.darken.capod.common.upgrade.core
|
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 eu.darken.capod.common.upgrade.UpgradeRepo
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@@ -15,7 +10,6 @@ import javax.inject.Singleton
|
|||||||
@Singleton
|
@Singleton
|
||||||
class UpgradeControlFoss @Inject constructor(
|
class UpgradeControlFoss @Inject constructor(
|
||||||
private val fossCache: FossCache,
|
private val fossCache: FossCache,
|
||||||
private val webpageTool: WebpageTool,
|
|
||||||
) : UpgradeRepo {
|
) : UpgradeRepo {
|
||||||
|
|
||||||
override val upgradeInfo: Flow<UpgradeRepo.Info> = fossCache.upgrade.flow.map { data ->
|
override val upgradeInfo: Flow<UpgradeRepo.Info> = fossCache.upgrade.flow.map { data ->
|
||||||
@@ -30,34 +24,11 @@ class UpgradeControlFoss @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun launchBillingFlow(activity: Activity) {
|
fun upgrade(reason: FossUpgrade.Reason) {
|
||||||
MaterialAlertDialogBuilder(activity).apply {
|
fossCache.upgrade.value = FossUpgrade(
|
||||||
setIcon(R.drawable.ic_heart)
|
upgradedAt = Instant.now(),
|
||||||
setTitle(R.string.upgrade_capod_label)
|
reason = reason
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Info(
|
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_donate_label">Donate</string>
|
||||||
<string name="foss_upgrade_alreadydonated_label">I already donated</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="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>
|
</resources>
|
||||||
@@ -1,16 +1,9 @@
|
|||||||
package eu.darken.capod.common.upgrade.core
|
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.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.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.log
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
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.flow.replayingShare
|
||||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||||
import eu.darken.capod.common.upgrade.core.data.BillingData
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.retryWhen
|
import kotlinx.coroutines.flow.retryWhen
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -31,7 +22,6 @@ import kotlin.math.pow
|
|||||||
@Singleton
|
@Singleton
|
||||||
class UpgradeRepoGplay @Inject constructor(
|
class UpgradeRepoGplay @Inject constructor(
|
||||||
@AppScope private val scope: CoroutineScope,
|
@AppScope private val scope: CoroutineScope,
|
||||||
private val dispatcherProvider: DispatcherProvider,
|
|
||||||
private val billingDataRepo: BillingDataRepo,
|
private val billingDataRepo: BillingDataRepo,
|
||||||
private val billingCache: BillingCache,
|
private val billingCache: BillingCache,
|
||||||
) : UpgradeRepo {
|
) : UpgradeRepo {
|
||||||
@@ -76,49 +66,6 @@ class UpgradeRepoGplay @Inject constructor(
|
|||||||
}
|
}
|
||||||
.replayingShare(scope)
|
.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(
|
data class Info(
|
||||||
private val gracePeriod: Boolean = false,
|
private val gracePeriod: Boolean = false,
|
||||||
private val billingData: BillingData?,
|
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_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_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="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>
|
</resources>
|
||||||
@@ -18,6 +18,9 @@ object Nav {
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object TroubleShooter : Main
|
data object TroubleShooter : Main
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Upgrade : Main
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface Settings : NavigationDestination {
|
sealed interface Settings : NavigationDestination {
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
package eu.darken.capod.common.upgrade
|
package eu.darken.capod.common.upgrade
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
interface UpgradeRepo {
|
interface UpgradeRepo {
|
||||||
val upgradeInfo: Flow<Info>
|
val upgradeInfo: Flow<Info>
|
||||||
|
|
||||||
fun launchBillingFlow(activity: Activity)
|
|
||||||
|
|
||||||
fun getSponsorUrl(): String? = null
|
fun getSponsorUrl(): String? = null
|
||||||
|
|
||||||
interface Info {
|
interface Info {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package eu.darken.capod.main.ui
|
package eu.darken.capod.main.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@@ -53,6 +55,10 @@ class MainActivity : Activity2() {
|
|||||||
val backStack = rememberNavBackStack(startDestination)
|
val backStack = rememberNavBackStack(startDestination)
|
||||||
navCtrl.setup(backStack)
|
navCtrl.setup(backStack)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
consumeUpgradeExtra(intent)
|
||||||
|
}
|
||||||
|
|
||||||
CapodTheme(state = themeState) {
|
CapodTheme(state = themeState) {
|
||||||
val backgroundColor = MaterialTheme.colorScheme.background
|
val backgroundColor = MaterialTheme.colorScheme.background
|
||||||
val useDarkIcons = backgroundColor.luminance() > 0.5f
|
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 {
|
companion object {
|
||||||
|
const val EXTRA_NAVIGATE_TO_UPGRADE = "navigate_to_upgrade"
|
||||||
private val TAG = logTag("MainActivity")
|
private val TAG = logTag("MainActivity")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.darken.capod.main.ui.overview
|
package eu.darken.capod.main.ui.overview
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
@@ -67,7 +66,6 @@ fun OverviewScreenHost(vm: OverviewViewModel = hiltViewModel()) {
|
|||||||
NavigationEventHandler(vm)
|
NavigationEventHandler(vm)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val activity = context as? Activity
|
|
||||||
|
|
||||||
// Collect workerAutolaunch passively to keep it active
|
// Collect workerAutolaunch passively to keep it active
|
||||||
LaunchedEffect(Unit) {
|
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 state by vm.state.collectAsStateWithLifecycle(initialValue = null)
|
||||||
val currentState = state ?: return
|
val currentState = state ?: return
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.darken.capod.main.ui.overview
|
package eu.darken.capod.main.ui.overview
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||||
@@ -50,7 +49,6 @@ class OverviewViewModel @Inject constructor(
|
|||||||
) : ViewModel4(dispatcherProvider) {
|
) : ViewModel4(dispatcherProvider) {
|
||||||
|
|
||||||
val requestPermissionEvent = SingleEventFlow<Permission>()
|
val requestPermissionEvent = SingleEventFlow<Permission>()
|
||||||
val launchUpgradeFlow = SingleEventFlow<(Activity) -> Unit>()
|
|
||||||
|
|
||||||
private val showUnmatchedDevices = MutableStateFlow(false)
|
private val showUnmatchedDevices = MutableStateFlow(false)
|
||||||
|
|
||||||
@@ -141,8 +139,8 @@ class OverviewViewModel @Inject constructor(
|
|||||||
navTo(Nav.Main.DeviceManager)
|
navTo(Nav.Main.DeviceManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onUpgrade() = launch {
|
fun onUpgrade() {
|
||||||
launchUpgradeFlow.tryEmit { upgradeRepo.launchBillingFlow(it) }
|
navTo(Nav.Main.Upgrade)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleUnmatchedDevices() {
|
fun toggleUnmatchedDevices() {
|
||||||
|
|||||||
@@ -94,11 +94,11 @@ fun SettingsScreen(
|
|||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
val sponsorUrl = state.sponsorUrl
|
val sponsorUrl = state.sponsorUrl
|
||||||
if (sponsorUrl != null) {
|
if (state.isPro && sponsorUrl != null) {
|
||||||
IconButton(onClick = { onSponsor(sponsorUrl) }) {
|
IconButton(onClick = { onSponsor(sponsorUrl) }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.TwoTone.Favorite,
|
imageVector = Icons.TwoTone.Favorite,
|
||||||
contentDescription = "Sponsor development",
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,7 @@ fun SettingsScreen(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SettingsScreenPreview() = PreviewWrapper {
|
private fun SettingsScreenPreview() = PreviewWrapper {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
state = SettingsViewModel.State(sponsorUrl = "https://example.com"),
|
state = SettingsViewModel.State(isPro = true, sponsorUrl = "https://example.com"),
|
||||||
onNavigateUp = {},
|
onNavigateUp = {},
|
||||||
onGeneralSettings = {},
|
onGeneralSettings = {},
|
||||||
onDeviceManager = {},
|
onDeviceManager = {},
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ class SettingsViewModel @Inject constructor(
|
|||||||
) : ViewModel4(dispatcherProvider) {
|
) : ViewModel4(dispatcherProvider) {
|
||||||
|
|
||||||
data class State(
|
data class State(
|
||||||
|
val isPro: Boolean,
|
||||||
val sponsorUrl: String?,
|
val sponsorUrl: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
val state = upgradeRepo.upgradeInfo
|
val state = upgradeRepo.upgradeInfo
|
||||||
.map { State(sponsorUrl = upgradeRepo.getSponsorUrl()) }
|
.map { State(isPro = it.isPro, sponsorUrl = upgradeRepo.getSponsorUrl()) }
|
||||||
.asLiveState()
|
.asLiveState()
|
||||||
|
|
||||||
fun openUrl(url: String) {
|
fun openUrl(url: String) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.darken.capod.main.ui.settings.general
|
package eu.darken.capod.main.ui.settings.general
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -35,14 +34,12 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -69,11 +66,6 @@ fun GeneralSettingsScreenHost(vm: GeneralSettingsViewModel = hiltViewModel()) {
|
|||||||
ErrorEventHandler(vm)
|
ErrorEventHandler(vm)
|
||||||
NavigationEventHandler(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)
|
val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
|
||||||
state?.let {
|
state?.let {
|
||||||
GeneralSettingsScreen(
|
GeneralSettingsScreen(
|
||||||
|
|||||||
+5
-9
@@ -1,11 +1,9 @@
|
|||||||
package eu.darken.capod.main.ui.settings.general
|
package eu.darken.capod.main.ui.settings.general
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
import eu.darken.capod.common.bluetooth.ScannerMode
|
||||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
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.navigation.Nav
|
||||||
import eu.darken.capod.common.theming.ThemeColor
|
import eu.darken.capod.common.theming.ThemeColor
|
||||||
import eu.darken.capod.common.theming.ThemeMode
|
import eu.darken.capod.common.theming.ThemeMode
|
||||||
@@ -42,8 +40,6 @@ class GeneralSettingsViewModel @Inject constructor(
|
|||||||
|
|
||||||
private val isPro = upgradeRepo.upgradeInfo.map { it.isPro }.asLiveState()
|
private val isPro = upgradeRepo.upgradeInfo.map { it.isPro }.asLiveState()
|
||||||
|
|
||||||
val launchUpgradeFlow = SingleEventFlow<(Activity) -> Unit>()
|
|
||||||
|
|
||||||
val state = combine(
|
val state = combine(
|
||||||
combine(
|
combine(
|
||||||
generalSettings.monitorMode.flow,
|
generalSettings.monitorMode.flow,
|
||||||
@@ -110,7 +106,7 @@ class GeneralSettingsViewModel @Inject constructor(
|
|||||||
if (isPro.first()) {
|
if (isPro.first()) {
|
||||||
generalSettings.themeMode.value = mode
|
generalSettings.themeMode.value = mode
|
||||||
} else {
|
} else {
|
||||||
launchUpgradeFlow.tryEmit { upgradeRepo.launchBillingFlow(it) }
|
navTo(Nav.Main.Upgrade)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +114,7 @@ class GeneralSettingsViewModel @Inject constructor(
|
|||||||
if (isPro.first()) {
|
if (isPro.first()) {
|
||||||
generalSettings.themeStyle.value = style
|
generalSettings.themeStyle.value = style
|
||||||
} else {
|
} else {
|
||||||
launchUpgradeFlow.tryEmit { upgradeRepo.launchBillingFlow(it) }
|
navTo(Nav.Main.Upgrade)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,12 +122,12 @@ class GeneralSettingsViewModel @Inject constructor(
|
|||||||
if (isPro.first()) {
|
if (isPro.first()) {
|
||||||
generalSettings.themeColor.value = color
|
generalSettings.themeColor.value = color
|
||||||
} else {
|
} else {
|
||||||
launchUpgradeFlow.tryEmit { upgradeRepo.launchBillingFlow(it) }
|
navTo(Nav.Main.Upgrade)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchUpgrade() = launch {
|
fun launchUpgrade() {
|
||||||
launchUpgradeFlow.tryEmit { upgradeRepo.launchBillingFlow(it) }
|
navTo(Nav.Main.Upgrade)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun goToDebugSettings() {
|
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.debug.logging.logTag
|
||||||
import eu.darken.capod.common.theming.CapodTheme
|
import eu.darken.capod.common.theming.CapodTheme
|
||||||
import eu.darken.capod.common.uix.Activity2
|
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.GeneralSettings
|
||||||
import eu.darken.capod.main.core.currentThemeState
|
import eu.darken.capod.main.core.currentThemeState
|
||||||
import eu.darken.capod.main.core.themeState
|
import eu.darken.capod.main.core.themeState
|
||||||
@@ -34,7 +34,6 @@ class WidgetConfigurationActivity : Activity2() {
|
|||||||
|
|
||||||
private val vm: WidgetConfigurationViewModel by viewModels()
|
private val vm: WidgetConfigurationViewModel by viewModels()
|
||||||
|
|
||||||
@Inject lateinit var upgradeRepo: UpgradeRepo
|
|
||||||
@Inject lateinit var generalSettings: GeneralSettings
|
@Inject lateinit var generalSettings: GeneralSettings
|
||||||
@ApplicationContext @Inject lateinit var appContext: Context
|
@ApplicationContext @Inject lateinit var appContext: Context
|
||||||
|
|
||||||
@@ -87,7 +86,11 @@ class WidgetConfigurationActivity : Activity2() {
|
|||||||
if (currentState.isPro) {
|
if (currentState.isPro) {
|
||||||
confirmSelection()
|
confirmSelection()
|
||||||
} else {
|
} else {
|
||||||
upgradeRepo.launchBillingFlow(this@WidgetConfigurationActivity)
|
startActivity(
|
||||||
|
Intent(this@WidgetConfigurationActivity, MainActivity::class.java).apply {
|
||||||
|
putExtra(MainActivity.EXTRA_NAVIGATE_TO_UPGRADE, true)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCancel = { finish() },
|
onCancel = { finish() },
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.darken.capod.reaction.ui
|
package eu.darken.capod.reaction.ui
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.PauseCircle
|
||||||
import androidx.compose.material.icons.twotone.PlayCircle
|
import androidx.compose.material.icons.twotone.PlayCircle
|
||||||
import androidx.compose.material.icons.twotone.QuestionMark
|
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.material.icons.twotone.Workspaces
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -35,7 +36,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -56,19 +56,18 @@ fun ReactionSettingsScreenHost(vm: ReactionSettingsViewModel = hiltViewModel())
|
|||||||
NavigationEventHandler(vm)
|
NavigationEventHandler(vm)
|
||||||
|
|
||||||
val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
|
val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
|
||||||
val activity = LocalContext.current as Activity
|
|
||||||
|
|
||||||
state?.let {
|
state?.let {
|
||||||
ReactionSettingsScreen(
|
ReactionSettingsScreen(
|
||||||
state = it,
|
state = it,
|
||||||
onNavigateUp = { vm.navUp() },
|
onNavigateUp = { vm.navUp() },
|
||||||
onOnePodModeChanged = { enabled -> vm.setOnePodMode(enabled) },
|
onOnePodModeChanged = { enabled -> vm.setOnePodMode(enabled) },
|
||||||
onAutoPlayChanged = { enabled -> vm.setAutoPlay(enabled, activity) },
|
onAutoPlayChanged = { enabled -> vm.setAutoPlay(enabled) },
|
||||||
onAutoPauseChanged = { enabled -> vm.setAutoPause(enabled, activity) },
|
onAutoPauseChanged = { enabled -> vm.setAutoPause(enabled) },
|
||||||
onAutoConnectChanged = { enabled -> vm.setAutoConnect(enabled, activity) },
|
onAutoConnectChanged = { enabled -> vm.setAutoConnect(enabled) },
|
||||||
onAutoConnectConditionSelected = { condition -> vm.setAutoConnectCondition(condition) },
|
onAutoConnectConditionSelected = { condition -> vm.setAutoConnectCondition(condition) },
|
||||||
onShowPopUpOnCaseOpenChanged = { enabled -> vm.setShowPopUpOnCaseOpen(enabled, activity) },
|
onShowPopUpOnCaseOpenChanged = { enabled -> vm.setShowPopUpOnCaseOpen(enabled) },
|
||||||
onShowPopUpOnConnectionChanged = { enabled -> vm.setShowPopUpOnConnection(enabled, activity) },
|
onShowPopUpOnConnectionChanged = { enabled -> vm.setShowPopUpOnConnection(enabled) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,11 +130,7 @@ fun ReactionSettingsScreen(
|
|||||||
icon = Icons.TwoTone.PlayCircle,
|
icon = Icons.TwoTone.PlayCircle,
|
||||||
onClick = { onAutoPlayChanged(!state.autoPlay) },
|
onClick = { onAutoPlayChanged(!state.autoPlay) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
ProGatedToggle(isPro = state.isPro, checked = state.autoPlay, onCheckedChange = onAutoPlayChanged)
|
||||||
checked = state.autoPlay,
|
|
||||||
onCheckedChange = onAutoPlayChanged,
|
|
||||||
modifier = Modifier.padding(start = 16.dp),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -146,11 +141,7 @@ fun ReactionSettingsScreen(
|
|||||||
icon = Icons.TwoTone.PauseCircle,
|
icon = Icons.TwoTone.PauseCircle,
|
||||||
onClick = { onAutoPauseChanged(!state.autoPause) },
|
onClick = { onAutoPauseChanged(!state.autoPause) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
ProGatedToggle(isPro = state.isPro, checked = state.autoPause, onCheckedChange = onAutoPauseChanged)
|
||||||
checked = state.autoPause,
|
|
||||||
onCheckedChange = onAutoPauseChanged,
|
|
||||||
modifier = Modifier.padding(start = 16.dp),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -173,11 +164,7 @@ fun ReactionSettingsScreen(
|
|||||||
icon = Icons.TwoTone.BluetoothConnected,
|
icon = Icons.TwoTone.BluetoothConnected,
|
||||||
onClick = { onAutoConnectChanged(!state.autoConnect) },
|
onClick = { onAutoConnectChanged(!state.autoConnect) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
ProGatedToggle(isPro = state.isPro, checked = state.autoConnect, onCheckedChange = onAutoConnectChanged)
|
||||||
checked = state.autoConnect,
|
|
||||||
onCheckedChange = onAutoConnectChanged,
|
|
||||||
modifier = Modifier.padding(start = 16.dp),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -200,11 +187,7 @@ fun ReactionSettingsScreen(
|
|||||||
icon = Icons.AutoMirrored.TwoTone.Message,
|
icon = Icons.AutoMirrored.TwoTone.Message,
|
||||||
onClick = { onShowPopUpOnCaseOpenChanged(!state.showPopUpOnCaseOpen) },
|
onClick = { onShowPopUpOnCaseOpenChanged(!state.showPopUpOnCaseOpen) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
ProGatedToggle(isPro = state.isPro, checked = state.showPopUpOnCaseOpen, onCheckedChange = onShowPopUpOnCaseOpenChanged)
|
||||||
checked = state.showPopUpOnCaseOpen,
|
|
||||||
onCheckedChange = onShowPopUpOnCaseOpenChanged,
|
|
||||||
modifier = Modifier.padding(start = 16.dp),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -215,11 +198,7 @@ fun ReactionSettingsScreen(
|
|||||||
icon = Icons.AutoMirrored.TwoTone.Message,
|
icon = Icons.AutoMirrored.TwoTone.Message,
|
||||||
onClick = { onShowPopUpOnConnectionChanged(!state.showPopUpOnConnection) },
|
onClick = { onShowPopUpOnConnectionChanged(!state.showPopUpOnConnection) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
ProGatedToggle(isPro = state.isPro, checked = state.showPopUpOnConnection, onCheckedChange = onShowPopUpOnConnectionChanged)
|
||||||
checked = state.showPopUpOnConnection,
|
|
||||||
onCheckedChange = onShowPopUpOnConnectionChanged,
|
|
||||||
modifier = Modifier.padding(start = 16.dp),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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
|
@Preview2
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReactionSettingsScreenPreview() = PreviewWrapper {
|
private fun ReactionSettingsScreenPreview() = PreviewWrapper {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package eu.darken.capod.reaction.ui
|
package eu.darken.capod.reaction.ui
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
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.uix.ViewModel4
|
||||||
import eu.darken.capod.common.upgrade.UpgradeRepo
|
import eu.darken.capod.common.upgrade.UpgradeRepo
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
@@ -63,7 +62,7 @@ class ReactionSettingsViewModel @Inject constructor(
|
|||||||
reactionSettings.onePodMode.value = enabled
|
reactionSettings.onePodMode.value = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAutoPlay(enabled: Boolean, activity: Activity) = launch {
|
fun setAutoPlay(enabled: Boolean) = launch {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
reactionSettings.autoPlay.value = false
|
reactionSettings.autoPlay.value = false
|
||||||
return@launch
|
return@launch
|
||||||
@@ -71,11 +70,11 @@ class ReactionSettingsViewModel @Inject constructor(
|
|||||||
if (isPro.first()) {
|
if (isPro.first()) {
|
||||||
reactionSettings.autoPlay.value = true
|
reactionSettings.autoPlay.value = true
|
||||||
} else {
|
} else {
|
||||||
upgradeRepo.launchBillingFlow(activity)
|
navTo(Nav.Main.Upgrade)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAutoPause(enabled: Boolean, activity: Activity) = launch {
|
fun setAutoPause(enabled: Boolean) = launch {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
reactionSettings.autoPause.value = false
|
reactionSettings.autoPause.value = false
|
||||||
return@launch
|
return@launch
|
||||||
@@ -83,11 +82,11 @@ class ReactionSettingsViewModel @Inject constructor(
|
|||||||
if (isPro.first()) {
|
if (isPro.first()) {
|
||||||
reactionSettings.autoPause.value = true
|
reactionSettings.autoPause.value = true
|
||||||
} else {
|
} else {
|
||||||
upgradeRepo.launchBillingFlow(activity)
|
navTo(Nav.Main.Upgrade)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAutoConnect(enabled: Boolean, activity: Activity) = launch {
|
fun setAutoConnect(enabled: Boolean) = launch {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
reactionSettings.autoConnect.value = false
|
reactionSettings.autoConnect.value = false
|
||||||
return@launch
|
return@launch
|
||||||
@@ -96,7 +95,7 @@ class ReactionSettingsViewModel @Inject constructor(
|
|||||||
reactionSettings.autoConnect.value = true
|
reactionSettings.autoConnect.value = true
|
||||||
generalSettings.monitorMode.value = MonitorMode.ALWAYS
|
generalSettings.monitorMode.value = MonitorMode.ALWAYS
|
||||||
} else {
|
} else {
|
||||||
upgradeRepo.launchBillingFlow(activity)
|
navTo(Nav.Main.Upgrade)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +103,7 @@ class ReactionSettingsViewModel @Inject constructor(
|
|||||||
reactionSettings.autoConnectCondition.value = condition
|
reactionSettings.autoConnectCondition.value = condition
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setShowPopUpOnCaseOpen(enabled: Boolean, activity: Activity) = launch {
|
fun setShowPopUpOnCaseOpen(enabled: Boolean) = launch {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
reactionSettings.showPopUpOnCaseOpen.value = false
|
reactionSettings.showPopUpOnCaseOpen.value = false
|
||||||
return@launch
|
return@launch
|
||||||
@@ -112,11 +111,11 @@ class ReactionSettingsViewModel @Inject constructor(
|
|||||||
if (isPro.first()) {
|
if (isPro.first()) {
|
||||||
reactionSettings.showPopUpOnCaseOpen.value = true
|
reactionSettings.showPopUpOnCaseOpen.value = true
|
||||||
} else {
|
} else {
|
||||||
upgradeRepo.launchBillingFlow(activity)
|
navTo(Nav.Main.Upgrade)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setShowPopUpOnConnection(enabled: Boolean, activity: Activity) = launch {
|
fun setShowPopUpOnConnection(enabled: Boolean) = launch {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
reactionSettings.showPopUpOnConnection.value = false
|
reactionSettings.showPopUpOnConnection.value = false
|
||||||
return@launch
|
return@launch
|
||||||
@@ -124,7 +123,7 @@ class ReactionSettingsViewModel @Inject constructor(
|
|||||||
if (isPro.first()) {
|
if (isPro.first()) {
|
||||||
reactionSettings.showPopUpOnConnection.value = true
|
reactionSettings.showPopUpOnConnection.value = true
|
||||||
} else {
|
} else {
|
||||||
upgradeRepo.launchBillingFlow(activity)
|
navTo(Nav.Main.Upgrade)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,13 @@
|
|||||||
|
|
||||||
<string name="upgrade_capod_label">Upgrade CAPod</string>
|
<string name="upgrade_capod_label">Upgrade CAPod</string>
|
||||||
<string name="upgrade_capod_description">Get additional features and support the developer.</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_label">Monitor mode</string>
|
||||||
<string name="settings_monitor_mode_description">Under which circumstances this app monitors Bluetooth data.</string>
|
<string name="settings_monitor_mode_description">Under which circumstances this app monitors Bluetooth data.</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user