diff --git a/app/src/debug/java/eu/darken/capod/screenshots/ScreenshotContent.kt b/app/src/debug/java/eu/darken/capod/screenshots/ScreenshotContent.kt index 92e7242e..cf98f7d6 100644 --- a/app/src/debug/java/eu/darken/capod/screenshots/ScreenshotContent.kt +++ b/app/src/debug/java/eu/darken/capod/screenshots/ScreenshotContent.kt @@ -20,8 +20,6 @@ import eu.darken.capod.common.compose.preview.MockPodDataProvider import eu.darken.capod.common.theming.CapodTheme import eu.darken.capod.main.ui.overview.OverviewScreen import eu.darken.capod.main.ui.overview.OverviewViewModel -import eu.darken.capod.main.ui.settings.SettingsScreen -import eu.darken.capod.main.ui.settings.SettingsViewModel import eu.darken.capod.main.ui.widget.ComposeWidgetPreview import eu.darken.capod.main.ui.widget.WidgetConfigurationScreen import eu.darken.capod.main.ui.widget.WidgetConfigurationViewModel @@ -32,9 +30,8 @@ import eu.darken.capod.profiles.ui.DeviceManagerScreen import eu.darken.capod.profiles.ui.DeviceManagerViewModel import eu.darken.capod.profiles.ui.creation.DeviceProfileCreationScreen import eu.darken.capod.profiles.ui.creation.DeviceProfileCreationViewModel -import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition -import eu.darken.capod.reaction.ui.ReactionSettingsScreen -import eu.darken.capod.reaction.ui.ReactionSettingsViewModel +import eu.darken.capod.main.ui.devicesettings.DeviceSettingsScreen +import eu.darken.capod.main.ui.devicesettings.DeviceSettingsViewModel import eu.darken.capod.reaction.ui.popup.PopUpContent as PopUpCard internal const val DS = "spec:width=1080px,height=2400px,dpi=428" @@ -113,44 +110,15 @@ internal fun AddProfileContent() = PreviewWrapper { } @Composable -internal fun SettingsIndexContent() = PreviewWrapper { - SettingsScreen( - state = SettingsViewModel.State(isPro = true, sponsorUrl = "https://example.com"), - onNavigateUp = {}, - onGeneralSettings = {}, - onDeviceManager = {}, - onReactions = {}, - onSupport = {}, - onWiki = {}, - onChangelog = {}, - onHelpTranslate = {}, - onAcknowledgements = {}, - onPrivacyPolicy = {}, - onSponsor = {}, - ) -} - -@Composable -internal fun ReactionSettingsContent() = PreviewWrapper { - ReactionSettingsScreen( - state = ReactionSettingsViewModel.State( +internal fun DeviceSettingsReactionsContent() = PreviewWrapper { + DeviceSettingsScreen( + state = DeviceSettingsViewModel.State( + device = MockPodDataProvider.dualPodMonitoredWithReactions(), + now = MOCK_NOW, isPro = true, - onePodMode = false, - autoPlay = true, - autoPause = true, - autoConnect = false, - autoConnectCondition = AutoConnectCondition.WHEN_SEEN, - showPopUpOnCaseOpen = true, - showPopUpOnConnection = false, + isNudgeAvailable = true, ), onNavigateUp = {}, - onOnePodModeChanged = {}, - onAutoPlayChanged = {}, - onAutoPauseChanged = {}, - onAutoConnectChanged = {}, - onAutoConnectConditionSelected = {}, - onShowPopUpOnCaseOpenChanged = {}, - onShowPopUpOnConnectionChanged = {}, ) } @@ -260,14 +228,10 @@ private fun PreviewDeviceProfiles() = DeviceProfilesContent() @Composable private fun PreviewAddProfile() = AddProfileContent() -@Preview(name = "7 - Settings", locale = "en", device = DS, showSystemUi = true) +@Preview(name = "7 - Device Settings Reactions", locale = "en", device = DS, showSystemUi = true) @Composable -private fun PreviewSettingsIndex() = SettingsIndexContent() +private fun PreviewDeviceSettingsReactions() = DeviceSettingsReactionsContent() -@Preview(name = "8 - Reaction Settings", locale = "en", device = DS, showSystemUi = true) -@Composable -private fun PreviewReactionSettings() = ReactionSettingsContent() - -@Preview(name = "9 - Homescreen Widget", device = DS, showSystemUi = true) +@Preview(name = "8 - Homescreen Widget", device = DS, showSystemUi = true) @Composable private fun PreviewHomescreenWidget() = HomescreenWidgetContent() diff --git a/app/src/main/java/eu/darken/capod/common/compose/preview/MockPodDataProvider.kt b/app/src/main/java/eu/darken/capod/common/compose/preview/MockPodDataProvider.kt index ee8eb402..a3f48ac3 100644 --- a/app/src/main/java/eu/darken/capod/common/compose/preview/MockPodDataProvider.kt +++ b/app/src/main/java/eu/darken/capod/common/compose/preview/MockPodDataProvider.kt @@ -23,6 +23,7 @@ import eu.darken.capod.pods.core.apple.ble.protocol.ProximityPayload import eu.darken.capod.pods.core.unknown.UnknownSnapshotBle import eu.darken.capod.profiles.core.AppleDeviceProfile import eu.darken.capod.profiles.core.DeviceProfile +import eu.darken.capod.profiles.core.ReactionConfig import java.time.Instant /** Fixed timestamp for deterministic preview/screenshot rendering. */ @@ -64,6 +65,7 @@ object MockPodDataProvider { leftPodIcon = R.drawable.device_airpods_pro2_left, rightPodIcon = R.drawable.device_airpods_pro2_right, _caseIcon = R.drawable.device_airpods_pro2_case, + _address = "AA:BB:CC:DD:EE:FF", ) fun airPodsProLowBattery(): DualBlePodSnapshot = MockDualBlePodSnapshot( @@ -120,6 +122,7 @@ object MockPodDataProvider { _label = "AirPods Max", batteryHeadsetPercent = 0.85f, _isBeingWorn = true, + _address = "AA:BB:CC:DD:EE:FF", ) fun airPodsMaxCharging(): SingleBlePodSnapshot = MockSingleAppleBlePodSnapshot( @@ -206,6 +209,22 @@ object MockPodDataProvider { ), ) + /** Dual pods with a profile that has reaction toggles set — used for the Reactions screenshot. */ + fun dualPodMonitoredWithReactions(): PodDevice = PodDevice( + profileId = "preview-dual-reactions", + label = "My AirPods Pro", + ble = airPodsProWithKeys(), + aap = null, + reactions = ReactionConfig( + autoPause = true, + autoPlay = true, + onePodMode = false, + autoConnect = true, + showPopUpOnCaseOpen = true, + showPopUpOnConnection = false, + ), + ) + fun singlePodMonitored(): PodDevice = PodDevice( profileId = "preview-single", ble = airPodsMax(), @@ -379,6 +398,7 @@ private class MockSingleBlePodSnapshot( override val batteryHeadsetPercent: Float?, private val _isHeadsetBeingCharged: Boolean = false, private val _isBeingWorn: Boolean = false, + private val _address: String? = null, rssi: Int = -50, ) : SingleBlePodSnapshot, HasChargeDetection, HasEarDetection { override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id() @@ -391,7 +411,7 @@ private class MockSingleBlePodSnapshot( override val signalQuality: Float = 0.80f override val iconRes: Int = _model.iconRes override val meta: BlePodSnapshot.Meta = object : BlePodSnapshot.Meta { - override val profile: DeviceProfile = AppleDeviceProfile(label = _label, model = _model) + override val profile: DeviceProfile = AppleDeviceProfile(label = _label, model = _model, address = _address) } override fun getLabel(context: Context): String = _model.label diff --git a/app/src/main/java/eu/darken/capod/common/navigation/Nav.kt b/app/src/main/java/eu/darken/capod/common/navigation/Nav.kt index 6949a1a0..a9429a0f 100644 --- a/app/src/main/java/eu/darken/capod/common/navigation/Nav.kt +++ b/app/src/main/java/eu/darken/capod/common/navigation/Nav.kt @@ -23,7 +23,7 @@ object Nav { data object Upgrade : Main @Serializable - data class DeviceSettings(val address: String) : Main + data class DeviceSettings(val profileId: String) : Main @Serializable data object StemActionConfig : Main @@ -36,9 +36,6 @@ object Nav { @Serializable data object General : Settings - @Serializable - data object Reactions : Settings - @Serializable data object Debug : Settings diff --git a/app/src/main/java/eu/darken/capod/main/core/PermissionTool.kt b/app/src/main/java/eu/darken/capod/main/core/PermissionTool.kt index c60256c0..4e54186d 100644 --- a/app/src/main/java/eu/darken/capod/main/core/PermissionTool.kt +++ b/app/src/main/java/eu/darken/capod/main/core/PermissionTool.kt @@ -6,10 +6,12 @@ import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.permissions.Permission import eu.darken.capod.common.permissions.isRequired -import eu.darken.capod.reaction.core.ReactionSettings +import eu.darken.capod.profiles.core.toReactionConfig +import eu.darken.capod.profiles.core.DeviceProfilesRepo import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import java.util.UUID @@ -20,7 +22,7 @@ import javax.inject.Singleton class PermissionTool @Inject constructor( @ApplicationContext private val context: Context, private val generalSettings: GeneralSettings, - private val reactionSettings: ReactionSettings, + private val profilesRepo: DeviceProfilesRepo, ) { private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID()) @@ -29,12 +31,20 @@ class PermissionTool @Inject constructor( permissionCheckTrigger.value = UUID.randomUUID() } + private val anyPopupEnabled: Flow = profilesRepo.profiles + .map { profiles -> + profiles.any { profile -> + profile.toReactionConfig().let { it.showPopUpOnCaseOpen || it.showPopUpOnConnection } + } + } + .distinctUntilChanged() + val missingPermissions: Flow> = combine( permissionCheckTrigger, generalSettings.monitorMode.flow, - reactionSettings.showPopUpOnCaseOpen.flow + anyPopupEnabled, ) { _, monitorMode, showPopUp -> - Permission.values() + Permission.entries .filter { it != Permission.IGNORE_BATTERY_OPTIMIZATION || monitorMode == MonitorMode.ALWAYS } .filter { it != Permission.ACCESS_BACKGROUND_LOCATION || monitorMode == MonitorMode.ALWAYS } .filter { it != Permission.SYSTEM_ALERT_WINDOW || showPopUp } diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsNavigation.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsNavigation.kt index 166bae2e..d76cb169 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsNavigation.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsNavigation.kt @@ -14,7 +14,7 @@ import javax.inject.Inject class DeviceSettingsNavigation @Inject constructor() : NavigationEntry { override fun EntryProviderScope.setup() { entry { key -> - DeviceSettingsScreenHost(address = key.address) + DeviceSettingsScreenHost(profileId = key.profileId) } } diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt index 278f51db..9b1793ac 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt @@ -18,21 +18,29 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape 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.automirrored.twotone.VolumeUp +import androidx.compose.material.icons.twotone.BluetoothConnected import androidx.compose.material.icons.twotone.DevicesOther import androidx.compose.material.icons.twotone.Edit import androidx.compose.material.icons.twotone.GraphicEq import androidx.compose.material.icons.twotone.Headphones import androidx.compose.material.icons.twotone.Hearing +import androidx.compose.material.icons.twotone.LooksOne import androidx.compose.material.icons.twotone.Loop import androidx.compose.material.icons.twotone.Mic import androidx.compose.material.icons.twotone.Nightlight +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.Speed import androidx.compose.material.icons.twotone.Swipe import androidx.compose.material.icons.twotone.Timer import androidx.compose.material.icons.twotone.TouchApp import androidx.compose.material.icons.twotone.Visibility import androidx.compose.material.icons.twotone.VisibilityOff +import androidx.compose.material.icons.twotone.Workspaces +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard @@ -48,7 +56,9 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface +import androidx.compose.material3.Switch 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 @@ -86,17 +96,18 @@ import eu.darken.capod.pods.core.apple.aap.AapPodState import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceInfo import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting import eu.darken.capod.pods.core.apple.ble.devices.HasStateDetection +import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition import java.time.Duration @Composable fun DeviceSettingsScreenHost( - address: String, + profileId: String, vm: DeviceSettingsViewModel = hiltViewModel(), ) { ErrorEventHandler(vm) NavigationEventHandler(vm) - LaunchedEffect(address) { vm.initialize(address) } + LaunchedEffect(profileId) { vm.initialize(profileId) } val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } @@ -140,7 +151,6 @@ fun DeviceSettingsScreenHost( onVolumeSwipeLengthChange = { vm.setVolumeSwipeLength(it) }, onEndCallMuteMicChange = { muteMic, endCall -> vm.setEndCallMuteMic(muteMic, endCall) }, onMicrophoneModeChange = { vm.setMicrophoneMode(it) }, - onEarDetectionEnabledChange = { vm.setEarDetectionEnabled(it) }, onListeningModeCycleChange = { vm.setListeningModeCycle(it) }, onAllowOffOptionChange = { vm.setAllowOffOption(it) }, onSleepDetectionChange = { vm.setSleepDetection(it) }, @@ -148,6 +158,13 @@ fun DeviceSettingsScreenHost( onStemActionsClick = { vm.navToStemConfig() }, onForceConnect = { vm.forceConnect() }, onUpgrade = { vm.launchUpgrade() }, + onOnePodModeChange = { vm.setOnePodMode(it) }, + onAutoPlayChange = { vm.setAutoPlay(it) }, + onAutoPauseChange = { vm.setAutoPause(it) }, + onAutoConnectChange = { vm.setAutoConnect(it) }, + onAutoConnectConditionChange = { vm.setAutoConnectCondition(it) }, + onShowPopUpOnCaseOpenChange = { vm.setShowPopUpOnCaseOpen(it) }, + onShowPopUpOnConnectionChange = { vm.setShowPopUpOnConnection(it) }, ) } @@ -169,7 +186,6 @@ fun DeviceSettingsScreen( onVolumeSwipeLengthChange: (AapSetting.VolumeSwipeLength.Value) -> Unit = {}, onEndCallMuteMicChange: (AapSetting.EndCallMuteMic.MuteMicMode, AapSetting.EndCallMuteMic.EndCallMode) -> Unit = { _, _ -> }, onMicrophoneModeChange: (AapSetting.MicrophoneMode.Mode) -> Unit = {}, - onEarDetectionEnabledChange: (Boolean) -> Unit = {}, onListeningModeCycleChange: (Int) -> Unit = {}, onAllowOffOptionChange: (Boolean) -> Unit = {}, onSleepDetectionChange: (Boolean) -> Unit = {}, @@ -177,11 +193,21 @@ fun DeviceSettingsScreen( onStemActionsClick: () -> Unit = {}, onForceConnect: () -> Unit = {}, onUpgrade: () -> Unit = {}, + onOnePodModeChange: (Boolean) -> Unit = {}, + onAutoPlayChange: (Boolean) -> Unit = {}, + onAutoPauseChange: (Boolean) -> Unit = {}, + onAutoConnectChange: (Boolean) -> Unit = {}, + onAutoConnectConditionChange: (AutoConnectCondition) -> Unit = {}, + onShowPopUpOnCaseOpenChange: (Boolean) -> Unit = {}, + onShowPopUpOnConnectionChange: (Boolean) -> Unit = {}, ) { val device = state.device val features = device?.model?.features val enabled = device?.isAapReady == true val isPro = state.isPro + val reactions = state.reactions + + var showAutoConnectConditionDialog by remember { mutableStateOf(false) } Scaffold( topBar = { @@ -254,6 +280,105 @@ fun DeviceSettingsScreen( } } + // ── Reactions (per-profile, not gated on AAP) ───────────────── + if (device != null && features != null) { + item("reactions_header") { + SettingsCategoryHeader(text = stringResource(R.string.settings_reaction_label)) + } + + if (features.hasEarDetection) { + item("reaction_auto_play") { + SettingsSwitchItem( + icon = Icons.TwoTone.PlayCircle, + title = stringResource(R.string.settings_autopplay_label), + subtitle = stringResource(R.string.settings_autoplay_description), + checked = reactions.autoPlay, + onCheckedChange = onAutoPlayChange, + requiresUpgrade = !isPro, + ) + } + item("reaction_auto_pause") { + SettingsSwitchItem( + icon = Icons.TwoTone.PauseCircle, + title = stringResource(R.string.settings_autopause_label), + subtitle = stringResource(R.string.settings_autopause_description), + checked = reactions.autoPause, + onCheckedChange = onAutoPauseChange, + requiresUpgrade = !isPro, + ) + } + if (features.hasDualPods) { + val earDetectionActive = reactions.autoPlay || reactions.autoPause + item("reaction_one_pod_mode") { + SettingsBaseItem( + title = stringResource(R.string.settings_onepod_mode_label), + subtitle = stringResource(R.string.settings_onepod_mode_description), + icon = Icons.TwoTone.LooksOne, + onClick = { if (earDetectionActive) onOnePodModeChange(!reactions.onePodMode) }, + enabled = earDetectionActive, + trailingContent = { + Switch( + checked = reactions.onePodMode, + onCheckedChange = onOnePodModeChange, + enabled = earDetectionActive, + modifier = Modifier.padding(start = 16.dp), + ) + }, + ) + } + } + item("reaction_ear_detection_info") { + SettingsBaseItem( + title = stringResource(R.string.settings_eardetection_info_label), + subtitle = stringResource(R.string.settings_eardetection_info_description), + icon = Icons.TwoTone.QuestionMark, + onClick = {}, + enabled = false, + ) + } + } + item("reaction_auto_connect") { + SettingsSwitchItem( + icon = Icons.TwoTone.BluetoothConnected, + title = stringResource(R.string.settings_autoconnect_label), + subtitle = stringResource(R.string.settings_autoconnect_description), + checked = reactions.autoConnect, + onCheckedChange = onAutoConnectChange, + ) + } + item("reaction_auto_connect_condition") { + SettingsBaseItem( + title = stringResource(R.string.settings_autoconnect_condition_label), + subtitle = stringResource(reactions.autoConnectCondition.labelRes), + icon = Icons.TwoTone.Workspaces, + onClick = { if (reactions.autoConnect) showAutoConnectConditionDialog = true }, + enabled = reactions.autoConnect, + ) + } + if (features.hasCase) { + item("reaction_popup_caseopen") { + SettingsSwitchItem( + icon = Icons.AutoMirrored.TwoTone.Message, + title = stringResource(R.string.settings_popup_caseopen_label), + subtitle = stringResource(R.string.settings_popup_caseopen_description), + checked = reactions.showPopUpOnCaseOpen, + onCheckedChange = onShowPopUpOnCaseOpenChange, + requiresUpgrade = !isPro, + ) + } + } + item("reaction_popup_connection") { + SettingsSwitchItem( + icon = Icons.AutoMirrored.TwoTone.Message, + title = stringResource(R.string.settings_popup_connected_label), + subtitle = stringResource(R.string.settings_popup_connected_description), + checked = reactions.showPopUpOnConnection, + onCheckedChange = onShowPopUpOnConnectionChange, + requiresUpgrade = !isPro, + ) + } + } + // Settings — only show when AAP is connected if (features != null && device.isAapConnected) { @@ -490,21 +615,6 @@ fun DeviceSettingsScreen( SettingsCategoryHeader(text = stringResource(R.string.device_settings_category_general_label)) } - if (features.hasEarDetectionToggle) { - val earDetection = device.earDetectionEnabled - ?: AapSetting.EarDetectionEnabled(enabled = true) - item("ear_detection_toggle") { - SettingsSwitchItem( - icon = Icons.TwoTone.Hearing, - title = stringResource(R.string.device_settings_ear_detection_label), - subtitle = stringResource(R.string.device_settings_ear_detection_description), - checked = earDetection.enabled, - onCheckedChange = onEarDetectionEnabledChange, - enabled = enabled, - ) - } - } - if (features.hasSleepDetection) { val sleepDet = device.sleepDetection ?: AapSetting.SleepDetection(enabled = true) @@ -554,9 +664,73 @@ fun DeviceSettingsScreen( Spacer(modifier = Modifier.height(16.dp)) } } + + if (showAutoConnectConditionDialog && device != null) { + AutoConnectConditionDialog( + current = reactions.autoConnectCondition, + hasEarDetection = features?.hasEarDetection == true, + hasCase = features?.hasCase == true, + onSelect = { + onAutoConnectConditionChange(it) + showAutoConnectConditionDialog = false + }, + onDismiss = { showAutoConnectConditionDialog = false }, + ) + } } } +@Composable +private fun AutoConnectConditionDialog( + current: AutoConnectCondition, + hasEarDetection: Boolean, + hasCase: Boolean, + onSelect: (AutoConnectCondition) -> Unit, + onDismiss: () -> Unit, +) { + val options = AutoConnectCondition.entries.filter { condition -> + when (condition) { + AutoConnectCondition.IN_EAR -> hasEarDetection + AutoConnectCondition.CASE_OPEN -> hasCase + AutoConnectCondition.WHEN_SEEN -> true + } + } + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(text = stringResource(R.string.settings_autoconnect_condition_label)) }, + text = { + Column(Modifier.selectableGroup()) { + options.forEach { condition -> + val isSelected = condition == current + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + onClick = { onSelect(condition) }, + role = Role.RadioButton, + ) + .padding(vertical = 12.dp, horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton(selected = isSelected, onClick = null) + Text( + text = stringResource(condition.labelRes), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(start = 16.dp), + ) + } + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(text = stringResource(android.R.string.cancel)) + } + }, + ) +} + @Composable private fun DeviceInfoCard( deviceInfo: AapDeviceInfo?, diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt index eafb4171..9677eb7c 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt @@ -1,14 +1,16 @@ package eu.darken.capod.main.ui.devicesettings import dagger.hilt.android.lifecycle.HiltViewModel -import eu.darken.capod.common.bluetooth.BluetoothAddress import eu.darken.capod.common.bluetooth.BluetoothManager2 import eu.darken.capod.common.coroutine.DispatcherProvider +import eu.darken.capod.common.datastore.value import eu.darken.capod.common.debug.logging.Logging.Priority.WARN 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.main.core.GeneralSettings +import eu.darken.capod.main.core.MonitorMode import eu.darken.capod.monitor.core.DeviceMonitor import eu.darken.capod.monitor.core.PodDevice import eu.darken.capod.common.navigation.Nav @@ -17,12 +19,18 @@ import eu.darken.capod.common.upgrade.isPro import eu.darken.capod.pods.core.apple.aap.AapConnectionManager import eu.darken.capod.pods.core.apple.aap.protocol.AapCommand import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting +import eu.darken.capod.profiles.core.AppleDeviceProfile +import eu.darken.capod.profiles.core.DeviceProfilesRepo +import eu.darken.capod.profiles.core.ReactionConfig +import eu.darken.capod.profiles.core.ProfileId +import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.isActive import java.time.Instant @@ -35,15 +43,17 @@ class DeviceSettingsViewModel @Inject constructor( private val aapManager: AapConnectionManager, private val upgradeRepo: UpgradeRepo, private val bluetoothManager: BluetoothManager2, + private val profilesRepo: DeviceProfilesRepo, + private val generalSettings: GeneralSettings, ) : ViewModel4(dispatcherProvider) { - private val targetAddress = MutableStateFlow(null) + private val targetProfileId = MutableStateFlow(null) private var initialized = false - fun initialize(address: BluetoothAddress) { - if (initialized && targetAddress.value == address) return + fun initialize(profileId: ProfileId) { + if (initialized && targetProfileId.value == profileId) return initialized = true - targetAddress.value = address + targetProfileId.value = profileId } private val updateTicker = channelFlow { @@ -63,16 +73,16 @@ class DeviceSettingsViewModel @Inject constructor( val events = SingleEventFlow() - val state = targetAddress.flatMapLatest { address -> - if (address == null) return@flatMapLatest flowOf(State(device = null)) + val state = targetProfileId.flatMapLatest { profileId -> + if (profileId == null) return@flatMapLatest flowOf(State(device = null)) combine( updateTicker, - deviceMonitor.devices, + deviceForProfile(profileId), upgradeRepo.upgradeInfo, isForceConnecting, - ) { _, devices, upgrade, forcing -> + ) { _, device, upgrade, forcing -> State( - device = devices.firstOrNull { it.address == address }, + device = device, now = Instant.now(), isPro = upgrade.isPro, isNudgeAvailable = bluetoothManager.isNudgeAvailable, @@ -81,13 +91,31 @@ class DeviceSettingsViewModel @Inject constructor( } }.asLiveState() + /** + * Returns the [PodDevice] for [profileId] reactively. Falls back to a synthesized bare + * device when the profile exists but isn't currently visible (no BLE / cache / AAP), + * so the Reactions section can still be edited for pristine profiles. + */ + private fun deviceForProfile(profileId: ProfileId): kotlinx.coroutines.flow.Flow = + deviceMonitor.devices.flatMapLatest { devices -> + val live = devices.firstOrNull { it.profileId == profileId } + flow { emit(live ?: deviceMonitor.getDeviceForProfile(profileId)) } + } + data class State( val device: PodDevice?, val now: Instant = Instant.now(), val isPro: Boolean = false, val isNudgeAvailable: Boolean = true, val isForceConnecting: Boolean = false, - ) + ) { + val reactions: ReactionConfig get() = device?.reactions ?: ReactionConfig() + } + + private suspend fun currentAddress(): String? { + val profileId = targetProfileId.value ?: return null + return deviceMonitor.getDeviceForProfile(profileId)?.address + } fun forceConnect() = launch { if (!isForceConnecting.compareAndSet(expect = false, update = true)) { @@ -95,7 +123,7 @@ class DeviceSettingsViewModel @Inject constructor( return@launch } try { - val address = targetAddress.value ?: run { + val address = currentAddress() ?: run { events.tryEmit(Event.OpenBluetoothSettings) return@launch } @@ -124,23 +152,18 @@ class DeviceSettingsViewModel @Inject constructor( if (!accepted) { events.tryEmit(Event.OpenBluetoothSettings) } - // On accepted=true, AapAutoConnect.initialConnect() will pick up the new - // connectedDevices entry and trigger the AAP handshake. The card disappears - // when device.isAapConnected becomes true. } finally { isForceConnecting.value = false } } private suspend fun sendInternal(command: AapCommand) { - val address = targetAddress.value ?: return + val address = currentAddress() ?: return try { aapManager.sendCommand(address, command) log(TAG) { "Sent $command to $address" } } catch (e: Exception) { log(TAG, WARN) { "Failed to send $command: ${e.message}" } - // SingleEventFlow is backed by a BUFFERED Channel — use the suspending emit to avoid - // dropping the event under momentary backpressure. events.emit(Event.SendFailed(command, e.message)) } } @@ -193,12 +216,15 @@ class DeviceSettingsViewModel @Inject constructor( fun setSleepDetection(enabled: Boolean) = sendProGated(AapCommand.SetSleepDetection(enabled)) fun setDeviceName(name: String) = launch { - val address = targetAddress.value ?: return@launch - sendInternal(AapCommand.SetDeviceName(name)) + val address = currentAddress() ?: return@launch + try { + aapManager.sendCommand(address, AapCommand.SetDeviceName(name)) + log(TAG) { "Sent SetDeviceName to $address" } + } catch (e: Exception) { + log(TAG, WARN) { "Failed to send SetDeviceName: ${e.message}" } + events.emit(Event.SendFailed(AapCommand.SetDeviceName(name), e.message)) + } - // Also try to update the Android-local bond alias so the new name shows in Android's - // Bluetooth settings too. The AAP rename only changes what the AirPods themselves report; - // Android's system display reads from the bond database and needs a separate update. val bonded = try { bluetoothManager.bondedDevices().first().firstOrNull { it.address == address } } catch (e: Exception) { @@ -212,6 +238,59 @@ class DeviceSettingsViewModel @Inject constructor( } } + // ── Reaction toggles (per-profile) ─────────────────────────────────────── + + private suspend fun updateProfileNow(transform: (AppleDeviceProfile) -> AppleDeviceProfile) { + val profileId = targetProfileId.value ?: return + try { + profilesRepo.updateAppleProfile(profileId, transform) + } catch (e: Exception) { + log(TAG, WARN) { "Failed to update profile $profileId: ${e.message}" } + errorEvents.emit(e) + } + } + + private fun proGatedReaction( + enabled: Boolean, + transform: (AppleDeviceProfile) -> AppleDeviceProfile, + ) = launch { + // Disabling never requires pro; enabling does. + if (enabled && !upgradeRepo.isPro()) { + navTo(Nav.Main.Upgrade) + return@launch + } + updateProfileNow(transform) + } + + fun setOnePodMode(enabled: Boolean) = launch { updateProfileNow { it.copy(onePodMode = enabled) } } + + fun setAutoPlay(enabled: Boolean) = proGatedReaction(enabled) { it.copy(autoPlay = enabled) } + + fun setAutoPause(enabled: Boolean) = proGatedReaction(enabled) { it.copy(autoPause = enabled) } + + fun setAutoConnect(enabled: Boolean) = launch { + updateProfileNow { it.copy(autoConnect = enabled) } + if (enabled) { + // Auto-connect requires ALWAYS mode to react to BLE/case/ear events when + // nothing is connected. We intentionally do NOT revert on disable — the user + // may have set ALWAYS manually, or another profile may still need it. + if (generalSettings.monitorMode.value() != MonitorMode.ALWAYS) { + log(TAG) { "Forcing monitorMode to ALWAYS because autoConnect was enabled" } + generalSettings.monitorMode.value(MonitorMode.ALWAYS) + } + } + } + + fun setAutoConnectCondition(condition: AutoConnectCondition) = launch { + updateProfileNow { it.copy(autoConnectCondition = condition) } + } + + fun setShowPopUpOnCaseOpen(enabled: Boolean) = + proGatedReaction(enabled) { it.copy(showPopUpOnCaseOpen = enabled) } + + fun setShowPopUpOnConnection(enabled: Boolean) = + proGatedReaction(enabled) { it.copy(showPopUpOnConnection = enabled) } + fun navToStemConfig() = launch { if (upgradeRepo.isPro()) { navTo(Nav.Main.StemActionConfig) diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt index f558943e..18086ecd 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt @@ -154,8 +154,8 @@ class OverviewViewModel @Inject constructor( } fun goToDeviceSettings(device: PodDevice) { - val address = device.address ?: return - navTo(Nav.Main.DeviceSettings(address)) + val profileId = device.profileId ?: return + navTo(Nav.Main.DeviceSettings(profileId)) } fun goToEditProfile(device: PodDevice) { diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualPodsCard.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualPodsCard.kt index ddcaad8f..bea0c443 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualPodsCard.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualPodsCard.kt @@ -141,7 +141,7 @@ fun DualPodsCard( } } - if (device.address != null && onDeviceSettings != null) { + if (device.profileId != null && onDeviceSettings != null) { IconButton(onClick = onDeviceSettings) { Icon( imageVector = Icons.TwoTone.Tune, @@ -149,7 +149,8 @@ fun DualPodsCard( tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } - } else if (device.profileId != null && device.address == null && onEditProfile != null) { + } + if (device.profileId != null && device.address == null && onEditProfile != null) { IconButton(onClick = onEditProfile) { Icon( imageVector = Icons.TwoTone.Warning, diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/SinglePodsCard.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/SinglePodsCard.kt index 3e61c140..e6fac054 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/SinglePodsCard.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/SinglePodsCard.kt @@ -140,7 +140,7 @@ fun SinglePodsCard( } } - if (device.address != null && onDeviceSettings != null) { + if (device.profileId != null && onDeviceSettings != null) { IconButton(onClick = onDeviceSettings) { Icon( imageVector = Icons.TwoTone.Tune, @@ -148,7 +148,8 @@ fun SinglePodsCard( tint = MaterialTheme.colorScheme.onSurfaceVariant, ) } - } else if (device.profileId != null && device.address == null && onEditProfile != null) { + } + if (device.profileId != null && device.address == null && onEditProfile != null) { IconButton(onClick = onEditProfile) { Icon( imageVector = Icons.TwoTone.Warning, diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsScreen.kt index a87d2acb..198f065a 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.material.icons.twotone.Favorite import androidx.compose.material.icons.twotone.Settings import androidx.compose.material.icons.twotone.SupportAgent import androidx.compose.material.icons.twotone.Translate -import androidx.compose.material.icons.twotone.Widgets import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold @@ -47,7 +46,6 @@ fun SettingsScreenHost(vm: SettingsViewModel = hiltViewModel()) { onNavigateUp = { vm.navUp() }, onGeneralSettings = { vm.navTo(Nav.Settings.General) }, onDeviceManager = { vm.navTo(Nav.Main.DeviceManager) }, - onReactions = { vm.navTo(Nav.Settings.Reactions) }, onSupport = { vm.navTo(Nav.Settings.Support) }, onWiki = { vm.openUrl("https://github.com/d4rken-org/capod/wiki") }, onChangelog = { vm.openUrl("https://capod.darken.eu/changelog") }, @@ -65,7 +63,6 @@ fun SettingsScreen( onNavigateUp: () -> Unit, onGeneralSettings: () -> Unit, onDeviceManager: () -> Unit, - onReactions: () -> Unit, onSupport: () -> Unit, onWiki: () -> Unit, onChangelog: () -> Unit, @@ -121,14 +118,6 @@ fun SettingsScreen( onClick = onDeviceManager, ) } - item { - SettingsBaseItem( - title = stringResource(R.string.settings_reaction_label), - subtitle = stringResource(R.string.settings_reaction_description), - icon = Icons.TwoTone.Widgets, - onClick = onReactions, - ) - } item { SettingsCategoryHeader(text = stringResource(R.string.settings_category_other_label)) } @@ -192,7 +181,6 @@ private fun SettingsScreenPreview() = PreviewWrapper { onNavigateUp = {}, onGeneralSettings = {}, onDeviceManager = {}, - onReactions = {}, onSupport = {}, onWiki = {}, onChangelog = {}, diff --git a/app/src/main/java/eu/darken/capod/monitor/core/DeviceMonitor.kt b/app/src/main/java/eu/darken/capod/monitor/core/DeviceMonitor.kt index 71691fd9..dcdfee24 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/DeviceMonitor.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/DeviceMonitor.kt @@ -15,6 +15,7 @@ import eu.darken.capod.pods.core.apple.aap.AapConnectionManager import eu.darken.capod.pods.core.apple.ble.devices.ApplePods import eu.darken.capod.pods.core.apple.aap.AapPodState import eu.darken.capod.profiles.core.AppleDeviceProfile +import eu.darken.capod.profiles.core.toReactionConfig import eu.darken.capod.profiles.core.DeviceProfile import eu.darken.capod.profiles.core.DeviceProfilesRepo import kotlinx.coroutines.CoroutineScope @@ -64,6 +65,7 @@ class DeviceMonitor @Inject constructor( profileAddress = profile?.address, profileModel = profile?.model, profileKeyState = profile.toBleKeyState(), + reactions = profile.toReactionConfig(), ) } @@ -114,6 +116,7 @@ class DeviceMonitor @Inject constructor( profileAddress = profile.address, profileModel = profile.model, profileKeyState = profile.toBleKeyState(), + reactions = profile.toReactionConfig(), ) } @@ -167,24 +170,26 @@ class DeviceMonitor @Inject constructor( } val profile = profilesRepo.profiles.firstOrNull()?.firstOrNull { it.id == profileId } - val cached = deviceStateCache.load(profileId) - val aap = profile?.let { aapManager.allStates.value.forProfile(it) } - - if (cached == null && aap == null) { - log(TAG) { "No device found for profile $profileId" } + if (profile == null) { + log(TAG) { "No profile with id $profileId" } return null } + val cached = deviceStateCache.load(profileId) + val aap = aapManager.allStates.value.forProfile(profile) - log(TAG) { "Found fallback device for profile $profileId (cached=${cached != null}, aap=${aap != null})" } + log(TAG) { + "Synthesizing device for profile $profileId (cached=${cached != null}, aap=${aap != null})" + } return PodDevice( profileId = profileId, - label = profile?.label, + label = profile.label, ble = null, aap = aap, cached = cached, - profileAddress = profile?.address, - profileModel = profile?.model, + profileAddress = profile.address, + profileModel = profile.model, profileKeyState = profile.toBleKeyState(), + reactions = profile.toReactionConfig(), ) } diff --git a/app/src/main/java/eu/darken/capod/monitor/core/PodDevice.kt b/app/src/main/java/eu/darken/capod/monitor/core/PodDevice.kt index cf4fad99..fdf3b2df 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/PodDevice.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/PodDevice.kt @@ -20,6 +20,7 @@ import eu.darken.capod.pods.core.apple.ble.devices.HasChargeDetectionDual import eu.darken.capod.pods.core.apple.ble.devices.HasDualMicrophone import eu.darken.capod.pods.core.apple.ble.devices.HasEarDetection import eu.darken.capod.pods.core.apple.ble.devices.HasEarDetectionDual +import eu.darken.capod.profiles.core.ReactionConfig import java.time.Duration import java.time.Instant @@ -45,6 +46,8 @@ data class PodDevice( * every time the BLE scanner misses the next advertisement batch. */ internal val profileKeyState: BleKeyState = BleKeyState.NONE, + /** Reaction toggle snapshot from the profile. Defaults to all-off when no profile is matched. */ + val reactions: ReactionConfig = ReactionConfig(), ) { val model: PodModel get() = ble?.model ?: profileModel ?: cached?.model ?: PodModel.UNKNOWN /** Bonded BR/EDR address (from profile). Used for AAP commands. */ diff --git a/app/src/main/java/eu/darken/capod/profiles/core/AppleDeviceProfile.kt b/app/src/main/java/eu/darken/capod/profiles/core/AppleDeviceProfile.kt index 13cdf938..fce63a56 100644 --- a/app/src/main/java/eu/darken/capod/profiles/core/AppleDeviceProfile.kt +++ b/app/src/main/java/eu/darken/capod/profiles/core/AppleDeviceProfile.kt @@ -4,6 +4,7 @@ import eu.darken.capod.common.serialization.ByteArrayBase64Serializer import eu.darken.capod.pods.core.apple.PodModel import eu.darken.capod.pods.core.apple.ble.protocol.IdentityResolvingKey import eu.darken.capod.pods.core.apple.ble.protocol.ProximityEncryptionKey +import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition import kotlinx.parcelize.Parcelize import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -21,4 +22,23 @@ data class AppleDeviceProfile( @SerialName("identityKey") @Serializable(with = ByteArrayBase64Serializer::class) val identityKey: IdentityResolvingKey? = null, @SerialName("encryptionKey") @Serializable(with = ByteArrayBase64Serializer::class) val encryptionKey: ProximityEncryptionKey? = null, @SerialName("address") override val address: String? = null, -) : DeviceProfile \ No newline at end of file + @SerialName("reactionAutoPause") val autoPause: Boolean = false, + @SerialName("reactionAutoPlay") val autoPlay: Boolean = false, + @SerialName("reactionOnePodMode") val onePodMode: Boolean = false, + @SerialName("reactionAutoConnect") val autoConnect: Boolean = false, + @SerialName("reactionAutoConnectCondition") val autoConnectCondition: AutoConnectCondition = AutoConnectCondition.WHEN_SEEN, + @SerialName("reactionShowPopUpOnCaseOpen") val showPopUpOnCaseOpen: Boolean = false, + @SerialName("reactionShowPopUpOnConnection") val showPopUpOnConnection: Boolean = false, +) : DeviceProfile, HasReactionConfig { + + override val reactionConfig: ReactionConfig + get() = ReactionConfig( + autoPause = autoPause, + autoPlay = autoPlay, + onePodMode = onePodMode, + autoConnect = autoConnect, + autoConnectCondition = autoConnectCondition, + showPopUpOnCaseOpen = showPopUpOnCaseOpen, + showPopUpOnConnection = showPopUpOnConnection, + ) +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/profiles/core/DeviceProfilesRepo.kt b/app/src/main/java/eu/darken/capod/profiles/core/DeviceProfilesRepo.kt index acea2b96..96e980ea 100644 --- a/app/src/main/java/eu/darken/capod/profiles/core/DeviceProfilesRepo.kt +++ b/app/src/main/java/eu/darken/capod/profiles/core/DeviceProfilesRepo.kt @@ -6,10 +6,14 @@ import eu.darken.capod.R import eu.darken.capod.common.coroutine.AppScope import eu.darken.capod.common.datastore.valueBlocking import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE +import eu.darken.capod.common.debug.logging.Logging.Priority.WARN import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.logTag +import eu.darken.capod.common.serialization.SerializationCapod import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.monitor.core.cache.DeviceStateCache +import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition +import kotlinx.serialization.json.Json import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -26,37 +30,80 @@ class DeviceProfilesRepo @Inject constructor( private val generalSettings: GeneralSettings, private val settings: DeviceProfilesSettings, private val deviceStateCache: DeviceStateCache, + @SerializationCapod private val json: Json, ) { private val mutex = Mutex() init { scope.launch { - if (settings.defaultProfileCreated.valueBlocking) return@launch - - - log(TAG) { "Creating default profile" } - var defaultProfile = AppleDeviceProfile( - label = context.getString(R.string.profiles_name_default), - ) - if (!settings.singleToMultiMigrationDone.valueBlocking) { - log(TAG) { "Migrating settings default profile" } - defaultProfile = defaultProfile.copy( - minimumSignalQuality = generalSettings.oldMinimumSignalQuality.valueBlocking, - model = generalSettings.oldMainDeviceModel.valueBlocking, - address = generalSettings.oldMainDeviceAddress.valueBlocking, - identityKey = generalSettings.oldMainDeviceIdentityKey.valueBlocking, - encryptionKey = generalSettings.oldMainDeviceEncryptionKey.valueBlocking, + if (!settings.defaultProfileCreated.valueBlocking) { + log(TAG) { "Creating default profile" } + var defaultProfile = AppleDeviceProfile( + label = context.getString(R.string.profiles_name_default), ) - settings.singleToMultiMigrationDone.valueBlocking = true + if (!settings.singleToMultiMigrationDone.valueBlocking) { + log(TAG) { "Migrating settings default profile" } + defaultProfile = defaultProfile.copy( + minimumSignalQuality = generalSettings.oldMinimumSignalQuality.valueBlocking, + model = generalSettings.oldMainDeviceModel.valueBlocking, + address = generalSettings.oldMainDeviceAddress.valueBlocking, + identityKey = generalSettings.oldMainDeviceIdentityKey.valueBlocking, + encryptionKey = generalSettings.oldMainDeviceEncryptionKey.valueBlocking, + ) + settings.singleToMultiMigrationDone.valueBlocking = true + } + + log(TAG) { "Default profile: $defaultProfile" } + addProfile(defaultProfile) + settings.defaultProfileCreated.valueBlocking = true } - log(TAG) { "Default profile: $defaultProfile" } - addProfile(defaultProfile) - settings.defaultProfileCreated.valueBlocking = true + // Reaction migration runs INDEPENDENTLY of defaultProfileCreated. Existing + // installs already have defaultProfileCreated=true, so nesting this inside the + // block above would silently swallow the migration on upgrade. + if (!settings.reactionMigrationDone.valueBlocking) { + migrateLegacyReactions() + } } } + private suspend fun migrateLegacyReactions() { + log(TAG) { "Migrating legacy global reaction settings onto profiles" } + mutex.withLock { + val legacy = try { + LegacyReactionSettingsReader(context, json).read() + } catch (e: Exception) { + log(TAG, WARN) { "Failed to read legacy reactions, defaulting all profiles: ${e.message}" } + null + } + val current = settings.profiles.valueBlocking.profiles + val migrated = current.map { p -> + if (p !is AppleDeviceProfile || legacy == null) return@map p + val features = p.model.features + val coercedCondition = when { + legacy.autoConnectCondition == AutoConnectCondition.IN_EAR && !features.hasEarDetection -> + AutoConnectCondition.WHEN_SEEN + legacy.autoConnectCondition == AutoConnectCondition.CASE_OPEN && !features.hasCase -> + AutoConnectCondition.WHEN_SEEN + else -> legacy.autoConnectCondition + } + p.copy( + autoPause = legacy.autoPause, + autoPlay = legacy.autoPlay, + onePodMode = legacy.onePodMode && features.hasDualPods && features.hasEarDetection, + autoConnect = legacy.autoConnect, + autoConnectCondition = coercedCondition, + showPopUpOnCaseOpen = legacy.showPopUpOnCaseOpen, + showPopUpOnConnection = legacy.showPopUpOnConnection, + ) + } + settings.profiles.valueBlocking = DeviceProfilesContainer(migrated) + settings.reactionMigrationDone.valueBlocking = true + } + log(TAG) { "Legacy reaction migration complete" } + } + val profiles: Flow> = settings.profiles.flow.map { it.profiles } suspend fun addProfile(profile: DeviceProfile, addFirst: Boolean = false) = mutex.withLock { @@ -80,6 +127,30 @@ class DeviceProfilesRepo @Inject constructor( log(VERBOSE) { "Updated device profile: ${profile.label}" } } + /** + * Read-modify-write helper that preserves every field the caller doesn't touch. + * Prevents callers that only know a subset of fields (e.g. the profile editor holding + * name/model/keys/address/signalQuality) from silently wiping fields the user set + * elsewhere — notably the reaction toggles edited from Device Settings. + */ + suspend fun updateAppleProfile( + id: ProfileId, + transform: (AppleDeviceProfile) -> AppleDeviceProfile, + ) = mutex.withLock { + val currentContainer = settings.profiles.valueBlocking + val existing = currentContainer.profiles.firstOrNull { it.id == id } as? AppleDeviceProfile + if (existing == null) { + log(TAG, WARN) { "updateAppleProfile($id): profile not found or not AppleDeviceProfile" } + return@withLock + } + val updated = transform(existing) + val otherProfiles = currentContainer.profiles.filter { it.id != id } + checkAddressUniqueness(updated, otherProfiles) + val updatedProfiles = currentContainer.profiles.map { if (it.id == id) updated else it } + settings.profiles.valueBlocking = DeviceProfilesContainer(updatedProfiles) + log(VERBOSE) { "Updated apple device profile: ${updated.label}" } + } + suspend fun removeProfile(profileId: ProfileId) = mutex.withLock { val currentContainer = settings.profiles.valueBlocking val updatedProfiles = currentContainer.profiles.filter { it.id != profileId } diff --git a/app/src/main/java/eu/darken/capod/profiles/core/DeviceProfilesSettings.kt b/app/src/main/java/eu/darken/capod/profiles/core/DeviceProfilesSettings.kt index 7d946782..6b6c0146 100644 --- a/app/src/main/java/eu/darken/capod/profiles/core/DeviceProfilesSettings.kt +++ b/app/src/main/java/eu/darken/capod/profiles/core/DeviceProfilesSettings.kt @@ -33,5 +33,6 @@ class DeviceProfilesSettings @Inject constructor( ) val singleToMultiMigrationDone = dataStore.createValue("profiles.migration.v2.done", false) val defaultProfileCreated = dataStore.createValue("profiles.default.v2.created", false) + val reactionMigrationDone = dataStore.createValue("profiles.reactions.migration.done", false) } diff --git a/app/src/main/java/eu/darken/capod/profiles/core/LegacyReactionSettingsReader.kt b/app/src/main/java/eu/darken/capod/profiles/core/LegacyReactionSettingsReader.kt new file mode 100644 index 00000000..f620835a --- /dev/null +++ b/app/src/main/java/eu/darken/capod/profiles/core/LegacyReactionSettingsReader.kt @@ -0,0 +1,75 @@ +package eu.darken.capod.profiles.core + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import eu.darken.capod.common.debug.logging.Logging.Priority.WARN +import eu.darken.capod.common.debug.logging.log +import eu.darken.capod.common.debug.logging.logTag +import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition +import kotlinx.coroutines.flow.first +import kotlinx.serialization.json.Json + +/** + * One-shot reader for the legacy global `settings_reaction` DataStore. Used by the reaction + * migration in [DeviceProfilesRepo] to copy the old global values onto every profile. Does + * not depend on the old `ReactionSettings` class (deleted in the same change), so the + * migration can still run after that class is gone. + */ +class LegacyReactionSettingsReader(context: Context, private val json: Json) { + + private val Context.legacyStore: DataStore by preferencesDataStore( + name = LEGACY_STORE_NAME, + ) + + private val dataStore: DataStore = context.applicationContext.legacyStore + + suspend fun read(): LegacyReactionValues { + val prefs = dataStore.data.first() + val conditionRaw = prefs[CONDITION_KEY] + val condition = conditionRaw?.let { + try { + json.decodeFromString(it) + } catch (e: Exception) { + log(TAG, WARN) { "Failed to decode legacy auto-connect condition: ${e.message}" } + null + } + } ?: AutoConnectCondition.WHEN_SEEN + + return LegacyReactionValues( + autoPause = prefs[AUTO_PAUSE_KEY] ?: false, + autoPlay = prefs[AUTO_PLAY_KEY] ?: false, + autoConnect = prefs[AUTO_CONNECT_KEY] ?: false, + autoConnectCondition = condition, + showPopUpOnCaseOpen = prefs[POPUP_CASE_OPEN_KEY] ?: false, + showPopUpOnConnection = prefs[POPUP_CONNECTED_KEY] ?: false, + onePodMode = prefs[ONE_POD_KEY] ?: false, + ) + } + + companion object { + private val TAG = logTag("LegacyReactionReader") + private const val LEGACY_STORE_NAME = "settings_reaction" + + internal val AUTO_PAUSE_KEY = booleanPreferencesKey("reaction.autopause.enabled") + internal val AUTO_PLAY_KEY = booleanPreferencesKey("reaction.autoplay.enabled") + internal val AUTO_CONNECT_KEY = booleanPreferencesKey("reaction.autoconnect.enabled") + internal val CONDITION_KEY = stringPreferencesKey("reaction.autoconnect.condition") + internal val POPUP_CASE_OPEN_KEY = booleanPreferencesKey("reaction.popup.caseopen") + internal val POPUP_CONNECTED_KEY = booleanPreferencesKey("reaction.popup.connected") + internal val ONE_POD_KEY = booleanPreferencesKey("reaction.onepod.enabled") + } +} + +data class LegacyReactionValues( + val autoPause: Boolean, + val autoPlay: Boolean, + val autoConnect: Boolean, + val autoConnectCondition: AutoConnectCondition, + val showPopUpOnCaseOpen: Boolean, + val showPopUpOnConnection: Boolean, + val onePodMode: Boolean, +) diff --git a/app/src/main/java/eu/darken/capod/profiles/core/ReactionConfig.kt b/app/src/main/java/eu/darken/capod/profiles/core/ReactionConfig.kt new file mode 100644 index 00000000..204bad35 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/profiles/core/ReactionConfig.kt @@ -0,0 +1,20 @@ +package eu.darken.capod.profiles.core + +import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition + +data class ReactionConfig( + val autoPause: Boolean = false, + val autoPlay: Boolean = false, + val onePodMode: Boolean = false, + val autoConnect: Boolean = false, + val autoConnectCondition: AutoConnectCondition = AutoConnectCondition.WHEN_SEEN, + val showPopUpOnCaseOpen: Boolean = false, + val showPopUpOnConnection: Boolean = false, +) + +interface HasReactionConfig { + val reactionConfig: ReactionConfig +} + +fun DeviceProfile?.toReactionConfig(): ReactionConfig = + (this as? HasReactionConfig)?.reactionConfig ?: ReactionConfig() diff --git a/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerScreen.kt b/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerScreen.kt index b752cd77..2d3a4d95 100644 --- a/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerScreen.kt +++ b/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.twotone.ArrowBack import androidx.compose.material.icons.twotone.Add import androidx.compose.material.icons.twotone.DragHandle +import androidx.compose.material.icons.twotone.Tune import androidx.compose.material3.Card import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -63,6 +64,7 @@ fun DeviceManagerScreenHost(vm: DeviceManagerViewModel = hiltViewModel()) { onBack = { vm.navUp() }, onAddDevice = { vm.onAddDevice() }, onEditProfile = { profile -> vm.onEditProfile(profile) }, + onDeviceSettings = { profile -> vm.onDeviceSettings(profile) }, onReorder = { ids -> vm.onReorder(ids) }, ) } @@ -74,6 +76,7 @@ fun DeviceManagerScreen( onBack: () -> Unit, onAddDevice: () -> Unit, onEditProfile: (DeviceProfile) -> Unit, + onDeviceSettings: (DeviceProfile) -> Unit = {}, onReorder: (List) -> Unit = {}, ) { val lazyListState = rememberLazyListState() @@ -142,6 +145,7 @@ fun DeviceManagerScreen( ), profile = profile, onClick = { if (!reorderableState.isDragging) onEditProfile(profile) }, + onDeviceSettings = { onDeviceSettings(profile) }, showDragHandle = showDragHandles, ) } @@ -230,13 +234,14 @@ private fun ProfileRow( modifier: Modifier = Modifier, profile: DeviceProfile, onClick: () -> Unit, + onDeviceSettings: () -> Unit = {}, showDragHandle: Boolean = false, ) { Row( modifier = modifier .fillMaxWidth() .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 12.dp), + .padding(start = 16.dp, end = 4.dp, top = 12.dp, bottom = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { Image( @@ -256,6 +261,13 @@ private fun ProfileRow( color = MaterialTheme.colorScheme.onSurfaceVariant, ) } + IconButton(onClick = onDeviceSettings) { + Icon( + imageVector = Icons.TwoTone.Tune, + contentDescription = stringResource(R.string.device_settings_open_cd), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } if (showDragHandle) { Icon( imageVector = Icons.TwoTone.DragHandle, diff --git a/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerViewModel.kt b/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerViewModel.kt index 0ce52bf7..90987a6c 100644 --- a/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerViewModel.kt +++ b/app/src/main/java/eu/darken/capod/profiles/ui/DeviceManagerViewModel.kt @@ -37,6 +37,11 @@ class DeviceManagerViewModel @Inject constructor( navTo(Nav.Main.DeviceProfileCreation(profileId = profile.id)) } + fun onDeviceSettings(profile: DeviceProfile) { + log(TAG) { "onDeviceSettings(): $profile" } + navTo(Nav.Main.DeviceSettings(profileId = profile.id)) + } + fun onReorder(profileIds: List) = launch { log(TAG) { "onReorder(): ${profileIds.size} profiles" } deviceProfilesRepo.reorderProfilesById(profileIds) diff --git a/app/src/main/java/eu/darken/capod/profiles/ui/creation/DeviceProfileCreationViewModel.kt b/app/src/main/java/eu/darken/capod/profiles/ui/creation/DeviceProfileCreationViewModel.kt index de338ef2..ddfe9a47 100644 --- a/app/src/main/java/eu/darken/capod/profiles/ui/creation/DeviceProfileCreationViewModel.kt +++ b/app/src/main/java/eu/darken/capod/profiles/ui/creation/DeviceProfileCreationViewModel.kt @@ -219,20 +219,30 @@ class DeviceProfileCreationViewModel @Inject constructor( launch { try { - val profile = AppleDeviceProfile( - id = if (isEditMode) profileId!! else UUID.randomUUID().toString(), - label = name, - model = editorState.selectedModel, - minimumSignalQuality = editorState.minimumSignalQuality, - identityKey = editorState.identityKeyHex?.fromHex(), - encryptionKey = editorState.encryptionKeyHex?.fromHex(), - address = editorState.selectedDeviceAddress, - ) - if (isEditMode) { - deviceProfilesRepo.updateProfile(profile) - log(TAG) { "Profile updated: $profile" } + // Read-modify-write so reaction toggles (and any other fields this editor + // doesn't know about) survive a rename or key-refresh. + deviceProfilesRepo.updateAppleProfile(profileId!!) { existing -> + existing.copy( + label = name, + model = editorState.selectedModel, + minimumSignalQuality = editorState.minimumSignalQuality, + identityKey = editorState.identityKeyHex?.fromHex(), + encryptionKey = editorState.encryptionKeyHex?.fromHex(), + address = editorState.selectedDeviceAddress, + ) + } + log(TAG) { "Profile updated: $profileId" } } else { + val profile = AppleDeviceProfile( + id = UUID.randomUUID().toString(), + label = name, + model = editorState.selectedModel, + minimumSignalQuality = editorState.minimumSignalQuality, + identityKey = editorState.identityKeyHex?.fromHex(), + encryptionKey = editorState.encryptionKeyHex?.fromHex(), + address = editorState.selectedDeviceAddress, + ) deviceProfilesRepo.addProfile(profile) log(TAG) { "Profile created: $profile" } } diff --git a/app/src/main/java/eu/darken/capod/reaction/core/ReactionSettings.kt b/app/src/main/java/eu/darken/capod/reaction/core/ReactionSettings.kt deleted file mode 100644 index 00f114b4..00000000 --- a/app/src/main/java/eu/darken/capod/reaction/core/ReactionSettings.kt +++ /dev/null @@ -1,48 +0,0 @@ -package eu.darken.capod.reaction.core - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.SharedPreferencesMigration -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.preferencesDataStore -import dagger.hilt.android.qualifiers.ApplicationContext -import eu.darken.capod.common.datastore.createValue -import eu.darken.capod.common.serialization.SerializationCapod -import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition -import kotlinx.serialization.json.Json -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ReactionSettings @Inject constructor( - @ApplicationContext private val context: Context, - @SerializationCapod json: Json, -) { - - private val Context.dataStore by preferencesDataStore( - name = "settings_reaction", - produceMigrations = { ctx -> listOf(SharedPreferencesMigration(ctx, "settings_reaction")) } - ) - - private val dataStore: DataStore get() = context.dataStore - - val autoPause = dataStore.createValue("reaction.autopause.enabled", false) - - val autoPlay = dataStore.createValue("reaction.autoplay.enabled", false) - - val autoConnect = dataStore.createValue("reaction.autoconnect.enabled", false) - - val autoConnectCondition = dataStore.createValue( - "reaction.autoconnect.condition", - AutoConnectCondition.WHEN_SEEN, - json, - onErrorFallbackToDefault = true, - ) - - val showPopUpOnCaseOpen = dataStore.createValue("reaction.popup.caseopen", false) - - val showPopUpOnConnection = dataStore.createValue("reaction.popup.connected", false) - - val onePodMode = dataStore.createValue("reaction.onepod.enabled", false) - -} diff --git a/app/src/main/java/eu/darken/capod/reaction/core/autoconnect/AutoConnect.kt b/app/src/main/java/eu/darken/capod/reaction/core/autoconnect/AutoConnect.kt index 4400c4f7..49b25361 100644 --- a/app/src/main/java/eu/darken/capod/reaction/core/autoconnect/AutoConnect.kt +++ b/app/src/main/java/eu/darken/capod/reaction/core/autoconnect/AutoConnect.kt @@ -10,10 +10,9 @@ import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.monitor.core.DeviceMonitor import eu.darken.capod.monitor.core.primaryDevice import eu.darken.capod.pods.core.apple.ble.devices.DualApplePods -import eu.darken.capod.profiles.core.DeviceProfilesRepo -import eu.darken.capod.reaction.core.ReactionSettings import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterNotNull @@ -22,23 +21,23 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import javax.inject.Inject import javax.inject.Singleton -import eu.darken.capod.common.datastore.valueBlocking @Singleton class AutoConnect @Inject constructor( private val bluetoothManager: BluetoothManager2, private val deviceMonitor: DeviceMonitor, - private val generalSettings: GeneralSettings, - private val reactionSettings: ReactionSettings, - private val deviceProfilesRepo: DeviceProfilesRepo, ) { - fun monitor(): Flow = reactionSettings.autoConnect.flow + fun monitor(): Flow = deviceMonitor.primaryDevice() + .map { it?.reactions?.autoConnect == true } + .distinctUntilChanged() .flatMapLatest { isAutoConnectEnabled -> if (isAutoConnectEnabled) { combine( bluetoothManager.connectedDevices, - deviceMonitor.primaryDevice().filterNotNull().distinctUntilChangedBy { it.rawDataHex }, + deviceMonitor.primaryDevice().filterNotNull().distinctUntilChangedBy { + Triple(it.rawDataHex, it.reactions.autoConnectCondition, it.reactions.onePodMode) + }, ) { connectedDevices, mainDevice -> connectedDevices to mainDevice } @@ -73,13 +72,14 @@ class AutoConnect @Inject constructor( return@map } - val condition = reactionSettings.autoConnectCondition.valueBlocking + val reactions = mainDevice.reactions + val condition = reactions.autoConnectCondition log(TAG) { "Checking condition $condition" } val lidState = mainDevice.caseLidState val isBeingWorn = mainDevice.isBeingWorn ?: false val isEitherPodInEar = mainDevice.isEitherPodInEar ?: false - val onePodMode = reactionSettings.onePodMode.valueBlocking + val onePodMode = reactions.onePodMode val decision = evaluateAutoConnect( mainDeviceAddr = mainDeviceAddr, diff --git a/app/src/main/java/eu/darken/capod/reaction/core/playpause/PlayPause.kt b/app/src/main/java/eu/darken/capod/reaction/core/playpause/PlayPause.kt index 1a2de6b1..d1ccc160 100644 --- a/app/src/main/java/eu/darken/capod/reaction/core/playpause/PlayPause.kt +++ b/app/src/main/java/eu/darken/capod/reaction/core/playpause/PlayPause.kt @@ -2,7 +2,6 @@ package eu.darken.capod.reaction.core.playpause import eu.darken.capod.common.MediaControl import eu.darken.capod.common.bluetooth.BluetoothManager2 -import eu.darken.capod.common.datastore.valueBlocking import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE import eu.darken.capod.common.debug.logging.Logging.Priority.WARN import eu.darken.capod.common.debug.logging.log @@ -11,12 +10,11 @@ import eu.darken.capod.common.flow.setupCommonEventHandlers import eu.darken.capod.common.flow.withPrevious import eu.darken.capod.monitor.core.DeviceMonitor import eu.darken.capod.monitor.core.primaryDevice -import eu.darken.capod.reaction.core.ReactionSettings -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import javax.inject.Inject import javax.inject.Singleton @@ -25,22 +23,21 @@ import javax.inject.Singleton class PlayPause @Inject constructor( private val deviceMonitor: DeviceMonitor, private val bluetoothManager: BluetoothManager2, - private val reactionSettings: ReactionSettings, private val mediaControl: MediaControl, ) { - fun monitor() = combine( - reactionSettings.autoPlay.flow, - reactionSettings.autoPause.flow, - reactionSettings.onePodMode.flow, // Included to restart pipeline when toggled; value read at decision time - ) { play, pause, _ -> play || pause } - .flatMapLatest { if (it) bluetoothManager.connectedDevices else emptyFlow() } - .flatMapLatest { - if (it.isEmpty()) { + fun monitor() = deviceMonitor.primaryDevice() + .map { device -> device?.reactions?.let { it.autoPlay || it.autoPause } == true } + .distinctUntilChanged() + .flatMapLatest { shouldMonitor -> + if (shouldMonitor) bluetoothManager.connectedDevices else emptyFlow() + } + .flatMapLatest { connected -> + if (connected.isEmpty()) { log(TAG) { "No known devices connected." } emptyFlow() } else { - log(TAG) { "Known devices connected: $it" } + log(TAG) { "Known devices connected: $connected" } deviceMonitor.primaryDevice() } } @@ -48,21 +45,28 @@ class PlayPause @Inject constructor( .withPrevious() .filter { (previous, current) -> if (previous == null || current == null) return@filter false - log(TAG, VERBOSE) { "previous-id=${previous.identifier}, current-id=${current.identifier}" } - val match = previous.identifier == current.identifier + // Use profileId (stable across BLE address rotations) rather than BLE identifier. + // Only profiled devices reach this point (outer gate requires profile.autoPlay/autoPause). + val match = previous.profileId != null && previous.profileId == current.profileId if (!match) log(TAG, WARN) { "Main device switched, skipping reaction." } match } .onEach { (previous, current) -> log(TAG, VERBOSE) { "Checking\nprevious=$previous\ncurrent=$current" } + val reactions = current?.reactions + if (reactions == null) { + log(TAG, VERBOSE) { "No reactions on current device, skipping reaction" } + return@onEach + } + // Convert to EarDetectionState based on device capabilities val prevState: EarDetectionState val currState: EarDetectionState when { previous!!.hasEarDetection && previous.hasDualPods && - current!!.hasEarDetection && current.hasDualPods -> { + current.hasEarDetection && current.hasDualPods -> { // Dual pod devices (AirPods, AirPods Pro, etc.) log(TAG, VERBOSE) { "Dual-pod device: left=${current.isLeftInEar}, right=${current.isRightInEar}" @@ -77,7 +81,7 @@ class PlayPause @Inject constructor( ) } - previous!!.hasEarDetection && current!!.hasEarDetection -> { + previous.hasEarDetection && current.hasEarDetection -> { // Single pod devices (AirPods Max, etc.) log(TAG, VERBOSE) { "Single-pod device: worn=${current.isBeingWorn}" } prevState = EarDetectionState.fromSinglePod(worn = previous.isBeingWorn ?: false) @@ -94,7 +98,7 @@ class PlayPause @Inject constructor( val decision = evaluatePlayPauseAction( previous = prevState, current = currState, - onePodMode = reactionSettings.onePodMode.valueBlocking, + onePodMode = reactions.onePodMode, isCurrentlyPlaying = mediaControl.isPlaying ) @@ -102,21 +106,21 @@ class PlayPause @Inject constructor( // Execute the decision when { - decision.shouldPlay && reactionSettings.autoPlay.valueBlocking -> { + decision.shouldPlay && reactions.autoPlay -> { log(TAG) { "autoPlay is triggered, sendPlay() - ${decision.reason}" } mediaControl.sendPlay() } - decision.shouldPlay && !reactionSettings.autoPlay.valueBlocking -> { + decision.shouldPlay && !reactions.autoPlay -> { log(TAG, VERBOSE) { "autoPlay is disabled" } } - decision.shouldPause && reactionSettings.autoPause.valueBlocking -> { + decision.shouldPause && reactions.autoPause -> { log(TAG) { "autoPause is triggered, sendPause() - ${decision.reason}" } mediaControl.sendPause() } - decision.shouldPause && !reactionSettings.autoPause.valueBlocking -> { + decision.shouldPause && !reactions.autoPause -> { log(TAG, VERBOSE) { "autoPause is disabled" } } } diff --git a/app/src/main/java/eu/darken/capod/reaction/core/popup/PopUpReaction.kt b/app/src/main/java/eu/darken/capod/reaction/core/popup/PopUpReaction.kt index 41dbb701..83d315fc 100644 --- a/app/src/main/java/eu/darken/capod/reaction/core/popup/PopUpReaction.kt +++ b/app/src/main/java/eu/darken/capod/reaction/core/popup/PopUpReaction.kt @@ -1,6 +1,7 @@ package eu.darken.capod.reaction.core.popup import eu.darken.capod.common.bluetooth.BluetoothAddress +import eu.darken.capod.common.bluetooth.BluetoothDevice2 import eu.darken.capod.common.bluetooth.BluetoothManager2 import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE import eu.darken.capod.common.debug.logging.log @@ -11,12 +12,9 @@ import eu.darken.capod.monitor.core.DeviceMonitor import eu.darken.capod.monitor.core.PodDevice import eu.darken.capod.monitor.core.primaryDevice import eu.darken.capod.pods.core.apple.ble.devices.DualApplePods -import eu.darken.capod.reaction.core.ReactionSettings import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChangedBy -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.merge import java.time.Duration @@ -27,35 +25,41 @@ import javax.inject.Singleton @Singleton class PopUpReaction @Inject constructor( private val deviceMonitor: DeviceMonitor, - private val reactionSettings: ReactionSettings, private val bluetoothManager: BluetoothManager2, ) { - private val caseCoolDowns = mutableMapOf() + private val caseCoolDowns = java.util.concurrent.ConcurrentHashMap() - private fun monitorCase(): Flow = reactionSettings.showPopUpOnCaseOpen.flow - .flatMapLatest { isEnabled -> - if (isEnabled) { - deviceMonitor.primaryDevice().distinctUntilChangedBy { it?.rawDataHex } - } else { - emptyFlow() - } + private fun monitorCase(): Flow = deviceMonitor.primaryDevice() + .distinctUntilChangedBy { + // Re-emit on profile changes (eligibility) AND on raw BLE state changes (lid). + Triple(it?.profileId, it?.reactions?.showPopUpOnCaseOpen, it?.rawDataHex) } .withPrevious() .setupCommonEventHandlers(TAG) { "popUpCase" } .mapNotNull { (previous, current) -> - if (current?.caseLidState == null) { - return@mapNotNull null + val wasEligible = previous?.reactions?.showPopUpOnCaseOpen == true + val isEligible = current?.reactions?.showPopUpOnCaseOpen == true + + // Eligibility transition: just lost it (toggled off, or switched to a profile + // with the toggle off). Dismiss any visible overlay immediately. + if (wasEligible && !isEligible) { + log(TAG) { "Case popup eligibility lost, emitting Hide" } + return@mapNotNull Event.PopupHide() } + + if (!isEligible) return@mapNotNull null + if (current.caseLidState == null) return@mapNotNull null + log(TAG, VERBOSE) { "previous=${previous?.caseLidState}, current=${current.caseLidState}" } - log(TAG, VERBOSE) { "previous-id=${previous?.identifier}, current-id=${current.identifier}" } - - val isSameDeviceOrProfile = previous?.identifier == current.identifier || - (previous?.profileId != null && previous.profileId == current.profileId) + val isSameDeviceOrProfile = previous?.profileId != null && previous.profileId == current.profileId val isSameDeviceWithCaseNowOpen = isSameDeviceOrProfile && previous?.caseLidState != current.caseLidState - val isNewDeviceWithJustOpenedCase = !isSameDeviceOrProfile && previous?.caseLidState != current.caseLidState + val isNewDeviceWithJustOpenedCase = !isSameDeviceOrProfile + && previous != null + && previous.caseLidState != null + && previous.caseLidState != current.caseLidState if (!isSameDeviceWithCaseNowOpen && !isNewDeviceWithJustOpenedCase) { return@mapNotNull null @@ -66,7 +70,7 @@ class PopUpReaction @Inject constructor( } private fun throttleCasePopUps(current: PodDevice): Event? { - val cooldownKey = current.profileId ?: current.identifier.toString() + val cooldownKey = current.profileId ?: current.identifier?.toString() ?: return null val now = Instant.now() val lastShown = caseCoolDowns[cooldownKey] @@ -99,38 +103,57 @@ class PopUpReaction @Inject constructor( } } - private val connectionCoolDowns = mutableMapOf() + private val connectionCoolDowns = java.util.concurrent.ConcurrentHashMap() - private fun monitorConnection(): Flow = reactionSettings.showPopUpOnConnection.flow - .flatMapLatest { isEnabled -> - if (!isEnabled) return@flatMapLatest emptyFlow() + private data class ConnectionWindow( + val direct: BluetoothDevice2?, + val broadcast: PodDevice?, + val eligible: Boolean, + ) - combine( - bluetoothManager.connectedDevices, - deviceMonitor.primaryDevice().distinctUntilChangedBy { it?.rawDataHex }, - ) { devices, broadcast -> - log(TAG) { "$broadcast $devices " } - val primaryAddr = broadcast?.address - val direct = devices.singleOrNull { it.address == primaryAddr }.also { - log(TAG, VERBOSE) { "Connected main device is $it" } - } - if (direct == null) { - connectionCoolDowns.remove(primaryAddr).also { - if (it != null) log(TAG) { "Cleared connection cooldown for $primaryAddr due to disconect" } - } - } - if (direct != null && broadcast != null) direct to broadcast else null + private fun monitorConnection(): Flow = combine( + bluetoothManager.connectedDevices, + deviceMonitor.primaryDevice().distinctUntilChangedBy { + Triple(it?.profileId, it?.reactions?.showPopUpOnConnection, it?.rawDataHex) + }, + ) { devices, broadcast -> + val eligible = broadcast?.reactions?.showPopUpOnConnection == true + if (!eligible) { + ConnectionWindow(direct = null, broadcast = null, eligible = false) + } else { + val primaryAddr = broadcast.address + ?: return@combine ConnectionWindow(direct = null, broadcast = null, eligible = false) + val direct = devices.singleOrNull { it.address == primaryAddr }.also { + log(TAG, VERBOSE) { "Connected main device is $it" } } + if (direct == null) { + connectionCoolDowns.remove(primaryAddr).also { + if (it != null) log(TAG) { "Cleared connection cooldown for $primaryAddr due to disconnect" } + } + } + ConnectionWindow(direct = direct, broadcast = broadcast, eligible = true) } + } .withPrevious() - .mapNotNull { (previouss, currents) -> - val previousConnected = previouss?.first + .mapNotNull { (previous, current) -> + val wasEligible = previous?.eligible == true + val isEligible = current.eligible + + // Eligibility transition: dismiss any visible overlay immediately. + if (wasEligible && !isEligible) { + log(TAG) { "Connection popup eligibility lost, emitting Hide" } + return@mapNotNull Event.PopupHide() + } + + if (!isEligible) return@mapNotNull null + + val previousConnected = previous?.direct log(TAG, VERBOSE) { "previousConnected: $previousConnected" } - val previousBroadcasted = previouss?.second + val previousBroadcasted = previous?.broadcast log(TAG, VERBOSE) { "previousBroadcasted: $previousBroadcasted" } - val currentConnected = currents?.first + val currentConnected = current.direct log(TAG, VERBOSE) { "currentConnected: $currentConnected" } - val currentBroadcasted = currents?.second + val currentBroadcasted = current.broadcast log(TAG, VERBOSE) { "currentBroadcasted: $currentBroadcasted" } if (previousConnected != null && previousBroadcasted != null && currentConnected == null) { diff --git a/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsNavigation.kt b/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsNavigation.kt deleted file mode 100644 index e250f4ee..00000000 --- a/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsNavigation.kt +++ /dev/null @@ -1,26 +0,0 @@ -package eu.darken.capod.reaction.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 ReactionSettingsNavigation @Inject constructor() : NavigationEntry { - override fun EntryProviderScope.setup() { - entry { ReactionSettingsScreenHost() } - } - - @Module - @InstallIn(SingletonComponent::class) - abstract class Mod { - @Binds - @IntoSet - abstract fun bind(entry: ReactionSettingsNavigation): NavigationEntry - } -} diff --git a/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsScreen.kt b/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsScreen.kt deleted file mode 100644 index e8303752..00000000 --- a/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsScreen.kt +++ /dev/null @@ -1,292 +0,0 @@ -package eu.darken.capod.reaction.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.twotone.ArrowBack -import androidx.compose.material.icons.twotone.BluetoothConnected -import androidx.compose.material.icons.twotone.LooksOne -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.Workspaces -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -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.res.stringResource -import androidx.compose.ui.semantics.Role -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 androidx.lifecycle.compose.collectAsStateWithLifecycle -import eu.darken.capod.common.error.ErrorEventHandler -import eu.darken.capod.common.navigation.NavigationEventHandler -import eu.darken.capod.common.settings.SettingsBaseItem -import eu.darken.capod.common.settings.SettingsCategoryHeader -import eu.darken.capod.common.settings.SettingsSwitchItem -import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition - -@Composable -fun ReactionSettingsScreenHost(vm: ReactionSettingsViewModel = hiltViewModel()) { - ErrorEventHandler(vm) - NavigationEventHandler(vm) - - val state by vm.state.collectAsStateWithLifecycle(initialValue = null) - - state?.let { - ReactionSettingsScreen( - state = it, - onNavigateUp = { vm.navUp() }, - onOnePodModeChanged = { enabled -> vm.setOnePodMode(enabled) }, - 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) }, - onShowPopUpOnConnectionChanged = { enabled -> vm.setShowPopUpOnConnection(enabled) }, - ) - } -} - -@Composable -fun ReactionSettingsScreen( - state: ReactionSettingsViewModel.State, - onNavigateUp: () -> Unit, - onOnePodModeChanged: (Boolean) -> Unit, - onAutoPlayChanged: (Boolean) -> Unit, - onAutoPauseChanged: (Boolean) -> Unit, - onAutoConnectChanged: (Boolean) -> Unit, - onAutoConnectConditionSelected: (AutoConnectCondition) -> Unit, - onShowPopUpOnCaseOpenChanged: (Boolean) -> Unit, - onShowPopUpOnConnectionChanged: (Boolean) -> Unit, - modifier: Modifier = Modifier, -) { - var showAutoConnectConditionDialog by remember { mutableStateOf(false) } - - Scaffold( - modifier = modifier, - topBar = { - TopAppBar( - title = { Text(text = stringResource(R.string.settings_reaction_label)) }, - navigationIcon = { - IconButton(onClick = onNavigateUp) { - Icon( - imageVector = Icons.AutoMirrored.TwoTone.ArrowBack, - contentDescription = null, - ) - } - }, - ) - }, - ) { innerPadding -> - LazyColumn(modifier = Modifier.padding(innerPadding)) { - item { - SettingsCategoryHeader(text = stringResource(R.string.settings_autopplay_label)) - } - item { - SettingsBaseItem( - title = stringResource(R.string.settings_onepod_mode_label), - subtitle = stringResource(R.string.settings_onepod_mode_description), - icon = Icons.TwoTone.LooksOne, - onClick = { onOnePodModeChanged(!state.onePodMode) }, - trailingContent = { - Switch( - checked = state.onePodMode, - onCheckedChange = onOnePodModeChanged, - modifier = Modifier.padding(start = 16.dp), - ) - }, - ) - } - item { - SettingsSwitchItem( - icon = Icons.TwoTone.PlayCircle, - title = stringResource(R.string.settings_autopplay_label), - subtitle = stringResource(R.string.settings_autoplay_description), - checked = state.autoPlay, - onCheckedChange = onAutoPlayChanged, - requiresUpgrade = !state.isPro, - ) - } - item { - SettingsSwitchItem( - icon = Icons.TwoTone.PauseCircle, - title = stringResource(R.string.settings_autopause_label), - subtitle = stringResource(R.string.settings_autopause_description), - checked = state.autoPause, - onCheckedChange = onAutoPauseChanged, - requiresUpgrade = !state.isPro, - ) - } - item { - SettingsBaseItem( - title = stringResource(R.string.settings_eardetection_info_label), - subtitle = stringResource(R.string.settings_eardetection_info_description), - icon = Icons.TwoTone.QuestionMark, - onClick = {}, - enabled = false, - ) - } - item { - SettingsCategoryHeader(text = stringResource(R.string.settings_autoconnect_label)) - } - item { - SettingsSwitchItem( - icon = Icons.TwoTone.BluetoothConnected, - title = stringResource(R.string.settings_autoconnect_label), - subtitle = stringResource(R.string.settings_autoconnect_description), - checked = state.autoConnect, - onCheckedChange = onAutoConnectChanged, - requiresUpgrade = !state.isPro, - ) - } - item { - SettingsBaseItem( - title = stringResource(R.string.settings_autoconnect_condition_label), - subtitle = stringResource(state.autoConnectCondition.labelRes), - icon = Icons.TwoTone.Workspaces, - onClick = { if (state.autoConnect) showAutoConnectConditionDialog = true }, - enabled = state.autoConnect, - ) - } - item { - SettingsCategoryHeader(text = stringResource(R.string.settings_category_other_label)) - } - item { - SettingsSwitchItem( - icon = Icons.AutoMirrored.TwoTone.Message, - title = stringResource(R.string.settings_popup_caseopen_label), - subtitle = stringResource(R.string.settings_popup_caseopen_description), - checked = state.showPopUpOnCaseOpen, - onCheckedChange = onShowPopUpOnCaseOpenChanged, - requiresUpgrade = !state.isPro, - ) - } - item { - SettingsSwitchItem( - icon = Icons.AutoMirrored.TwoTone.Message, - title = stringResource(R.string.settings_popup_connected_label), - subtitle = stringResource(R.string.settings_popup_connected_description), - checked = state.showPopUpOnConnection, - onCheckedChange = onShowPopUpOnConnectionChanged, - requiresUpgrade = !state.isPro, - ) - } - } - } - - if (showAutoConnectConditionDialog) { - AlertDialog( - onDismissRequest = { showAutoConnectConditionDialog = false }, - title = { Text(text = stringResource(R.string.settings_autoconnect_condition_label)) }, - text = { - Column(Modifier.selectableGroup()) { - AutoConnectCondition.entries.forEach { condition -> - val isSelected = condition == state.autoConnectCondition - Row( - modifier = Modifier - .fillMaxWidth() - .selectable( - selected = isSelected, - onClick = { - onAutoConnectConditionSelected(condition) - showAutoConnectConditionDialog = false - }, - role = Role.RadioButton, - ) - .padding(vertical = 12.dp, horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - RadioButton( - selected = isSelected, - onClick = null, - ) - Text( - text = stringResource(condition.labelRes), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(start = 16.dp), - ) - } - } - } - }, - confirmButton = { - TextButton(onClick = { showAutoConnectConditionDialog = false }) { - Text(text = stringResource(android.R.string.cancel)) - } - }, - ) - } -} - -@Preview2 -@Composable -private fun ReactionSettingsScreenPreview() = PreviewWrapper { - ReactionSettingsScreen( - state = ReactionSettingsViewModel.State( - isPro = true, - onePodMode = false, - autoPlay = true, - autoPause = true, - autoConnect = false, - autoConnectCondition = AutoConnectCondition.WHEN_SEEN, - showPopUpOnCaseOpen = true, - showPopUpOnConnection = false, - ), - onNavigateUp = {}, - onOnePodModeChanged = {}, - onAutoPlayChanged = {}, - onAutoPauseChanged = {}, - onAutoConnectChanged = {}, - onAutoConnectConditionSelected = {}, - onShowPopUpOnCaseOpenChanged = {}, - onShowPopUpOnConnectionChanged = {}, - ) -} - -@Preview2 -@Composable -private fun ReactionSettingsScreenNonProPreview() = PreviewWrapper { - ReactionSettingsScreen( - state = ReactionSettingsViewModel.State( - isPro = false, - onePodMode = false, - autoPlay = false, - autoPause = false, - autoConnect = false, - autoConnectCondition = AutoConnectCondition.WHEN_SEEN, - showPopUpOnCaseOpen = false, - showPopUpOnConnection = false, - ), - onNavigateUp = {}, - onOnePodModeChanged = {}, - onAutoPlayChanged = {}, - onAutoPauseChanged = {}, - onAutoConnectChanged = {}, - onAutoConnectConditionSelected = {}, - onShowPopUpOnCaseOpenChanged = {}, - onShowPopUpOnConnectionChanged = {}, - ) -} diff --git a/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsViewModel.kt b/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsViewModel.kt deleted file mode 100644 index ab69cc57..00000000 --- a/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsViewModel.kt +++ /dev/null @@ -1,134 +0,0 @@ -package eu.darken.capod.reaction.ui - -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 -import eu.darken.capod.main.core.MonitorMode -import eu.darken.capod.reaction.core.ReactionSettings -import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import javax.inject.Inject -import eu.darken.capod.common.datastore.valueBlocking - -@HiltViewModel -class ReactionSettingsViewModel @Inject constructor( - dispatcherProvider: DispatcherProvider, - private val reactionSettings: ReactionSettings, - private val generalSettings: GeneralSettings, - private val upgradeRepo: UpgradeRepo, -) : ViewModel4(dispatcherProvider) { - - data class State( - val isPro: Boolean, - val onePodMode: Boolean, - val autoPlay: Boolean, - val autoPause: Boolean, - val autoConnect: Boolean, - val autoConnectCondition: AutoConnectCondition, - val showPopUpOnCaseOpen: Boolean, - val showPopUpOnConnection: Boolean, - ) - - private val isPro = upgradeRepo.upgradeInfo.map { it.isPro }.asLiveState() - - val state = combine( - isPro, - reactionSettings.onePodMode.flow, - reactionSettings.autoPlay.flow, - reactionSettings.autoPause.flow, - reactionSettings.autoConnect.flow, - reactionSettings.autoConnectCondition.flow, - reactionSettings.showPopUpOnCaseOpen.flow, - reactionSettings.showPopUpOnConnection.flow, - ) { values -> - State( - isPro = values[0] as Boolean, - onePodMode = values[1] as Boolean, - autoPlay = values[2] as Boolean, - autoPause = values[3] as Boolean, - autoConnect = values[4] as Boolean, - autoConnectCondition = values[5] as AutoConnectCondition, - showPopUpOnCaseOpen = values[6] as Boolean, - showPopUpOnConnection = values[7] as Boolean, - ) - }.asLiveState() - - fun setOnePodMode(enabled: Boolean) { - reactionSettings.onePodMode.valueBlocking = enabled - } - - fun setAutoPlay(enabled: Boolean) = launch { - if (!enabled) { - reactionSettings.autoPlay.valueBlocking = false - return@launch - } - if (isPro.first()) { - reactionSettings.autoPlay.valueBlocking = true - } else { - navTo(Nav.Main.Upgrade) - } - } - - fun setAutoPause(enabled: Boolean) = launch { - if (!enabled) { - reactionSettings.autoPause.valueBlocking = false - return@launch - } - if (isPro.first()) { - reactionSettings.autoPause.valueBlocking = true - } else { - navTo(Nav.Main.Upgrade) - } - } - - fun setAutoConnect(enabled: Boolean) = launch { - if (!enabled) { - reactionSettings.autoConnect.valueBlocking = false - return@launch - } - if (isPro.first()) { - reactionSettings.autoConnect.valueBlocking = true - generalSettings.monitorMode.valueBlocking = MonitorMode.ALWAYS - } else { - navTo(Nav.Main.Upgrade) - } - } - - fun setAutoConnectCondition(condition: AutoConnectCondition) { - reactionSettings.autoConnectCondition.valueBlocking = condition - } - - fun setShowPopUpOnCaseOpen(enabled: Boolean) = launch { - if (!enabled) { - reactionSettings.showPopUpOnCaseOpen.valueBlocking = false - return@launch - } - if (isPro.first()) { - reactionSettings.showPopUpOnCaseOpen.valueBlocking = true - } else { - navTo(Nav.Main.Upgrade) - } - } - - fun setShowPopUpOnConnection(enabled: Boolean) = launch { - if (!enabled) { - reactionSettings.showPopUpOnConnection.valueBlocking = false - return@launch - } - if (isPro.first()) { - reactionSettings.showPopUpOnConnection.valueBlocking = true - } else { - navTo(Nav.Main.Upgrade) - } - } - - companion object { - private val TAG = logTag("Settings", "Reaction", "VM") - } -} diff --git a/app/src/main/res/values-af-rZA/strings.xml b/app/src/main/res/values-af-rZA/strings.xml index 9fcca56e..6e964fb2 100644 --- a/app/src/main/res/values-af-rZA/strings.xml +++ b/app/src/main/res/values-af-rZA/strings.xml @@ -21,7 +21,6 @@ CAPod word deur een persoon ontwikkel. Opgradering ontsluit ekstra funksies en help om die app aan die lewe te hou. Tema-aanpassing Outo-speel & outo-pouseer - Outo-koppel Opspringvenster by kas oop & verbinding Tuisskerm-widgets Gevorderde toestel-instellings @@ -66,7 +65,6 @@ Toestelle Bestuur jou toestelle. Reaksies - Reageer op gebeure en gedrag. Jou toestel Verenigbaarheidsopsies Moenie aanraak as alles werk nie ;) diff --git a/app/src/main/res/values-am/strings.xml b/app/src/main/res/values-am/strings.xml index 700077f7..46d84115 100644 --- a/app/src/main/res/values-am/strings.xml +++ b/app/src/main/res/values-am/strings.xml @@ -21,7 +21,6 @@ CAPod በአንድ ሰው ብቻ ተዘጋጅቷል። ማሻሻያ ተጨማሪ ባህሪያትን ያስከፍትና መተግበሪያውን ህያው ለማቆየት ይረዳል። ጭብጥ ብጁ አሰናዳ ራስ-ሰር አጫውት & ራስ-ሰር አቁም - ራስ-ሰር ግንኙነት ቴሌ ሲከፈት & ሲገናኝ ፖፕ-አፕ የመነሻ ስክሪን ዊጀቶች የተሻሻሉ የመሣሪያ ቅንብሮች @@ -66,7 +65,6 @@ መሳሪያዎች መሳሪያዎችዎን ያስተዳድሩ። ግብረመልሶች - ለክስተቶች እና ባህሪዎች ምላሽ ይስጡ። የእርስዎ መሣሪያ የተኳኋኝነት አማራጮች ሁሉም ነገር የሚሰራ ከሆነ አይንኩ ;) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index d71a11e1..39c44f18 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -21,7 +21,6 @@ تم تطوير CAPod من قِبل شخص واحد. الترقية تفتح ميزات إضافية وتساعد في إبقاء التطبيق حياً. تخصيص السمة تشغيل تلقائي وإيقاف مؤقت تلقائي - اتصال تلقائي نافذة منبثقة عند فتح الغطاء والاتصال عناصر واجهة الشاشة الرئيسية إعدادات الجهاز المتقدمة @@ -66,7 +65,6 @@ الأجهزة إدارة أجهزتك. ردود الفعل - التفاعل مع الأحداث والسلوكيات. جهازك خيارات التوافق لا تلمس إذا كان كل شيء يعمل ؛) diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index a99a66fe..d209acf2 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -21,7 +21,6 @@ CAPod tək bir şəxs tərəfindən hazırlanıb. Yüksəltmə əlavə funksiyaları açır və tətbiqin yaşamasına kömək edir. Tema fərdiləşdirməsi Avtomatik oynat və avtomatik dayandır - Avtomatik qoşulma Qutunu açarkən və qoşulduqda açılan pəncərə Ana ekran vidcetləri Qabaqcıl cihaz parametrləri @@ -66,7 +65,6 @@ Cihazlar Cihazlarınızı idarə edin. Reaksiyalar - Hadisələrə və davranışlara reaksiya verin. Sizin cihazınız Uyğunluq seçimləri Hər şey işləyirsə toxunmayın ;) diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 84aa939a..dbd5978b 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -21,7 +21,6 @@ CAPod распрацаваны адным чалавекам. Абнаўленне разблакоўвае дадатковыя функцыі і дапамагае трымаць праграму ў жывых. Наладка тэм Аўтаматычнае прайграванне і паўза - Аўтаматычнае падключэнне Усплывальнае акно пры адкрыцці скрынкі і падключэнні Віджэты галоўнага экрана Пашыраныя налады прылады @@ -66,7 +65,6 @@ Прылады Кіруйце вашымі прыладамі. Рэакцыі - Рэагаваць на падзеі і паводзіны. Ваша прылада Параметры сумяшчальнасці Не чапайце, калі ўсё працуе ;) diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 0a94d9cc..3afcd92a 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -21,7 +21,6 @@ CAPod е разработен от един човек. Надграждането отключва допълнителни функции и помага да поддържате приложението живо. Персонализиране на тема Автоматично възпроизвеждане и пауза - Автоматично свързване Изскачащ прозорец при отваряне на калъфа и свързване Джаджи на началния екран Разширени настройки на устройството @@ -66,7 +65,6 @@ Устройства Управлявайте устройствата си. Реакции - Реагирайте на събития и поведения. Вашето устройство Опции за съвместимост Не пипайте, ако всичко работи ;) diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 73cd24d9..f664b208 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -21,7 +21,6 @@ CAPod একজন একক ব্যক্তি দ্বারা তৈরি। আপগ্রেড করলে অতিরিক্ত বৈশিষ্ট্য আনলক হয় এবং অ্যাপটি বাঁচিয়ে রাখতে সহায়তা করে। থিম কাস্টমাইজেশন স্বয়ংক্রিয় প্লে এবং স্বয়ংক্রিয় পজ - স্বয়ংক্রিয় সংযোগ কেস খোলা ও সংযোগের সময় পপআপ হোম স্ক্রিন উইজেট উন্নত ডিভাইস সেটিংস @@ -66,7 +65,6 @@ ডিভাইস আপনার ডিভাইসগুলি পরিচালনা করুন। প্রতিক্রিয়া - ঘটনা এবং আচরণের প্রতি প্রতিক্রিয়া জানান। আপনার ডিভাইস সামঞ্জস্যতার বিকল্প সবকিছু কাজ করলে স্পর্শ করবেন না ;) diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index d7cb4b2a..455be2ea 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -21,7 +21,6 @@ El CAPod està desenvolupat per una sola persona. La millora desbloqueja funcions addicionals i ajuda a mantenir l\'aplicació activa. Personalització de temes Reproducció i pausa automàtica - Connexió automàtica Finestra emergent en obrir la funda i en connectar Ginys de la pantalla d\'inici Configuració avançada del dispositiu @@ -66,7 +65,6 @@ Dispositius Gestioneu els dispositius. Reaccions - Reacció davant esdeveniments i comportaments. El vostre dispositiu Opcions de compatibilitat No ho toqueu si tot funciona ;) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 7558a02d..b259e0c4 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -21,7 +21,6 @@ CAPod vyvíjí jediná osoba. Upgrade odemkne další funkce a pomůže udržet aplikaci naživu. Přizpůsobení motivu Automatické přehrávání a pozastavení - Automatické připojení Vyskakovací okno při otevření a připojení Widgety domovské obrazovky Pokročilá nastavení zařízení @@ -66,7 +65,6 @@ Zařízení Správa zařízení. Reakce - Reakce na události a chování. Vaše zařízení Možnosti kompatibility Nedotýkejte se, pokud vše funguje ;) diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index a3a11865..686ee4ea 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -21,7 +21,6 @@ CAPod er udviklet af én person. Opgradering låser ekstra funktioner op og hjælper med at holde appen i live. Tema-tilpasning Auto-afspilning & auto-pause - Auto-tilslut Popup ved åbning af etui & tilslutning Startskærm-widgets Avancerede enhedsindstillinger @@ -66,7 +65,6 @@ Enheder Administrer dine enheder. Reaktioner - Reager på begivenheder og adfærd. Din enhed Kompatibilitetsindstillinger Rør ikke, hvis alt virker ;) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 279fec4d..28c357fd 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -21,7 +21,6 @@ CAPod wird von einer einzelnen Person entwickelt. Ein Upgrade schaltet zusätzliche Funktionen frei und hilft, die App am Leben zu erhalten. Design-Anpassung Automatische Wiedergabe & automatisches Pausieren - Automatisch verbinden Popup beim Öffnen des Cases & bei Verbindung Startbildschirm-Widgets Erweiterte Geräteeinstellungen @@ -66,7 +65,6 @@ Geräte Verwalte Deine Geräte. Reaktionen - Reagier auf Ereignisse und Verhaltensweisen. Dein Gerät Kompatibilitätsoptionen Nicht anfassen, wenn alles funktioniert ;) diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e4bf14b3..80c6f180 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -21,7 +21,6 @@ Το CAPod αναπτύσσεται από ένα μόνο άτομο. Η αναβάθμιση ξεκλειδώνει επιπλέον λειτουργίες και βοηθά στη συνέχεια της εφαρμογής. Προσαρμογή θέματος Αυτόματη αναπαραγωγή & αυτόματη παύση - Αυτόματη σύνδεση Αναδυόμενο παράθυρο κατά το άνοιγμα θήκης & σύνδεση Γραφικά στοιχεία αρχικής οθόνης Προηγμένες ρυθμίσεις συσκευής @@ -66,7 +65,6 @@ Συσκευές Διαχειριστείτε τις συσκευές σας. Αντιδράσεις - Αντίδραση σε συμβάντα και συμπεριφορές. Η συσκευή σας Επιλογές συμβατότητας Μην αγγίζετε αν όλα λειτουργούν ;) diff --git a/app/src/main/res/values-es-rAR/strings.xml b/app/src/main/res/values-es-rAR/strings.xml index 77b38097..453ef360 100644 --- a/app/src/main/res/values-es-rAR/strings.xml +++ b/app/src/main/res/values-es-rAR/strings.xml @@ -21,7 +21,6 @@ CAPod es desarrollado por una sola persona. Actualizar desbloquea funciones extra y ayuda a mantener la app viva. Personalización de tema Reproducción automática y pausa automática - Conexión automática Ventana emergente al abrir el estuche y al conectar Widgets de pantalla de inicio Configuración avanzada del dispositivo @@ -66,7 +65,6 @@ Dispositivos Gestionar tus dispositivos. Reacciones - Reaccionar a eventos y comportamientos. Tu dispositivo Opciones de compatibilidad No tocar si todo funciona ;) diff --git a/app/src/main/res/values-es-rMX/strings.xml b/app/src/main/res/values-es-rMX/strings.xml index 03516654..40cce0f1 100644 --- a/app/src/main/res/values-es-rMX/strings.xml +++ b/app/src/main/res/values-es-rMX/strings.xml @@ -21,7 +21,6 @@ CAPod es desarrollado por una sola persona. Actualizar desbloquea funciones adicionales y ayuda a mantener la aplicación activa. Personalización de temas Reproducción automática y pausa automática - Conexión automática Popup al abrir la funda y conexión Widgets de pantalla de inicio Ajustes avanzados del dispositivo @@ -66,7 +65,6 @@ Dispositivos Gestionar tus dispositivos. Reacciones - Reaccionar a eventos y comportamientos. Tu dispositivo Opciones de compatibilidad No tocar si todo funciona ;) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index f9b1463a..b2368b80 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -21,7 +21,6 @@ CAPod es desarrollado por una sola persona. Actualizar desbloquea funciones adicionales y ayuda a mantener la app viva. Personalización de tema Reproducción automática y pausa automática - Conexión automática Ventana emergente al abrir el estuche y al conectar Widgets de pantalla de inicio Configuración avanzada del dispositivo @@ -66,7 +65,6 @@ Dispositivos Gestionar tus dispositivos. Reacciones - Reaccionar a eventos y comportamientos. Tu dispositivo Opciones de compatibilidad No toques si todo funciona ;) diff --git a/app/src/main/res/values-et-rEE/strings.xml b/app/src/main/res/values-et-rEE/strings.xml index 48c6081b..f9c9d7f7 100644 --- a/app/src/main/res/values-et-rEE/strings.xml +++ b/app/src/main/res/values-et-rEE/strings.xml @@ -21,7 +21,6 @@ CAPodi arendab üks inimene. Uuendamine avab lisavõimalused ja aitab rakendust arendada. Teema kohandamine Automaatesitus ja automaatne paus - Automaatne ühendamine Hüpikaken korpuse avamisel ja ühendamisel Avakuva vidinad Täpsemad seadme sätted @@ -66,7 +65,6 @@ Seadmed Seadmete haldamine. Reaktsioonid - Reageerige sündmustele ja tegevustele. Teie seade Ühilduvuse valikud Las jääda, kui kõik toimib ;) diff --git a/app/src/main/res/values-eu-rES/strings.xml b/app/src/main/res/values-eu-rES/strings.xml index b11448d8..0c389eba 100644 --- a/app/src/main/res/values-eu-rES/strings.xml +++ b/app/src/main/res/values-eu-rES/strings.xml @@ -21,7 +21,6 @@ CAPod pertsona bakar batek garatzen du. Berrituz gero, funtzio gehigarriak desblokeatzen dira eta aplikazioa bizirik mantentzen laguntzen duzu. Gai pertsonalizazioa Erreprodukzio automatikoa eta eten automatikoa - Automatikoki konektatu Leiho gainerakorra kutxako irekieran eta konexioan Hasierako pantailako widgetak Gailuaren ezarpen aurreratuak @@ -66,7 +65,6 @@ Gailuak Kudeatu zure gailuak. Erreakzioak - Erreakzionatu gertaera eta portaerei. Zure gailua Bateragarritasun aukerak Ez ukitu dena ondo badabil ;) diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index cf70155a..b03b2281 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -21,7 +21,6 @@ CAPod توسط یک نفر توسعه داده می‌شود. ارتقا دادن ویژگی‌های اضافی را باز می‌کند و به زنده نگه داشتن برنامه کمک می‌کند. سفارشی‌سازی پوسته پخش خودکار و توقف خودکار - اتصال خودکار پنجره‌ی بازشو هنگام باز کردن جعبه و اتصال ابزارک‌های صفحه‌ی اصلی تنظیمات پیشرفته دستگاه @@ -66,7 +65,6 @@ دستگاه‌ها مدیریت دستگاه‌های شما. واکنش‌ها - به رویدادها و رفتارها واکنش نشان دهید. دستگاه شما گزینه‌های سازگاری اگر همه چیز کار می‌کند، دست نزنید ؛) diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index e77876b9..940f7a3f 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -21,7 +21,6 @@ CAPod on yhden henkilön kehittämä. Päivittäminen avaa lisäominaisuuksia ja auttaa pitämään sovelluksen hengissä. Teeman mukauttaminen Automaattinen toisto & tauko - Automaattinen yhdistäminen Ponnahdusikkuna koteloa avattaessa & yhdistettäessä Aloitusnäytön widgetit Laitteiden lisäasetukset @@ -66,7 +65,6 @@ Laitteet Hallitse laitteitasi. Reaktiot - Reagoi tapahtumiin ja käyttäytymiseen. Laitteesi Yhteensopivuusasetukset Älä koske, jos kaikki toimii ;) diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index 94693700..f344062d 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -21,7 +21,6 @@ Ang CAPod ay ginawa ng isang tao. Ang pag-upgrade ay nagbubukas ng mga karagdagang feature at tumutulong na mapanatiling buhay ang app. Pag-customize ng tema Auto-play & auto-pause - Auto-connect Popup sa pagbubukas ng case & koneksyon Mga widget ng home screen Mga advanced na setting ng device @@ -66,7 +65,6 @@ Mga Device Pamahalaan ang iyong mga device. Mga Reaksyon - Mag-react sa mga kaganapan at pag-uugali. Iyong device Mga opsyon sa compatibility Huwag galawin kung gumagana ang lahat ;) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index e0b635a4..43df1836 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -21,7 +21,6 @@ CAPod est développée par une seule personne. Passer à la version Pro déverrouille des fonctions supplémentaires et contribue à la pérennité de l’appli. Personnalisation du thème Lecture et pause automatiques - Connexion automatique Fenêtre de notification à l\'ouverture de l\'étui et à la connexion Widgets sur l’écran d’accueil Paramètres avancés de l’appareil @@ -66,7 +65,6 @@ Appareils Gérez vos appareils. Réactions - Réagir à des événements et à des comportements. Votre appareil Options de compatibilité Ne rien changer rien si tout fonctionne ;) diff --git a/app/src/main/res/values-gl-rES/strings.xml b/app/src/main/res/values-gl-rES/strings.xml index 6a424f36..f4c806b4 100644 --- a/app/src/main/res/values-gl-rES/strings.xml +++ b/app/src/main/res/values-gl-rES/strings.xml @@ -21,7 +21,6 @@ CAPod está desenvolvido por unha soa persoa. Actualizar desbloquea funcionalidades adicionais e axuda a manter a aplicación viva. Personalización de temas Reprodución automática e pausa automática - Conexión automática Ventá emerxente ao abrir a funda e ao conectar Widgets da pantalla de inicio Configuración avanzada do dispositivo @@ -66,7 +65,6 @@ Dispositivos Xestiona os teus dispositivos. Reaccións - Reaccionar a eventos e comportamentos. O teu dispositivo Opcións de compatibilidade Non tocar se todo funciona ;) diff --git a/app/src/main/res/values-hi-rIN/strings.xml b/app/src/main/res/values-hi-rIN/strings.xml index 1e9438fe..17b561fa 100644 --- a/app/src/main/res/values-hi-rIN/strings.xml +++ b/app/src/main/res/values-hi-rIN/strings.xml @@ -21,7 +21,6 @@ CAPod एक अकेले व्यक्ति द्वारा विकसित किया गया है। अपग्रेड करने से अतिरिक्त सुविधाएं अनलॉक होती हैं और ऐप को जीवित रखने में मदद मिलती है। थीम अनुकूलन ऑटो-प्ले और ऑटो-पॉज़ - ऑटो-कनेक्ट केस खोलने और कनेक्शन पर पॉपअप होम स्क्रीन विजेट उन्नत डिवाइस सेटिंग्स @@ -66,7 +65,6 @@ डिवाइस अपने डिवाइस प्रबंधित करें। प्रतिक्रियाएँ - घटनाओं और व्यवहारों पर प्रतिक्रिया दें। आपका डिवाइस संगतता विकल्प अगर सब कुछ काम कर रहा है तो स्पर्श न करें ;) diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 4bb9555b..81c1a993 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -21,7 +21,6 @@ CAPod razvija jedna osoba. Nadogradnja otključava dodatne značajke i pomaže da aplikacija ostane aktivna. Prilagodba tema Automatska reprodukcija i pauza - Automatsko povezivanje Skočni prozor pri otvaranju kutije i spajanju Widgeti početnog zaslona Napredne postavke uređaja @@ -66,7 +65,6 @@ Uređaji Upravljajte svojim uređajima. Reakcije - Reagirajte na događaje i ponašanja. Vaš uređaj Opcije kompatibilnosti Ne dirajte ako sve radi ;) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 8a8039a7..b4e51791 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -21,7 +21,6 @@ A CAPod-ot egyetlen fejlesztő készíti. A frissítéssel extra funkciókat érhetsz el, és segíted az alkalmazás életben tartását. Téma testreszabás Automatikus lejátszás és szüneteltetés - Automatikus csatlakozás Felugró ablak tok megnyitásakor és csatlakozáskor Kezdőképernyő-widgetek Speciális eszközbeállítások @@ -66,7 +65,6 @@ Eszközök Eszközök kezelése. Reakciók - Reagáljon az eseményekre és viselkedésekre. Eszközei Kompatibilitási opciók Ezekhez ne nyúljon, ha minden jól műkődik ;) diff --git a/app/src/main/res/values-hy-rAM/strings.xml b/app/src/main/res/values-hy-rAM/strings.xml index 2f74f58b..4d54bff5 100644 --- a/app/src/main/res/values-hy-rAM/strings.xml +++ b/app/src/main/res/values-hy-rAM/strings.xml @@ -21,7 +21,6 @@ CAPod-ը մշակվում է մեկ անձի կողմից: Բարելավելը բացում է լրացուցիչ հնարավորություններ եւ օգնում է հավելվածը կենդանի պահել: Թեմայի հարմարեցում Ավտոնվագում & ավտոդադար - Ինքնամիացում Թռուցիկ՝ պատյան բացելիս & կապվելիս Գլխավոր էկրանի վիջեթներ Սառքի լրացկին կառավարումներ @@ -66,7 +65,6 @@ Սարքեր Կառավարեք ձեր սարքերը։ Արձագանքներ - Արձագանքեք իրադարձություններին և վարքագծերին։ Ձեր սարքը Համատեղելիության ընտրանքներ Մի դիպչեք, եթե ամեն ինչ աշխատում է ;) diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index a8b8d148..9454139d 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -21,7 +21,6 @@ CAPod dikembangkan oleh satu orang. Meningkatkan versi membuka fitur tambahan dan membantu menjaga aplikasi tetap berjalan. Kustomisasi tema Putar otomatis & jeda otomatis - Sambungkan otomatis Popup saat kotak dibuka & dihubungkan Widget layar beranda Pengaturan perangkat lanjutan @@ -66,7 +65,6 @@ Perangkat Kelola perangkat Anda. Reaksi - Bereaksi terhadap peristiwa dan perilaku. Perangkat Anda Opsi kompatibilitas Jangan sentuh jika semuanya berfungsi ;) diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 454ef56b..f0f749e4 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -21,7 +21,6 @@ CAPod er þróað af einum einstaklingi. Uppfærsla opnar aukaeiginleika og hjálpar til við að halda forritinu lifandi. Sérsniðið þema Sjálfvirk spilun og hlé - Sjálfvirk tenging Sprettigluggi við opnun hulsins og tengingu Heimaskjáviðbætur Tæki stillingar (avanó) @@ -66,7 +65,6 @@ Tæki Hafa umsjón með tækjunum þínum. Viðbrögð - Bregðast við atburðum og hegðun. Tækið þitt Samhæfisvalkostir Ekki snerta ef allt virkar ;) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 433c1e3d..7fd7fcae 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -21,7 +21,6 @@ CAPod è sviluppato da una singola persona. Aggiornare sblocca funzionalità extra e aiuta a mantenere l\'app in vita. Personalizzazione tema Riproduzione automatica e pausa automatica - Connessione automatica Popup all\'apertura della custodia e alla connessione Widget della schermata home Impostazioni avanzate dispositivo @@ -66,7 +65,6 @@ Dispositivi Gestisci i tuoi dispositivi. Reazioni - Reagisci a eventi e comportamenti. Il tuo dispositivo Opzioni di compatibilità Non toccare se tutto funziona ;) diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index fa81bf4f..3a5d65dc 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -21,7 +21,6 @@ CAPod פותחה על ידי אדם אחד. שדרוג פותח תכונות נוספות ועוזר לשמור על האפליקציה בחיים. התאמה אישית של ערכת נושא השמעה אוטומטית והשהיה אוטומטית - חיבור אוטומטי חלון קופץ בפתיחת הנרתיק וחיבור ווידג\'טים למסך הבית הגדרות מכשיר מתקדמות @@ -66,7 +65,6 @@ מכשירים ניהול המכשירים שלך. תגובות - להגיב לאירועים והתנהגויות. המכשיר שלך מצב תאימות אל תיגע אם הכל עובד ;) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 4af95f7d..a24a3ae6 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -21,7 +21,6 @@ CAPodは一人の開発者によって作られています。アップグレードすると追加機能がアンロックされ、アプリの存続を支援できます。 テーマのカスタマイズ 自動再生 & 自動一時停止 - 自動接続 ケースオープン & 接続時のポップアップ ホーム画面ウィジェット 詳細なデバイス設定 @@ -66,7 +65,6 @@ デバイス デバイスを管理します。 リアクション - イベントや行動に反応します。 あなたのデバイス 互換性オプション すべてが機能する場合は触れないでください ;) diff --git a/app/src/main/res/values-ka-rGE/strings.xml b/app/src/main/res/values-ka-rGE/strings.xml index 8b6eb15d..43db765c 100644 --- a/app/src/main/res/values-ka-rGE/strings.xml +++ b/app/src/main/res/values-ka-rGE/strings.xml @@ -21,7 +21,6 @@ CAPod შემუშავებულია ერთი ადამიანის მიერ. განახლება ხსნის დამატებით ფუნქციებს და ეხმარება აპს გადარჩენაში. თემის მორგება ავტო-დაკვრა და ავტო-პაუზა - ავტომატური დაკავშირება Popup ქეისის გახსნისა და დაკავშირებისას მთავარი ეკრანის ვიჯეტები მოწყობილობის გაფართოებული პარამეტრები @@ -66,7 +65,6 @@ მოწყობილობები მართეთ თქვენი მოწყობილობები. რეაქციები - რეაგირება მოვლენებსა და ქცევებზე. თქვენი მოწყობილობა თავსებადობის პარამეტრები არ შეეხოთ, თუ ყველაფერი მუშაობს ;) diff --git a/app/src/main/res/values-km-rKH/strings.xml b/app/src/main/res/values-km-rKH/strings.xml index 9a929b24..26a7564b 100644 --- a/app/src/main/res/values-km-rKH/strings.xml +++ b/app/src/main/res/values-km-rKH/strings.xml @@ -21,7 +21,6 @@ CAPod ត្រូវបានអភិវឌ្ឍដោយបុគ្គលម្នាក់។ ការតម្លើងកំណែថ្មីដោះសោមុខងារបន្ថែម ហើយជួយឱ្យកម្មវិធីនៅតែដំណើរការ។ ការប្ដូរទ្រង់ទ្រាយតាមបំណង ចាក់ដោយស្វ័យប្រវត្តិ & ផ្អាកដោយស្វ័យប្រវត្តិ - ភ្ជាប់ដោយស្វ័យប្រវត្តិ Popup នៅពេលបើកប្រអប់ & ការភ្ជាប់ វិជេតអេក្រង់ដើម ការកំណត់ឧបករណ៍កម្រិតខ្ពស់ @@ -66,7 +65,6 @@ ឧបករណ៍ គ្រប់គ្រងឧបករណ៍របស់អ្នក។ ប្រតិកម្ម - ប្រតិកម្មចំពោះព្រឹត្តិការណ៍ និងអាកប្បកិរិយា។ ឧបករណ៍របស់អ្នក ជម្រើសភាពឆបគ្នា កុំប៉ះប្រសិនបើអ្វីៗដំណើរការបានល្អ ;) diff --git a/app/src/main/res/values-kmr-rTR/strings.xml b/app/src/main/res/values-kmr-rTR/strings.xml index 42362bdd..03cafc36 100644 --- a/app/src/main/res/values-kmr-rTR/strings.xml +++ b/app/src/main/res/values-kmr-rTR/strings.xml @@ -21,7 +21,6 @@ CAPod ji aliyê kesekî yekane ve tê pêşxistin. Nûvekirina sepanê taybetmendiyên zêde vedike û dibe alîkar ku sepan sax bimîne. Xweşandina temayê Otomatîk lêdan & otomatîk rawestin - Girêdana otomatîk Popup dema vekirina qutiyê & girêdanê Widgetên ekrana malê Mîhengen pêşkertî yên cîhazê @@ -66,7 +65,6 @@ Amûr Amûrên xwe birêve bibin. Bertek - Bertek nîşanî bûyer û tevgeran bide. Amûra we Vebijarkên lihevhatinê Heke her tişt baş dixebite dest nedin ;) diff --git a/app/src/main/res/values-kn-rIN/strings.xml b/app/src/main/res/values-kn-rIN/strings.xml index 3bfc1d1a..8326520b 100644 --- a/app/src/main/res/values-kn-rIN/strings.xml +++ b/app/src/main/res/values-kn-rIN/strings.xml @@ -21,7 +21,6 @@ CAPod ಅನ್ನು ಒಬ್ಬ ವ್ಯಕ್ತಿ ಅಭಿವೃದ್ಧಿಪಡಿಸಿದ್ದಾರೆ. ಅಪ್‌ಗ್ರೇಡ್ ಮಾಡುವುದರಿಂದ ಹೆಚ್ಚುವರಿ ವೈಶಿಷ್ಟ್ಯಗಳನ್ನು ಅನ್ಲಾಕ್ ಮಾಡಲಾಗುತ್ತದೆ ಮತ್ತು ಅ್ಯಾಪ್ ಅನ್ನು ಜೀವಂತವಾಗಿಡಲು ಸಹಾಯ ಮಾಡುತ್ತದೆ. ಥೀಮ್ ಕಸ್ಟಮೈಸೇಶನ್ ಆಟೋ-ಪ್ಲೇ ಮತ್ತು ಆಟೋ-ಪಾಸ್ - ಸ್ವಯಂ-ಸಂಪರ್ಕ ಕೇಸ್ ತೆರೆದಾಗ ಮತ್ತು ಸಂಪರ್ಕದ ಸಮಯದಲ್ಲಿ ಪಾಪಪ್ ಹೋಮ್ ಸ್ಕ್ರೀನ್ ವಿಜೆಟ್‌ಗಳು ಸುಧಾರಿತ ಸಾಧನ ಸೆಟ್ಟಿಂಗ್ಸ್ @@ -66,7 +65,6 @@ ಸಾಧನಗಳು ನಿಮ್ಮ ಸಾಧನಗಳನ್ನು ನಿರ್ವಹಿಸಿ. ಪ್ರತಿಕ್ರಿಯೆಗಳು - ಘಟನೆಗಳು ಮತ್ತು ನಡವಳಿಕೆಗಳಿಗೆ ಪ್ರತಿಕ್ರಿಯಿಸಿ. ನಿಮ್ಮ ಸಾಧನ ಹೊಂದಾಣಿಕೆ ಆಯ್ಕೆಗಳು ಎಲ್ಲವೂ ಕಾರ್ಯನಿರ್ವಹಿಸುತ್ತಿದ್ದರೆ ಮುಟ್ಟಬೇಡಿ ;) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 2c4fc221..9f40616f 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -21,7 +21,6 @@ CAPod는 한 명이 개발합니다. 업그레이드하면 추가 기능이 잠금 해제되고 앱 유지에 도움이 됩니다. 테마 사용자 지정 자동 재생 & 자동 일시정지 - 자동 연결 케이스 열기 & 연결 시 팝업 홈 화면 위젯 고급 기기 설정 @@ -66,7 +65,6 @@ 장치 당신의 장치를 관리하세요. 반응 - 이벤트 및 행동에 반응합니다. 내 기기 호환성 옵션 모든 것이 작동하면 건드리지 마세요 ;) diff --git a/app/src/main/res/values-ky-rKG/strings.xml b/app/src/main/res/values-ky-rKG/strings.xml index 29193b3c..0d143b34 100644 --- a/app/src/main/res/values-ky-rKG/strings.xml +++ b/app/src/main/res/values-ky-rKG/strings.xml @@ -21,7 +21,6 @@ CAPod бир адам тарабынан иштелип чыгат. Жаңыртуу кошумча функцияларды ачып, колдонмонун тирүү калуусуна жардам берет. Темаларды ыңгайлаштыруу Авто-ойнотуу & авто-токтотуу - Авто туташуу Кутуча ачылганда & туташканда калкыма терезе Башкы экран виджеттери Өнүккөн түзмөк жөндөөлөрү @@ -66,7 +65,6 @@ Түзмөктөр Түзмөктөрүңүздү башкаруу. Реакциялар - Окуяларга жана жүрүм-турумдарга реакция кылыңыз. Сиздин түзмөгүңүз Шайкештик опциялары Эгер баары иштесе тийбеңиз ;) diff --git a/app/src/main/res/values-lo-rLA/strings.xml b/app/src/main/res/values-lo-rLA/strings.xml index f6d42b69..8f1672a8 100644 --- a/app/src/main/res/values-lo-rLA/strings.xml +++ b/app/src/main/res/values-lo-rLA/strings.xml @@ -21,7 +21,6 @@ CAPod ຖືກພັດທະນາໂດຍຄົນດຽວ. ການອັບເກຣດຈະປົດລັອກຟີເຈີເພີ່ມເຕີມ ແລະ ຊ່ວຍຮັກສາແອັບໃຫ້ດຳເນີນຕໍ່ໄປ. ປັບແຕ່ງຮູບແບບ ຫຼິ້ນອັດຕະໂນມັດ & ຢຸດຊົ່ວຄາວອັດຕະໂນມັດ - ເຊື່ອມຕໍ່ອັດຕະໂນມັດ ປັອບອັບເມື່ອເປີດກ່ອງ & ເຊື່ອມຕໍ່ ວິດເຈັດໜ້າຈໍຫຼັກ ການຕັ້ງຄ່າອຸປະກອນຂັ້ນສູງ @@ -66,7 +65,6 @@ ອຸປະກອນ ຈັດການອຸປະກອນຂອງທ່ານ. ປະຕິກິລິຍາ - ປະຕິກິລິຍາຕໍ່ເຫດການ ແລະ ພຶດຕິກຳ. ອຸປະກອນຂອງທ່ານ ຕົວເລືອກຄວາມເຂົ້າກັນໄດ້ ຢ່າແຕະຕ້ອງຖ້າທຸກຢ່າງເຮັດວຽກ ;) diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 102b1e26..9d5ab080 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -21,7 +21,6 @@ CAPod sukūrė vienas žmogus. Atnaujinimas atrakins papildomas funkcijas ir padės išlaikyti programėlę gyvą. Temos pritaikymas Automatinis grojimas ir pauzė - Automatinis prisijungimas Iššokantis langas atidarius dėklą ir prisijungus Pradinio ekrano valdikliai Išplėstiniai įrenginio nustatymai @@ -66,7 +65,6 @@ Įrenginiai Tvarkykite savo įrenginius. Reakcijos - Reaguoti į įvykius ir elgseną. Jūsų įrenginys Suderinamumo parinktys Nelieskite, jei viskas veikia ;) diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 4abe2ab8..c0843f9a 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -21,7 +21,6 @@ CAPod ir izstrādājusi viena persona. Jaunināšana atbloķē papildu funkcijas un palīdz uzturēt lietotni pie dzīvības. Tēmu pielāgošana Automātiska atskaņošana un pauze - Automātiska savienošana Uznirstošs logs, atverot futrāli un savienojoties Sākuma ekrāna logrīki Papildu ierīces iestatījumi @@ -66,7 +65,6 @@ Ierīces Pārvaldiet savas ierīces. Reakcijas - Reaģēt uz notikumiem un uzvedību. Jūsu ierīce Saderības opcijas Neaiztieciet, ja viss darbojas ;) diff --git a/app/src/main/res/values-mk-rMK/strings.xml b/app/src/main/res/values-mk-rMK/strings.xml index aad48819..ca4a5e83 100644 --- a/app/src/main/res/values-mk-rMK/strings.xml +++ b/app/src/main/res/values-mk-rMK/strings.xml @@ -21,7 +21,6 @@ CAPod е развиен од една личност. Надградувањето отклучува дополнителни функции и помага да се одржи апликацијата жива. Прилагодување на теми Автоматско пуштање & автоматска пауза - Автоматско поврзување Исскочувачки прозорец при отворање на куќиштето & поврзување Виџети на почетниот екран Напредни поставки на уредот @@ -66,7 +65,6 @@ Уреди Управувајте со вашите уреди. Реакции - Реагирајте на настани и однесувања. Вашиот уред Опции за компатибилност Не допирајте ако сè работи ;) diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index 22db84fc..1e95ef1f 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -21,7 +21,6 @@ CAPod ഒരൊറ്റ വ്യക്തി വികസിപ്പിച്ചതാണ്. അപ്‌ഗ്രേഡ് ചെയ്യുന്നത് അധിക സവിശേഷതകൾ അൺലോക്ക് ചെയ്യുകയും ആപ്പ് നിലനിർത്താൻ സഹായിക്കുകയും ചെയ്യുന്നു. തീം ഇഷ്ടാനുസൃതമാക്കൽ ഓട്ടോ-പ്ലേ & ഓട്ടോ-പോസ് - ഓട്ടോ-കണക്റ്റ് കേസ് തുറക്കുമ്പോഴും കണക്ഷൻ സമയത്തും പോപ്പ്അപ്പ് ഹോം സ്ക്രീൻ വിജറ്റുകൾ വിപുലമായ ഉപകരണ ക്രമീകരണങ്ങൾ @@ -66,7 +65,6 @@ ഉപകരണങ്ങൾ നിങ്ങളുടെ ഉപകരണങ്ങൾ നിയന്ത്രിക്കുക. പ്രতিকകരണങ്ങൾ - സംഭവങ്ങളോടും പെരുമാറ്റങ്ങളോടും പ്രതികരിക്കുക. നിങ്ങളുടെ ഉപകരണം അനുയോജ്യത ഓപ്ഷനുകൾ എല്ലാം ശരിയായി പ്രവർത്തിക്കുന്നുണ്ടെങ്കിൽ തൊടരുത് ;) diff --git a/app/src/main/res/values-mn-rMN/strings.xml b/app/src/main/res/values-mn-rMN/strings.xml index 2edaa82f..9ed02991 100644 --- a/app/src/main/res/values-mn-rMN/strings.xml +++ b/app/src/main/res/values-mn-rMN/strings.xml @@ -21,7 +21,6 @@ CAPod-ийг нэг хүн боловсруулсан. Шинэчлэх нь нэмэлт функцуудыг нээж, аппликейшнийг амьд байлгахад тусалдаг. Загварын тохиргоо Автомат тоглуулах & автомат зогсоох - Автомат холболт Тохиолдол нээх & холбогдох үед popup Нүүр дэлгэцийн виджетүүд Төхөөрөмжийн нарийнчилсан тохиргоо @@ -66,7 +65,6 @@ Төхөөрөмжүүд Төхөөрөмжүүдээ удирдах. Урвалууд - Үйл явдал, зан үйлд хариу үйлдэл үзүүлнэ. Таны төхөөрөмж Тохиромжтой байдлын сонголтууд Хэрэв бүх зүйл ажиллаж байвал бүү хүр ;) diff --git a/app/src/main/res/values-mr-rIN/strings.xml b/app/src/main/res/values-mr-rIN/strings.xml index 72f69d69..5e9cbac2 100644 --- a/app/src/main/res/values-mr-rIN/strings.xml +++ b/app/src/main/res/values-mr-rIN/strings.xml @@ -21,7 +21,6 @@ CAPod एका व्यक्तीने विकसित केला आहे. अपग्रेड केल्यास अतिरिक्त वैशिष्ट्ये मिळतात आणि अॅप सुरू ठेवण्यास मदत होते. थीम सानुकूलन ऑटो-प्ले आणि ऑटो-पॉज - ऑटो-कनेक्ट केस उघडताना आणि कनेक्शनवर पॉपअप होम स्क्रीन विजेट प्रगत डिव्हाइस सेटिंग्ज @@ -66,7 +65,6 @@ डिव्हाइस आपल्या डिव्हाइसचे व्यवस्थापन करा. प्रतिक्रिया - घटना आणि वर्तनांवर प्रतिक्रिया द्या. तुमचे डिव्हाइस सुसंगतता पर्याय सर्व काही ठीक चालत असल्यास स्पर्श करू नका ;) diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 2a9a07f8..600107be 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -21,7 +21,6 @@ CAPod dibangunkan oleh seorang individu. Menaik taraf membuka kunci ciri-ciri tambahan dan membantu mengekalkan aplikasi ini. Penyesuaian tema Main automatik & jeda automatik - Sambung automatik Popup semasa buka kotak & sambungan Widget skrin utama Tetapan peranti lanjutan @@ -66,7 +65,6 @@ Peranti Urus peranti anda. Reaksi - Bertindak balas terhadap peristiwa dan tingkah laku. Peranti anda Pilihan keserasian Jangan sentuh jika semuanya berfungsi ;) diff --git a/app/src/main/res/values-my-rMM/strings.xml b/app/src/main/res/values-my-rMM/strings.xml index a661019b..3165c508 100644 --- a/app/src/main/res/values-my-rMM/strings.xml +++ b/app/src/main/res/values-my-rMM/strings.xml @@ -21,7 +21,6 @@ CAPod ကို တစ်ဦးတည်းသောသူက တည်ဆောက်ထားသည်။ အဆင့်မြှင့်ခြင်းဖြင့် အပိုဝန်ဆောင်မှုများကို ရယူနိုင်ပြီး အက်ပ်ကို ဆက်လက်ရှင်သန်စေရန် ကူညီပေးသည်။ အပြင်အဆင် စိတ်ကြိုက်ပြင်ဆင်မှု အလိုအလျောက် ဖွင့်ခြင်း & ခေတ္တရပ်ခြင်း - အလိုအလျောက် ချိတ်ဆက်ခြင်း ဖွင့်သောအခါ & ချိတ်ဆက်သောအခါ Popup ပေါ်လာခြင်း ပင်မစခရင် Widget များ အဆင့်မြင့့်် ကိရိယာ ဆက်တင်များ @@ -66,7 +65,6 @@ စက်ပစ္စည်းများ သင့်စက်ပစ္စည်းများကို စီမံခန့်ခွဲပါ။ တုံ့ပြန်မှုများ - ဖြစ်ရပ်များနှင့် အပြုအမူများအပေါ် တုံ့ပြန်ပါ။ သင့်စက်ပစ္စည်း လိုက်ဖက်ညီမှုရွေးချယ်စရာများ အရာအားလုံးအဆင်ပြေနေပါက မထိပါနှင့် ;) diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 230cdd50..f6ead146 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -21,7 +21,6 @@ CAPod er utviklet av én person. Oppgradering låser opp ekstra funksjoner og bidrar til å holde appen i live. Tematilpasning Autoavspilling & autopause - Autotilkobling Popup ved åpning av etui & tilkobling Hjemmeskjerm-widgeter Avanserte enhetsinnstillinger @@ -66,7 +65,6 @@ Enheter Administrer enhetene dine. Reaksjoner - Reager på hendelser og oppførsel. Enheten din Kompatibilitets-alternativer Ikke rør hvis alt virker ;) diff --git a/app/src/main/res/values-ne-rNP/strings.xml b/app/src/main/res/values-ne-rNP/strings.xml index 4b530eb2..43120e12 100644 --- a/app/src/main/res/values-ne-rNP/strings.xml +++ b/app/src/main/res/values-ne-rNP/strings.xml @@ -21,7 +21,6 @@ CAPod एकल व्यक्तिद्वारा विकास गरिएको छ। अपग्रेड गर्दा थप सुविधाहरू अनलक हुन्छन् र एपलाई जीवित राख्न मद्दत गर्छ। थिम अनुकूलन स्वत: प्ले र स्वत: पज - स्वत: जडान केस खोल्दा र जडान हुँदा पपअप होम स्क्रिन विजेटहरू उन्नत उपकरण सेटिङहरू @@ -66,7 +65,6 @@ उपकरणहरू आफ्ना उपकरणहरू व्यवस्थापन गर्नुहोस्। प्रतिक्रियाहरू - घटनाहरू र व्यवहारहरूमा प्रतिक्रिया दिनुहोस्। तपाईंको उपकरण अनुकूलता विकल्पहरू सबै कुरा ठीकठाक चलिरहेको छ भने नचलाउनुहोस् ;) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index ea7cc192..a343dcf9 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -21,7 +21,6 @@ CAPod wordt ontwikkeld door één persoon. Upgraden ontgrendelt extra functies en helpt de app in leven te houden. Thema-aanpassing Automatisch afspelen & pauzeren - Automatisch verbinden Popup bij openen van hoesje & verbinding Startscherm-widgets Geavanceerde apparaatinstellingen @@ -66,7 +65,6 @@ Apparaten Beheer je apparaten. Reacties - Reageren op gebeurtenissen en gedragingen. Uw apparaat Compatibiliteitsopties Niet aankomen als alles werkt ;-) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index b8f1ee87..ff604714 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -21,7 +21,6 @@ CAPod jest rozwijany przez jedną osobę. Uaktualnienie odblokowuje dodatkowe funkcje i pomaga utrzymać aplikację przy życiu. Personalizacja motywu Automatyczne odtwarzanie i automatyczna pauza - Automatyczne połączenie Wyskakujące okienko po otwarciu etui i połączeniu Widżety ekranu głównego Zaawansowane ustawienia urządzenia @@ -66,7 +65,6 @@ Urządzenia Zarządzaj swoimi urządzeniami. Reakcje - Reakcje na wydarzenia lub zachowania. Twoje urządzenie Opcje kompatybilności Nie dotykaj, jeżeli wszystko działa ;) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 3807b328..db50b74a 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -21,7 +21,6 @@ O CAPod é desenvolvido por uma única pessoa. Fazer upgrade desbloqueia recursos extras e ajuda a manter o aplicativo ativo. Personalização de tema Reprodução automática & pausa automática - Conexão automática Popup ao abrir o estojo & conexão Widgets da tela inicial Configurações avançadas do dispositivo @@ -66,7 +65,6 @@ Dispositivos Gerenciar seus dispositivos Reações - Reaja a eventos e comportamentos. Seu dispositivo Opções de compatibilidade Não toque se tudo funcionar ;) diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 0b8c2133..b17803b1 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -21,7 +21,6 @@ O CAPod é desenvolvido por uma única pessoa. Atualizar desbloqueia funcionalidades extra e ajuda a manter a aplicação ativa. Personalização de tema Reprodução automática & pausa automática - Ligar automaticamente Pop-up ao abrir a caixa & ao ligar Widgets do ecrã principal Definições avançadas do dispositivo @@ -66,7 +65,6 @@ Dispositivos Gerir os seus dispositivos. Reações - Reagir a eventos e comportamentos. O seu dispositivo Opções de compatibilidade Não mexa se tudo estiver a funcionar ;) diff --git a/app/src/main/res/values-rm/strings.xml b/app/src/main/res/values-rm/strings.xml index 6a5ae989..0dd7738f 100644 --- a/app/src/main/res/values-rm/strings.xml +++ b/app/src/main/res/values-rm/strings.xml @@ -21,7 +21,6 @@ CAPod è sviluppà da ina singula persuna. Actualisar sblucca funcziuns supplementaras ed agida a mantegnair l\'app viva. Persunalisaziun dal tema Start automatic & pausa automatica - Connectar automaticamain Popup sin avrir il cais & connexiun Widgets sin l\'ècran principal Parameters avanzads da l\'apparat @@ -66,7 +65,6 @@ Apparats Administrar Voss apparats. Reacziuns - Reagir sin eveniments e cumportaments. Voss apparat Opziuns da cumpatibilitad Betg tuccar sche tut funcziuna ;) diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 16d798c5..34730cb5 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -21,7 +21,6 @@ CAPod este dezvoltat de o singură persoană. Actualizarea deblochează funcții extra și ajută la menținerea aplicației în viață. Personalizare temă Redare automată și pauză automată - Conectare automată Fereastră pop-up la deschiderea husă și conectare Widgeturi ecran principal Setări avansate dispozitiv @@ -66,7 +65,6 @@ Dispozitive Gestionați dispozitivele dvs. Reacții - Reacționați la evenimente și comportamente. Dispozitivul tău Opțiuni de compatibilitate Nu atingeți dacă totul funcționează ;) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 01c97af7..d52a49b7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -21,7 +21,6 @@ CAPod разработан одним человеком. Обновление открывает дополнительные функции и помогает поддерживать приложение. Настройка темы Авто-воспроизведение и авто-пауза - Автоподключение Всплывающее окно при открытии чехла и подключении Виджеты главного экрана Расширенные настройки устройства @@ -66,7 +65,6 @@ Устройства Управление устройствами. Реакции - Реакции на события и действия. Ваше устройство Настройки совместимости Не трогать, если всё работает :) diff --git a/app/src/main/res/values-sc-rIT/strings.xml b/app/src/main/res/values-sc-rIT/strings.xml index 2e8f9e3e..cf18f4f3 100644 --- a/app/src/main/res/values-sc-rIT/strings.xml +++ b/app/src/main/res/values-sc-rIT/strings.xml @@ -21,7 +21,6 @@ CAPod est isvilupadu dae una sola persona. Aggiornar isbloca funtzionalidades in prus e agiudat a mantènnere s\'app in vita. Personalizzatzione de su tema Reprodutzione autom. e pausa autom. - Cullegamentu automàticu Popup a s\'apertùra de sa custodia e cullegamentu Widget de sa pantalla printzipale Impostatziones avanzadas de su dispositivo @@ -66,7 +65,6 @@ Dispositivos Gestisci is dispositivos tuos. Reatziones - Reaghit a eventos e cumportamentos. Su dispositivu tuo Opsiones de cumpatibilidade Non toches si totu funtzionat ;) diff --git a/app/src/main/res/values-si-rLK/strings.xml b/app/src/main/res/values-si-rLK/strings.xml index 4b6c2bc7..7cab752d 100644 --- a/app/src/main/res/values-si-rLK/strings.xml +++ b/app/src/main/res/values-si-rLK/strings.xml @@ -21,7 +21,6 @@ CAPod සකසා ඇත්තේ එක් කෙනෙකු විසිනි. යාවත්කාලීන කිරීම අමතර විශේෂාංග විවෘත කරන අතර යෙදුම ක්‍රියාත්මකව තබා ගැනීමට උපකාරී වේ. තේමා අභිරුචිකරණය ස්වයංක්‍රීය ධාවනය සහ ස්වයංක්‍රීය විරාමය - ස්වයංක්‍රීයව සම්බන්ධ වන්න කේස් විවෘත කිරීමේ සහ සම්බන්ධතාවයේ පොප්අප් මුල් පිටු තිර විජට් උසස් උපාංග සැකසුම් @@ -66,7 +65,6 @@ උපාංග ඔබගේ උපාංග කළමනාකරණය කරන්න. ප්‍රතික්‍රියා - සිදුවීම් සහ හැසිරීම් වලට ප්‍රතිචාර දක්වන්න. ඔබගේ උපාංගය ගැළපුම් විකල්ප සියල්ල ක්‍රියාත්මක නම් අත නොගසන්න ;) diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 9ec00529..9a3ca99b 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -21,7 +21,6 @@ CAPod vyvíja jedna osoba. Aktualizácia odomkne extra funkcie a pomáha udržiavať aplikáciu v prevádzke. Prispôsobenie tém Automatické prehrávanie & pauza - Automatické pripojenie Vyskakovacie okno pri otvorení puzdra & pripojení Widgety domovskej obrazovky Pokročilé nastavenia zariadenia @@ -66,7 +65,6 @@ Zariadenia Spravujte svoje zariadenia. Reakcie - Reakcia na udalosti a správanie. Vaše zariadenie Možnosti kompatibility Nedotýkajte sa, ak všetko funguje ;) diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index fd89130a..62f79b9a 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -21,7 +21,6 @@ CAPod je razvila ena sama oseba. Z nadgradnjo odklenete dodatne funkcije in pomagate ohranjati aplikacijo. Prilagajanje teme Samodejno predvajanje in zaustavitev - Samodejna povezava Pojavno okno ob odprtju ohišja in povezavi Gradniki na začetnem zaslonu Napredne nastavitve naprave @@ -66,7 +65,6 @@ Naprave Upravljajte svoje naprave. Reakcije - Odzovite se na dogodke in vedenja. Vaša naprava Možnosti združljivosti Ne dotikajte se, če vse deluje ;) diff --git a/app/src/main/res/values-sq-rAL/strings.xml b/app/src/main/res/values-sq-rAL/strings.xml index b03c901a..62948e54 100644 --- a/app/src/main/res/values-sq-rAL/strings.xml +++ b/app/src/main/res/values-sq-rAL/strings.xml @@ -21,7 +21,6 @@ CAPod është zhvilluar nga një person i vetëm. Përmirësimi zhbllokon veçori shtesë dhe ndihmon të mbajë aplikacionin gjallë. Personalizimi i temës Auto-luaj & auto-pauzë - Lidhje automatike Dritare kërcyese kur hapet kuti & lidhja Miniaplikacionet e ekranit kryesor Cilësimet e avancuara të pajisjes @@ -66,7 +65,6 @@ Pajisjet Menaxhoni pajisjet tuaja. Reagimet - Reago ndaj ngjarjeve dhe sjelljeve. Pajisja juaj Opsionet e përputhshmërisë Mos i prekni nëse gjithçka funksionon ;) diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index f80a64ef..51ab8893 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -21,7 +21,6 @@ CAPod је развила једна особа. Надоградњом откључавате додатне функције и помажете да апликација опстане. Прилагођавање теме Аутоматско репродуковање и паузирање - Аутоматско повезивање Искачући прозор при отварању кућишта и повезивању Виџети почетног екрана Напредна подешавања уређаја @@ -66,7 +65,6 @@ Уређаји Управљајте својим уређајима. Реакције - Реагујте на догађаје и понашања. Ваш уређај Опције компатибилности Не дирајте ако све ради ;) diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index b0bae25c..678cece2 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -21,7 +21,6 @@ CAPod är utvecklad av en enda person. Uppgraderingen låser upp extra funktioner och hjälper till att hålla appen vid liv. Temoanpassning Uppspelning automatiskt & automatisk paus - Anslut automatiskt Popup vid öppning av fodral & anslutning Hemskärmswidgetar Avancerade enhetsinställningar @@ -66,7 +65,6 @@ Enheter Hantera dina enheter. Reaktioner - Reagera på händelser och beteenden. Din enhet Kompatibilitetsalternativ Rör inte om allt fungerar ;) diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml index c1803f0c..a92c732d 100644 --- a/app/src/main/res/values-sw/strings.xml +++ b/app/src/main/res/values-sw/strings.xml @@ -21,7 +21,6 @@ CAPod inaendelezwa na mtu mmoja. Kuboresha kunafungua vipengele vya ziada na husaidia kuweka programu iwe hai. Ubinafsishaji wa mandhari Cheza kiotomatiki & simama kiotomatiki - Unganisha kiotomatiki Dirisha la kuibuka wakati wa kufungua kifuniko & muunganiko Wijeti za skrini ya nyumbani Mipangilio ya kifaa ya hali ya juu @@ -66,7 +65,6 @@ Vifaa Simamia vifaa vyako. Matendo - Jibu kwa matukio na tabia. Kifaa chako Chaguo za uoanifu Usiguse ikiwa kila kitu kinafanya kazi ;) diff --git a/app/src/main/res/values-ta-rIN/strings.xml b/app/src/main/res/values-ta-rIN/strings.xml index 44e35521..ff4fde45 100644 --- a/app/src/main/res/values-ta-rIN/strings.xml +++ b/app/src/main/res/values-ta-rIN/strings.xml @@ -21,7 +21,6 @@ CAPod ஒரு நபரால் உருவாக்கப்பட்டது. மேம்படுத்துவது கூடுதல் அம்சங்களை திறக்கிறது மற்றும் பயன்பாட்டை உயிருடன் வைத்திருக்க உதவுகிறது. தீம் தனிப்பயனாக்கம் தானியங்கு இயக்கம் மற்றும் தானியங்கு இடைநிறுத்தம் - தானியங்கு இணைப்பு கேஸ் திறக்கும்போது மற்றும் இணைப்பின்போது பாப்அப் முகப்புத் திரை விட்ஜெட்கள் மேம்பட்ட சாதன அமைப்புகள் @@ -66,7 +65,6 @@ சாதனங்கள் உங்கள் சாதனங்களை நிர்வகிக்கவும். எதிர்வினைகள் - நிகழ்வுகள் மற்றும் நடத்தைகளுக்கு எதிர்வினையாற்று. உங்கள் சாதனம் இணக்கத்தன்மை விருப்பங்கள் எல்லாம் வேலை செய்தால் தொடாதே ;) diff --git a/app/src/main/res/values-te-rIN/strings.xml b/app/src/main/res/values-te-rIN/strings.xml index 051d3d86..f9b6b27c 100644 --- a/app/src/main/res/values-te-rIN/strings.xml +++ b/app/src/main/res/values-te-rIN/strings.xml @@ -21,7 +21,6 @@ CAPod ఒక వ్యక్తిచే అభివృద్ధి చేయబడింది. అప్‌గ్రేడ్ చేయడం అదనపు ఫీచర్లను అన్‌లాక్ చేస్తుంది మరియు యాప్‌ను సజీవంగా ఉంచడానికి సహాయపడుతుంది. థీమ్ కస్టమైజేషన్ ఆటో-ప్లే & ఆటో-పాజ్ - ఆటో-కనెక్ట్ కేస్ తెరిచినప్పుడు & కనెక్షన్‌పై పాప్‌అప్ హోమ్ స్క్రీన్ విడ్జెట్‌లు అధునాతన పరికర సెట్టింగ్‌లు @@ -66,7 +65,6 @@ పరికరాలు మీ పరికరాలను నిర్వహించండి. ప్రతిచర్యలు - ఈవెంట్‌లు మరియు ప్రవర్తనలకు ప్రతిస్పందించండి. మీ పరికరం అనుకూలత ఎంపికలు అన్నీ పని చేస్తే తాకవద్దు ;) diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index ea632678..ac6f9327 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -21,7 +21,6 @@ CAPod พัฒนาโดยบุคคลเพียงคนเดียว การอัปเกรดจะปลดล็อกฟีเจอร์เพิ่มเติมและช่วยให้แอปนี้ดำเนินต่อไปได้ การปรับแต่งธีม เล่นอัตโนมัติและหยุดชั่วคราวอัตโนมัติ - เชื่อมต่ออัตโนมัติ ป๊อปอัปเมื่อเปิดเคสและเชื่อมต่อ วิดเจ็ตหน้าจอหลัก การตั้งค่าอุปกรณ์ขั้นสูง @@ -66,7 +65,6 @@ อุปกรณ์ จัดการอุปกรณ์ของคุณ ปฏิกิริยา - ตอบสนองต่อเหตุการณ์และพฤติกรรม อุปกรณ์ของคุณ ตัวเลือกความเข้ากันได้ อย่าแตะต้องหากทุกอย่างทำงานได้ดี ;) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index f6c4bc91..d7d85c7d 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -21,7 +21,6 @@ CAPod bir kişi tarafından geliştirilmiştir. Yükseltme ekstra özelliklerin kilidini açar ve uygulamanın canlı kalabilmesine yardımcı olur. Tema özelleştirmesi Oto-oynatma & oto-duraklatma - Oto-bağlanma Kutu açıldığında & bağlandığında popup Ana ekran widgetleri Gelişmiş cihaz ayarları @@ -66,7 +65,6 @@ Cihazlar Cihazlarınızı yönetin. Tepkiler - Olaylar ve etkileşimlere verilen tepkiler. Aygıtınız Uyumluluk seçenekleri Eğer her şey düzgün çalışıyorsa lütfen dokunma ;) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 915c2973..2a0c7db0 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -21,7 +21,6 @@ CAPod розробляється однією людиною. Оновлення розблоковує додаткові функції та допомагає підтримувати застосунок. Налаштування теми Автовідтворення та автопауза - Автопід\'єднання Спливаюче вікно при відкритті футляра та підключенні Віджети головного екрана Розширені налаштування пристрою @@ -66,7 +65,6 @@ Пристрої Керування вашими пристроями. Реакції - Реагування на події та дії. Ваш пристрій Опції сумісності Не чіпай, якщо все працює ;) diff --git a/app/src/main/res/values-ur-rIN/strings.xml b/app/src/main/res/values-ur-rIN/strings.xml index 4ad511ab..9dd39f76 100644 --- a/app/src/main/res/values-ur-rIN/strings.xml +++ b/app/src/main/res/values-ur-rIN/strings.xml @@ -21,7 +21,6 @@ CAPod ایک اکیلے شخص نے بنائی ہے۔ اپگریڈ کرنے سے اضافی خصوصیات ملتی ہیں اور ایپ کو زندہ رکھنے میں مدد ملتی ہے۔ تھیم کی تخصیص خودکار چلائیں اور خودکار رکیں - خودکار کنیکٹ کیس کھلنے اور کنیکشن پر پاپ اپ ہوم اسکرین ویجٹس ڈیوائس کی اعلی ترتیبات @@ -66,7 +65,6 @@ آلات اپنے آلات کا نظم کریں۔ رد عمل - واقعات اور رویوں پر رد عمل ظاہر کریں۔ آپ کا آلہ مطابقت کے اختیارات اگر سب کچھ کام کر رہا ہو تو ہاتھ نہ لگائیں ؛) diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index 1ecd6a59..a49fc54a 100644 --- a/app/src/main/res/values-uz/strings.xml +++ b/app/src/main/res/values-uz/strings.xml @@ -21,7 +21,6 @@ CAPod bitta shaxs tomonidan ishlab chiqilgan. Yangilash qo\'shimcha funksiyalarni ochadi va ilovani tirik saqlashga yordam beradi. Mavzu sozlamalari Avtomatik ijro & avtomatik to\'xtatish - Avtomatik ulanish Quti ochilganda & ulanishda qalqib chiquvchi oyna Asosiy ekran vidjetlari Qurilmaning kengaytirilgan sozlamalari @@ -66,7 +65,6 @@ Qurilmalar Qurilmalaringizni boshqaring. Reaksiyalar - Voqealar va xatti-harakatlarga munosabat bildiring. Sizning qurilmangiz Moslik parametrlari Agar hamma narsa ishlayotgan bo\'lsa, tegmang ;) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 4aee7cc8..ead53d87 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -21,7 +21,6 @@ CAPod được phát triển bởi một người duy nhất. Việc nâng cấp sẽ mở khóa các tính năng bổ sung và giúp duy trì ứng dụng. Tùy chỉnh giao diện Tự động phát & tự động tạm dừng - Tự động kết nối Cửa sổ bật lên khi mở hộp & kết nối Widget màn hình chính Cài đặt thiết bị nâng cao @@ -66,7 +65,6 @@ Thiết bị Quản lý thiết bị của bạn. Phản ứng - Phản ứng với các sự kiện và hành vi. Thiết bị của bạn Tùy chọn tương thích Đừng chạm vào nếu mọi thứ hoạt động ;) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 987e5e81..d669cf6f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -21,7 +21,6 @@ CAPod 由一个人开发。升级可解锁额外功能,并帮助应用持续运营。 主题定制 自动播放 & 自动暂停 - 自动连接 打开耳机盒 & 连接时弹出提示 主屏幕小组件 高级设备设置 @@ -66,7 +65,6 @@ 设备 管理您的设备。 动作 - 响应事件和行为的动作。 您的设备 兼容性选项 如果一切正常,请不要修改 ;) diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 6f624fc8..4745c9b3 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -21,7 +21,6 @@ CAPod 由單人開發。升級可解鎖額外功能,並協助維持應用程式的運作。 佈景主題自訂 自動播放及自動暫停 - 自動連線 開啟充電盒及連線時顯示彈出視窗 主畫面小工具 進階裝置設定 @@ -66,7 +65,6 @@ 裝置 管理您的裝置。 反應 - 對事件和行為作出反應。 您的裝置 相容性選項 若一切如常,就不要觸碰 ;) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index aec9f390..13df0390 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -21,7 +21,6 @@ CAPod 由個人獨立開發。升級可解鎖額外功能,並幫助維持應用程式的持續運營。 主題自訂 自動播放與自動暫停 - 自動連線 開蓋與連線時顯示彈出視窗 主畫面小工具 進階裝置設定 @@ -66,7 +65,6 @@ 裝置 管理你的裝置。 反應 - 對事件和行為作出反應。 您的裝置 相容性選項 若一切如常,就不要觸碰 ;) diff --git a/app/src/main/res/values-zu/strings.xml b/app/src/main/res/values-zu/strings.xml index 9d436406..b41ba4e8 100644 --- a/app/src/main/res/values-zu/strings.xml +++ b/app/src/main/res/values-zu/strings.xml @@ -21,7 +21,6 @@ I-CAPod yenziwa umuntu oyedwa. Ukuthuthuka kuvula izici ezithe thula futhi kusiza ukugcina uhlelo lokusebenza luphila. Ukuhlelwa kwetheme Ukudlala ngokwezinhlelo & ukumisa ngokwezinhlelo - Xhuma ngokuzenzakalela Ividiyo yokuvula indwangu & ukuxhuma Amawijethi esikrini sasekhaya Izilungiselelo ezithuthukisiwe zedivayisi @@ -66,7 +65,6 @@ Amadivayisi Phatha amadivayisi akho. Ukusabela - Sabela ezenzakalweni nasekuziphatheni. Idivayisi yakho Izinketho zokuhambisana Ungathinti uma konke kusebenza ;) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f892b17d..2ed64fa2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,7 +23,6 @@ CAPod is developed by a single person. Upgrading unlocks extra features and helps keep the app alive. Theme customization Auto-play & auto-pause - Auto-connect Popup on case open & connection Home screen widgets Advanced device settings @@ -71,7 +70,6 @@ Devices Manage your devices. Reactions - React to events and behaviors. Your device Compatibility options @@ -483,8 +481,6 @@ Auto Right Left - Automatic Ear Detection - Pause audio when AirPods are removed from ears Noise Control Cycle Select which modes cycle when pressing and holding the stem Off diff --git a/app/src/screenshotTest/kotlin/eu/darken/capod/screenshots/PlayStoreScreenshots.kt b/app/src/screenshotTest/kotlin/eu/darken/capod/screenshots/PlayStoreScreenshots.kt index 18bd0ee6..ede3012e 100644 --- a/app/src/screenshotTest/kotlin/eu/darken/capod/screenshots/PlayStoreScreenshots.kt +++ b/app/src/screenshotTest/kotlin/eu/darken/capod/screenshots/PlayStoreScreenshots.kt @@ -32,12 +32,7 @@ fun AddProfile() = AddProfileContent() @PreviewTest @PlayStoreLocales @Composable -fun SettingsIndex() = SettingsIndexContent() - -@PreviewTest -@PlayStoreLocales -@Composable -fun ReactionSettings() = ReactionSettingsContent() +fun DeviceSettingsReactions() = DeviceSettingsReactionsContent() @PreviewTest @PlayStoreLocales diff --git a/app/src/test/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModelTest.kt b/app/src/test/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModelTest.kt index 00a13b89..ca8bbf88 100644 --- a/app/src/test/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModelTest.kt +++ b/app/src/test/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModelTest.kt @@ -4,10 +4,12 @@ import eu.darken.capod.common.bluetooth.BluetoothAddress import eu.darken.capod.common.bluetooth.BluetoothDevice2 import eu.darken.capod.common.bluetooth.BluetoothManager2 import eu.darken.capod.common.upgrade.UpgradeRepo +import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.monitor.core.DeviceMonitor import eu.darken.capod.monitor.core.PodDevice import eu.darken.capod.pods.core.apple.aap.AapConnectionManager import eu.darken.capod.pods.core.apple.aap.protocol.AapCommand +import eu.darken.capod.profiles.core.DeviceProfilesRepo import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf import io.mockk.Called @@ -46,6 +48,8 @@ class DeviceSettingsViewModelTest : BaseTest() { private lateinit var aapManager: AapConnectionManager private lateinit var upgradeRepo: UpgradeRepo private lateinit var bluetoothManager: BluetoothManager2 + private lateinit var profilesRepo: DeviceProfilesRepo + private lateinit var generalSettings: GeneralSettings private lateinit var devicesFlow: MutableStateFlow> private lateinit var upgradeInfoFlow: MutableStateFlow @@ -63,8 +67,14 @@ class DeviceSettingsViewModelTest : BaseTest() { every { it.isPro } returns false }) + // Synthesize a PodDevice for forceConnect/sendInternal lookups (currentAddress()). + val syntheticDevice = mockk().also { + every { it.profileId } returns testAddress + every { it.address } returns testAddress + } deviceMonitor = mockk().also { every { it.devices } returns devicesFlow + coEvery { it.getDeviceForProfile(testAddress) } returns syntheticDevice } aapManager = mockk(relaxed = true) upgradeRepo = mockk().also { @@ -74,6 +84,8 @@ class DeviceSettingsViewModelTest : BaseTest() { every { isNudgeAvailable } returns true every { bondedDevices() } returns flowOf(emptySet()) } + profilesRepo = mockk(relaxed = true) + generalSettings = mockk(relaxed = true) } @AfterEach @@ -87,6 +99,8 @@ class DeviceSettingsViewModelTest : BaseTest() { aapManager = aapManager, upgradeRepo = upgradeRepo, bluetoothManager = bluetoothManager, + profilesRepo = profilesRepo, + generalSettings = generalSettings, ) @Test diff --git a/app/src/test/java/eu/darken/capod/monitor/core/DeviceMonitorTest.kt b/app/src/test/java/eu/darken/capod/monitor/core/DeviceMonitorTest.kt index 01cc3452..58610d3f 100644 --- a/app/src/test/java/eu/darken/capod/monitor/core/DeviceMonitorTest.kt +++ b/app/src/test/java/eu/darken/capod/monitor/core/DeviceMonitorTest.kt @@ -312,7 +312,7 @@ class DeviceMonitorTest : BaseTest() { } @Test - fun `getDeviceForProfile no BLE no cache no AAP - returns null`() = runTest(testDispatcher) { + fun `getDeviceForProfile no BLE no cache no AAP - returns synthesized bare device`() = runTest(testDispatcher) { val monitor = createMonitor( ble = emptyList(), aap = emptyMap(), @@ -323,6 +323,27 @@ class DeviceMonitorTest : BaseTest() { val device = monitor.getDeviceForProfile(testProfile.id) + // Pristine profile must still be reachable so the Reactions section in + // DeviceSettings can be edited even before the device has ever been observed. + device shouldNotBe null + device!!.profileId shouldBe testProfile.id + device.reactions shouldBe testProfile.reactionConfig + device.ble shouldBe null + device.isLive shouldBe false + } + + @Test + fun `getDeviceForProfile profile not in repo - returns null`() = runTest(testDispatcher) { + val monitor = createMonitor( + ble = emptyList(), + aap = emptyMap(), + cache = emptyMap(), + profiles = emptyList(), + scope = backgroundScope, + ) + + val device = monitor.getDeviceForProfile("nonexistent-profile-id") + device shouldBe null } diff --git a/app/src/test/java/eu/darken/capod/profiles/core/DeviceProfilesRepoReorderTest.kt b/app/src/test/java/eu/darken/capod/profiles/core/DeviceProfilesRepoReorderTest.kt index 0344128c..4751b38c 100644 --- a/app/src/test/java/eu/darken/capod/profiles/core/DeviceProfilesRepoReorderTest.kt +++ b/app/src/test/java/eu/darken/capod/profiles/core/DeviceProfilesRepoReorderTest.kt @@ -21,6 +21,7 @@ class DeviceProfilesRepoReorderTest : BaseTest() { val settings = mockk { every { profiles } returns fakeProfiles.mock every { defaultProfileCreated } returns FakeDataStoreValue(true).mock + every { reactionMigrationDone } returns FakeDataStoreValue(true).mock } val repo = DeviceProfilesRepo( @@ -29,6 +30,7 @@ class DeviceProfilesRepoReorderTest : BaseTest() { generalSettings = mockk(relaxed = true), settings = settings, deviceStateCache = mockk(relaxed = true), + json = kotlinx.serialization.json.Json { ignoreUnknownKeys = true }, ) return repo to fakeProfiles diff --git a/app/src/test/java/eu/darken/capod/profiles/core/LegacyReactionSettingsReaderTest.kt b/app/src/test/java/eu/darken/capod/profiles/core/LegacyReactionSettingsReaderTest.kt new file mode 100644 index 00000000..5c7141e4 --- /dev/null +++ b/app/src/test/java/eu/darken/capod/profiles/core/LegacyReactionSettingsReaderTest.kt @@ -0,0 +1,74 @@ +package eu.darken.capod.profiles.core + +import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition +import io.kotest.matchers.shouldBe +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +/** + * Verifies the legacy reaction DataStore key names and JSON serialization format. + * A key-name mismatch would silently zero all reaction settings for upgrading users. + */ +class LegacyReactionSettingsReaderTest : BaseTest() { + + /** + * The keys that were used by the now-deleted `ReactionSettings` class. + * If any of these change in `LegacyReactionSettingsReader`, upgrading users lose their settings. + * This test acts as a frozen snapshot — DO NOT update these values to "fix" the test. + */ + @Test + fun `legacy key names match the deleted ReactionSettings class`() { + // Boolean keys — frozen snapshot of the old ReactionSettings key names. + // If LegacyReactionSettingsReader keys change, upgrading users silently lose settings. + LegacyReactionSettingsReader.AUTO_PAUSE_KEY.name shouldBe "reaction.autopause.enabled" + LegacyReactionSettingsReader.AUTO_PLAY_KEY.name shouldBe "reaction.autoplay.enabled" + LegacyReactionSettingsReader.AUTO_CONNECT_KEY.name shouldBe "reaction.autoconnect.enabled" + LegacyReactionSettingsReader.POPUP_CASE_OPEN_KEY.name shouldBe "reaction.popup.caseopen" + LegacyReactionSettingsReader.POPUP_CONNECTED_KEY.name shouldBe "reaction.popup.connected" + LegacyReactionSettingsReader.ONE_POD_KEY.name shouldBe "reaction.onepod.enabled" + + // String key for serialized enum + LegacyReactionSettingsReader.CONDITION_KEY.name shouldBe "reaction.autoconnect.condition" + } + + /** + * Verifies the project Json can round-trip AutoConnectCondition enum values using their + * @SerialName annotations. The legacy DataStore stored these as JSON strings. + */ + @Test + fun `AutoConnectCondition round-trips through project Json`() { + val projectJson = Json { + ignoreUnknownKeys = true + encodeDefaults = true + explicitNulls = false + classDiscriminator = "type" + } + + for (condition in AutoConnectCondition.entries) { + val encoded = projectJson.encodeToString(condition) + val decoded = projectJson.decodeFromString(encoded) + decoded shouldBe condition + } + } + + /** + * Verifies the serialized form matches what the deleted ReactionSettings class stored. + * The old class used `createValue(key, default, json, onErrorFallbackToDefault = true)` + * which serializes the enum to its @SerialName value as a JSON string. + */ + @Test + fun `AutoConnectCondition serialized format matches legacy storage`() { + val json = Json { + ignoreUnknownKeys = true + encodeDefaults = true + explicitNulls = false + classDiscriminator = "type" + } + + json.encodeToString(AutoConnectCondition.WHEN_SEEN) shouldBe "\"autoconnect.condition.seen\"" + json.encodeToString(AutoConnectCondition.CASE_OPEN) shouldBe "\"autoconnect.condition.case\"" + json.encodeToString(AutoConnectCondition.IN_EAR) shouldBe "\"autoconnect.condition.inear\"" + } +} diff --git a/app/src/test/java/eu/darken/capod/reaction/core/autoconnect/AutoConnectLogicTest.kt b/app/src/test/java/eu/darken/capod/reaction/core/autoconnect/AutoConnectLogicTest.kt index 0f11600d..ad43f9ed 100644 --- a/app/src/test/java/eu/darken/capod/reaction/core/autoconnect/AutoConnectLogicTest.kt +++ b/app/src/test/java/eu/darken/capod/reaction/core/autoconnect/AutoConnectLogicTest.kt @@ -18,8 +18,6 @@ class AutoConnectLogicTest : BaseTest() { bluetoothManager = mockk(relaxed = true), deviceMonitor = mockk(relaxed = true), generalSettings = mockk(relaxed = true), - reactionSettings = mockk(relaxed = true), - deviceProfilesRepo = mockk(relaxed = true), ) } diff --git a/app/src/test/java/eu/darken/capod/reaction/core/playpause/PlayPauseLogicTest.kt b/app/src/test/java/eu/darken/capod/reaction/core/playpause/PlayPauseLogicTest.kt index 0aecba39..d8f42b29 100644 --- a/app/src/test/java/eu/darken/capod/reaction/core/playpause/PlayPauseLogicTest.kt +++ b/app/src/test/java/eu/darken/capod/reaction/core/playpause/PlayPauseLogicTest.kt @@ -18,7 +18,6 @@ class PlayPauseLogicTest : BaseTest() { playPause = PlayPause( deviceMonitor = mockk(relaxed = true), bluetoothManager = mockk(relaxed = true), - reactionSettings = mockk(relaxed = true), mediaControl = mockk(relaxed = true) ) } diff --git a/app/src/test/java/eu/darken/capod/reaction/core/popup/PopUpReactionLogicTest.kt b/app/src/test/java/eu/darken/capod/reaction/core/popup/PopUpReactionLogicTest.kt index 6a9d564a..71e56ad6 100644 --- a/app/src/test/java/eu/darken/capod/reaction/core/popup/PopUpReactionLogicTest.kt +++ b/app/src/test/java/eu/darken/capod/reaction/core/popup/PopUpReactionLogicTest.kt @@ -18,7 +18,6 @@ class PopUpReactionLogicTest : BaseTest() { fun setup() { popUpReaction = PopUpReaction( deviceMonitor = mockk(relaxed = true), - reactionSettings = mockk(relaxed = true), bluetoothManager = mockk(relaxed = true), ) } diff --git a/app/src/test/java/eu/darken/capod/reaction/ui/ReactionSettingsViewModelTest.kt b/app/src/test/java/eu/darken/capod/reaction/ui/ReactionSettingsViewModelTest.kt deleted file mode 100644 index 666f4d82..00000000 --- a/app/src/test/java/eu/darken/capod/reaction/ui/ReactionSettingsViewModelTest.kt +++ /dev/null @@ -1,321 +0,0 @@ -package eu.darken.capod.reaction.ui - -import eu.darken.capod.common.navigation.Nav -import eu.darken.capod.common.navigation.NavEvent -import eu.darken.capod.common.upgrade.UpgradeRepo -import eu.darken.capod.main.core.GeneralSettings -import eu.darken.capod.main.core.MonitorMode -import eu.darken.capod.reaction.core.ReactionSettings -import eu.darken.capod.reaction.core.autoconnect.AutoConnectCondition -import io.kotest.matchers.shouldBe -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import testhelpers.BaseTest -import testhelpers.coroutine.TestDispatcherProvider -import testhelpers.datastore.FakeDataStoreValue -import testhelpers.livedata.InstantExecutorExtension -import org.junit.jupiter.api.extension.ExtendWith - -@OptIn(ExperimentalCoroutinesApi::class) -@ExtendWith(InstantExecutorExtension::class) -class ReactionSettingsViewModelTest : BaseTest() { - - private val testDispatcher = UnconfinedTestDispatcher() - - private lateinit var upgradeInfoFlow: MutableStateFlow - private lateinit var upgradeRepo: UpgradeRepo - private lateinit var reactionSettings: ReactionSettings - private lateinit var generalSettings: GeneralSettings - - private lateinit var fakeOnePodMode: FakeDataStoreValue - private lateinit var fakeAutoPlay: FakeDataStoreValue - private lateinit var fakeAutoPause: FakeDataStoreValue - private lateinit var fakeAutoConnect: FakeDataStoreValue - private lateinit var fakeAutoConnectCondition: FakeDataStoreValue - private lateinit var fakeShowPopUpOnCaseOpen: FakeDataStoreValue - private lateinit var fakeShowPopUpOnConnection: FakeDataStoreValue - private lateinit var fakeMonitorMode: FakeDataStoreValue - - @BeforeEach - fun setup() { - Dispatchers.setMain(testDispatcher) - - upgradeInfoFlow = MutableStateFlow(mockUpgradeInfo(isPro = false)) - upgradeRepo = mockk().also { - every { it.upgradeInfo } returns upgradeInfoFlow - } - - fakeOnePodMode = FakeDataStoreValue(false) - fakeAutoPlay = FakeDataStoreValue(false) - fakeAutoPause = FakeDataStoreValue(false) - fakeAutoConnect = FakeDataStoreValue(false) - fakeAutoConnectCondition = FakeDataStoreValue(AutoConnectCondition.WHEN_SEEN) - fakeShowPopUpOnCaseOpen = FakeDataStoreValue(false) - fakeShowPopUpOnConnection = FakeDataStoreValue(false) - - reactionSettings = mockk().also { - every { it.onePodMode } returns fakeOnePodMode.mock - every { it.autoPlay } returns fakeAutoPlay.mock - every { it.autoPause } returns fakeAutoPause.mock - every { it.autoConnect } returns fakeAutoConnect.mock - every { it.autoConnectCondition } returns fakeAutoConnectCondition.mock - every { it.showPopUpOnCaseOpen } returns fakeShowPopUpOnCaseOpen.mock - every { it.showPopUpOnConnection } returns fakeShowPopUpOnConnection.mock - } - - fakeMonitorMode = FakeDataStoreValue(MonitorMode.AUTOMATIC) - generalSettings = mockk().also { - every { it.monitorMode } returns fakeMonitorMode.mock - } - } - - @AfterEach - fun teardown() { - Dispatchers.resetMain() - } - - private fun createViewModel() = ReactionSettingsViewModel( - dispatcherProvider = TestDispatcherProvider(testDispatcher), - reactionSettings = reactionSettings, - generalSettings = generalSettings, - upgradeRepo = upgradeRepo, - ) - - private fun mockUpgradeInfo(isPro: Boolean): UpgradeRepo.Info = mockk().also { - every { it.isPro } returns isPro - } - - @Nested - inner class StateTests { - - @Test - fun `state combines all flows correctly`() = runTest(testDispatcher) { - fakeAutoPlay.value = true - fakeAutoPause.value = true - fakeAutoConnect.value = true - fakeOnePodMode.value = true - fakeShowPopUpOnCaseOpen.value = true - fakeShowPopUpOnConnection.value = true - fakeAutoConnectCondition.value = AutoConnectCondition.CASE_OPEN - upgradeInfoFlow.value = mockUpgradeInfo(isPro = true) - - val vm = createViewModel() - val state = vm.state.first() - - state.isPro shouldBe true - state.autoPlay shouldBe true - state.autoPause shouldBe true - state.autoConnect shouldBe true - state.onePodMode shouldBe true - state.showPopUpOnCaseOpen shouldBe true - state.showPopUpOnConnection shouldBe true - state.autoConnectCondition shouldBe AutoConnectCondition.CASE_OPEN - } - } - - @Nested - inner class AutoPlayTests { - - @Test - fun `setAutoPlay true when pro - sets setting`() = runTest(testDispatcher) { - upgradeInfoFlow.value = mockUpgradeInfo(isPro = true) - val vm = createViewModel() - vm.state.first() // ensure state is initialized - - vm.setAutoPlay(true) - - fakeAutoPlay.value shouldBe true - } - - @Test - fun `setAutoPlay true when not pro - navigates to upgrade, does NOT change setting`() = runTest(testDispatcher) { - upgradeInfoFlow.value = mockUpgradeInfo(isPro = false) - val vm = createViewModel() - vm.state.first() - - vm.setAutoPlay(true) - - fakeAutoPlay.value shouldBe false - val event = vm.navEvents.first() - (event as NavEvent.GoTo).destination shouldBe Nav.Main.Upgrade - } - - @Test - fun `setAutoPlay false - always sets (no pro check)`() = runTest(testDispatcher) { - fakeAutoPlay.value = true - val vm = createViewModel() - vm.state.first() - - vm.setAutoPlay(false) - - fakeAutoPlay.value shouldBe false - } - } - - @Nested - inner class AutoPauseTests { - - @Test - fun `setAutoPause true when pro - sets setting`() = runTest(testDispatcher) { - upgradeInfoFlow.value = mockUpgradeInfo(isPro = true) - val vm = createViewModel() - vm.state.first() - - vm.setAutoPause(true) - - fakeAutoPause.value shouldBe true - } - - @Test - fun `setAutoPause true when not pro - navigates to upgrade`() = runTest(testDispatcher) { - upgradeInfoFlow.value = mockUpgradeInfo(isPro = false) - val vm = createViewModel() - vm.state.first() - - vm.setAutoPause(true) - - fakeAutoPause.value shouldBe false - val event = vm.navEvents.first() - (event as NavEvent.GoTo).destination shouldBe Nav.Main.Upgrade - } - - @Test - fun `setAutoPause false - always sets`() = runTest(testDispatcher) { - fakeAutoPause.value = true - val vm = createViewModel() - vm.state.first() - - vm.setAutoPause(false) - - fakeAutoPause.value shouldBe false - } - } - - @Nested - inner class AutoConnectTests { - - @Test - fun `setAutoConnect true when pro - sets setting AND sets monitorMode to ALWAYS`() = runTest(testDispatcher) { - upgradeInfoFlow.value = mockUpgradeInfo(isPro = true) - val vm = createViewModel() - vm.state.first() - - vm.setAutoConnect(true) - - fakeAutoConnect.value shouldBe true - fakeMonitorMode.value shouldBe MonitorMode.ALWAYS - } - - @Test - fun `setAutoConnect true when not pro - navigates to upgrade`() = runTest(testDispatcher) { - upgradeInfoFlow.value = mockUpgradeInfo(isPro = false) - val vm = createViewModel() - vm.state.first() - - vm.setAutoConnect(true) - - fakeAutoConnect.value shouldBe false - val event = vm.navEvents.first() - (event as NavEvent.GoTo).destination shouldBe Nav.Main.Upgrade - } - - @Test - fun `setAutoConnect false - sets setting, does NOT change monitorMode`() = runTest(testDispatcher) { - fakeAutoConnect.value = true - fakeMonitorMode.value = MonitorMode.ALWAYS - val vm = createViewModel() - vm.state.first() - - vm.setAutoConnect(false) - - fakeAutoConnect.value shouldBe false - fakeMonitorMode.value shouldBe MonitorMode.ALWAYS - } - } - - @Nested - inner class PopUpTests { - - @Test - fun `setShowPopUpOnCaseOpen true when pro - sets setting`() = runTest(testDispatcher) { - upgradeInfoFlow.value = mockUpgradeInfo(isPro = true) - val vm = createViewModel() - vm.state.first() - - vm.setShowPopUpOnCaseOpen(true) - - fakeShowPopUpOnCaseOpen.value shouldBe true - } - - @Test - fun `setShowPopUpOnCaseOpen true when not pro - navigates to upgrade`() = runTest(testDispatcher) { - upgradeInfoFlow.value = mockUpgradeInfo(isPro = false) - val vm = createViewModel() - vm.state.first() - - vm.setShowPopUpOnCaseOpen(true) - - fakeShowPopUpOnCaseOpen.value shouldBe false - val event = vm.navEvents.first() - (event as NavEvent.GoTo).destination shouldBe Nav.Main.Upgrade - } - - @Test - fun `setShowPopUpOnConnection true when pro - sets setting`() = runTest(testDispatcher) { - upgradeInfoFlow.value = mockUpgradeInfo(isPro = true) - val vm = createViewModel() - vm.state.first() - - vm.setShowPopUpOnConnection(true) - - fakeShowPopUpOnConnection.value shouldBe true - } - - @Test - fun `setShowPopUpOnConnection true when not pro - navigates to upgrade`() = runTest(testDispatcher) { - upgradeInfoFlow.value = mockUpgradeInfo(isPro = false) - val vm = createViewModel() - vm.state.first() - - vm.setShowPopUpOnConnection(true) - - fakeShowPopUpOnConnection.value shouldBe false - val event = vm.navEvents.first() - (event as NavEvent.GoTo).destination shouldBe Nav.Main.Upgrade - } - } - - @Nested - inner class DirectSettingTests { - - @Test - fun `setOnePodMode - direct assignment (no pro check)`() = runTest(testDispatcher) { - val vm = createViewModel() - - vm.setOnePodMode(true) - - fakeOnePodMode.value shouldBe true - } - - @Test - fun `setAutoConnectCondition - direct assignment (no pro check)`() = runTest(testDispatcher) { - val vm = createViewModel() - - vm.setAutoConnectCondition(AutoConnectCondition.IN_EAR) - - fakeAutoConnectCondition.value shouldBe AutoConnectCondition.IN_EAR - } - } -} diff --git a/fastlane/copy_screenshots.sh b/fastlane/copy_screenshots.sh index 206a046f..44c7c3b9 100755 --- a/fastlane/copy_screenshots.sh +++ b/fastlane/copy_screenshots.sh @@ -27,8 +27,7 @@ declare -A SCREEN_MAP=( [WidgetConfiguration]="4_widget_configuration" [DeviceProfiles]="5_device_profiles" [AddProfile]="6_add_profile" - [SettingsIndex]="7_settings" - [ReactionSettings]="8_reaction_settings" + [DeviceSettingsReactions]="7_device_settings_reactions" ) if [[ ! -d "$REF_DIR" ]]; then diff --git a/fastlane/generate_screenshots.sh b/fastlane/generate_screenshots.sh index 3c455a41..5ff1c598 100755 --- a/fastlane/generate_screenshots.sh +++ b/fastlane/generate_screenshots.sh @@ -14,7 +14,7 @@ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" LOCALES_FILE="$PROJECT_DIR/app/src/screenshotTest/kotlin/eu/darken/capod/screenshots/PlayStoreLocales.kt" REF_DIR="$PROJECT_DIR/app/src/screenshotTestGplayDebug/reference" -# Default batch size — 2 locales × 8 composables = 16 renders per batch. +# Default batch size — 2 locales × 7 composables = 14 renders per batch. # Kept small to avoid layoutlib OOM (leaks ~10MB per rendered image at 1080p). BATCH_SIZE=2 SMOKE=false @@ -211,7 +211,7 @@ done # Count final results FINAL_COUNT=$(find "$REF_DIR" -name "*.png" 2>/dev/null | wc -l) -EXPECTED=$(( TOTAL * 8 )) # 8 composables per locale +EXPECTED=$(( TOTAL * 7 )) # 7 composables per locale echo "=== Generation Complete ===" echo "Generated: $FINAL_COUNT images (expected: $EXPECTED)"