feat(overview): Gate dashboard device count for free users

Free users see only their highest-priority profiled device. Additional devices are hidden behind an upgrade card showing the count and a flavor-aware CTA (Upgrade/Donate).
This commit is contained in:
darken
2026-04-02 18:30:40 +02:00
committed by Matthias Urhahn
parent 927a198d8e
commit c609b03148
5 changed files with 232 additions and 2 deletions
@@ -46,6 +46,7 @@ import eu.darken.capod.common.navigation.NavigationEventHandler
import eu.darken.capod.common.permissions.Permission
import eu.darken.capod.common.upgrade.UpgradeRepo
import eu.darken.capod.main.ui.overview.cards.BluetoothDisabledCard
import eu.darken.capod.main.ui.overview.cards.DeviceLimitUpgradeCard
import eu.darken.capod.main.ui.overview.cards.DualPodsCard
import eu.darken.capod.main.ui.overview.cards.MonitoringActiveCard
import eu.darken.capod.main.ui.overview.cards.NoProfilesCard
@@ -221,10 +222,10 @@ fun OverviewScreen(
}
}
// 4. Profiled device cards
// 4. Profiled device cards (limited to 1 for free users)
if (!state.isScanBlocked && state.isBluetoothEnabled) {
items(
items = state.profiledDevices,
items = state.visibleProfiledDevices,
key = { it.identifier?.toString() ?: it.hashCode() },
) { device ->
PodDeviceCard(
@@ -236,6 +237,17 @@ fun OverviewScreen(
)
}
// 4b. Upgrade card when additional devices are hidden
if (state.hiddenProfiledDeviceCount > 0) {
item(key = "device_limit_upgrade") {
DeviceLimitUpgradeCard(
hiddenCount = state.hiddenProfiledDeviceCount,
upgradeType = state.upgradeInfo.type,
onUpgrade = onUpgrade,
)
}
}
// 5. Monitoring active card
if (state.profiles.isNotEmpty() && state.devices.isEmpty()) {
item(key = "monitoring_active") {
@@ -135,6 +135,9 @@ class OverviewViewModel @Inject constructor(
) {
val isScanBlocked: Boolean get() = permissions.any { it.isScanBlocking }
val profiledDevices: List<PodDevice> get() = devices.filter { it.profileId != null }
val visibleProfiledDevices: List<PodDevice>
get() = if (upgradeInfo.isPro) profiledDevices else profiledDevices.take(FREE_DEVICE_LIMIT)
val hiddenProfiledDeviceCount: Int get() = profiledDevices.size - visibleProfiledDevices.size
val unmatchedDevices: List<PodDevice> get() = devices.filter { it.profileId == null }
}
@@ -187,6 +190,7 @@ class OverviewViewModel @Inject constructor(
}
companion object {
private const val FREE_DEVICE_LIMIT = 1
private val TAG = logTag("Overview", "VM")
}
}
@@ -0,0 +1,94 @@
package eu.darken.capod.main.ui.overview.cards
import androidx.compose.foundation.layout.Column
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.material.icons.Icons
import androidx.compose.material.icons.twotone.Stars
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.upgrade.UpgradeRepo
@Composable
fun DeviceLimitUpgradeCard(
hiddenCount: Int,
upgradeType: UpgradeRepo.Type,
onUpgrade: () -> Unit,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
) {
Column(
modifier = Modifier.padding(16.dp),
) {
Text(
text = pluralStringResource(R.plurals.overview_more_devices_upgrade, hiddenCount, hiddenCount),
style = MaterialTheme.typography.titleMedium,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(R.string.upgrade_capod_description),
style = MaterialTheme.typography.bodyMedium,
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onUpgrade,
modifier = Modifier.align(Alignment.End),
) {
Icon(
imageVector = Icons.TwoTone.Stars,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp),
)
Text(
text = stringResource(
when (upgradeType) {
UpgradeRepo.Type.FOSS -> R.string.general_donate_action
UpgradeRepo.Type.GPLAY -> R.string.general_upgrade_action
}
),
)
}
}
}
}
@Preview2
@Composable
private fun DeviceLimitUpgradeCardGplayPreview() = PreviewWrapper {
DeviceLimitUpgradeCard(
hiddenCount = 2,
upgradeType = UpgradeRepo.Type.GPLAY,
onUpgrade = {},
)
}
@Preview2
@Composable
private fun DeviceLimitUpgradeCardFossPreview() = PreviewWrapper {
DeviceLimitUpgradeCard(
hiddenCount = 1,
upgradeType = UpgradeRepo.Type.FOSS,
onUpgrade = {},
)
}
+4
View File
@@ -210,6 +210,10 @@
<item quantity="one">%d device without matching profile</item>
<item quantity="other">%d devices without matching profile</item>
</plurals>
<plurals name="overview_more_devices_upgrade">
<item quantity="one">%d more device</item>
<item quantity="other">%d more devices</item>
</plurals>
<string name="permission_bluetooth_connect_label">Bluetooth connect</string>
<string name="permission_bluetooth_connect_description">This app requires the \"Bluetooth connect\" permission to interact with paired devices and initiate connections.</string>