diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepository.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepository.kt index 8de42a398..89b07c87a 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepository.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepository.kt @@ -13,4 +13,6 @@ interface WearPrefsRepository { suspend fun setWearHapticFeedback(enabled: Boolean) suspend fun getWearToastConfirmation(): Boolean suspend fun setWearToastConfirmation(enabled: Boolean) + suspend fun getWearFavoritesOnly(): Boolean + suspend fun setWearFavoritesOnly(enabled: Boolean) } diff --git a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepositoryImpl.kt b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepositoryImpl.kt index d865fd0cb..fc8d8398f 100644 --- a/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepositoryImpl.kt +++ b/common/src/main/java/io/homeassistant/companion/android/common/data/prefs/WearPrefsRepositoryImpl.kt @@ -21,6 +21,7 @@ class WearPrefsRepositoryImpl @Inject constructor( private const val PREF_TILE_TEMPLATE_REFRESH_INTERVAL = "tile_template_refresh_interval" private const val PREF_WEAR_HAPTIC_FEEDBACK = "wear_haptic_feedback" private const val PREF_WEAR_TOAST_CONFIRMATION = "wear_toast_confirmation" + private const val PREF_WEAR_FAVORITES_ONLY = "wear_favorites_only" } init { @@ -101,4 +102,12 @@ class WearPrefsRepositoryImpl @Inject constructor( override suspend fun setWearToastConfirmation(enabled: Boolean) { localStorage.putBoolean(PREF_WEAR_TOAST_CONFIRMATION, enabled) } + + override suspend fun getWearFavoritesOnly(): Boolean { + return localStorage.getBoolean(PREF_WEAR_FAVORITES_ONLY) + } + + override suspend fun setWearFavoritesOnly(enabled: Boolean) { + localStorage.putBoolean(PREF_WEAR_FAVORITES_ONLY, enabled) + } } diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 6df0f184e..4505ddf59 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -1015,4 +1015,5 @@ Assist Please launch the Home Assistant app and login before you can use the assist feature. HA: Assist + Only Show Favorites diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/HomeActivity.kt b/wear/src/main/java/io/homeassistant/companion/android/home/HomeActivity.kt index a6b914e78..d2e552f46 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/HomeActivity.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/HomeActivity.kt @@ -57,9 +57,11 @@ class HomeActivity : ComponentActivity(), HomeView { entityUpdateJob = launch { mainViewModel.entityUpdates() } } } - launch { mainViewModel.areaUpdates() } - launch { mainViewModel.deviceUpdates() } launch { mainViewModel.entityRegistryUpdates() } + if (!mainViewModel.isFavoritesOnly) { + launch { mainViewModel.areaUpdates() } + launch { mainViewModel.deviceUpdates() } + } } } } diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenter.kt b/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenter.kt index 386fce12e..700f0071c 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenter.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenter.kt @@ -50,4 +50,6 @@ interface HomePresenter { suspend fun setTemplateTileContent(content: String) suspend fun getTemplateTileRefreshInterval(): Int suspend fun setTemplateTileRefreshInterval(interval: Int) + suspend fun getWearFavoritesOnly(): Boolean + suspend fun setWearFavoritesOnly(enabled: Boolean) } diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt b/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt index 5311285ab..9f94548f3 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/HomePresenterImpl.kt @@ -271,4 +271,12 @@ class HomePresenterImpl @Inject constructor( override suspend fun setTemplateTileRefreshInterval(interval: Int) { wearPrefsRepository.setTemplateTileRefreshInterval(interval) } + + override suspend fun getWearFavoritesOnly(): Boolean { + return wearPrefsRepository.getWearFavoritesOnly() + } + + override suspend fun setWearFavoritesOnly(enabled: Boolean) { + wearPrefsRepository.setWearFavoritesOnly(enabled) + } } diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/MainViewModel.kt b/wear/src/main/java/io/homeassistant/companion/android/home/MainViewModel.kt index 3f0911aec..7dd74f80c 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/MainViewModel.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/MainViewModel.kt @@ -3,9 +3,11 @@ package io.homeassistant.companion.android.home import android.app.Application import android.util.Log import androidx.compose.runtime.State +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope @@ -108,6 +110,8 @@ class MainViewModel @Inject constructor( private set var templateTileRefreshInterval = mutableStateOf(0) private set + var isFavoritesOnly by mutableStateOf(false) + private set fun supportedDomains(): List = HomePresenterImpl.supportedDomains @@ -129,6 +133,7 @@ class MainViewModel @Inject constructor( isShowShortcutTextEnabled.value = homePresenter.getShowShortcutText() templateTileContent.value = homePresenter.getTemplateTileContent() templateTileRefreshInterval.value = homePresenter.getTemplateTileRefreshInterval() + isFavoritesOnly = homePresenter.getWearFavoritesOnly() } } @@ -176,11 +181,13 @@ class MainViewModel @Inject constructor( val getEntityRegistry = async { homePresenter.getEntityRegistry() } val getEntities = async { homePresenter.getEntities() } - areaRegistry = getAreaRegistry.await()?.also { - areas.clear() - areas.addAll(it) + if (!isFavoritesOnly) { + areaRegistry = getAreaRegistry.await()?.also { + areas.clear() + areas.addAll(it) + } + deviceRegistry = getDeviceRegistry.await() } - deviceRegistry = getDeviceRegistry.await() entityRegistry = getEntityRegistry.await() _supportedEntities.value = getSupportedEntities() @@ -189,7 +196,8 @@ class MainViewModel @Inject constructor( entities.clear() it.forEach { state -> updateEntityStates(state) } } - updateEntityDomains() + if (!isFavoritesOnly) + updateEntityDomains() } suspend fun entityUpdates() { @@ -197,12 +205,13 @@ class MainViewModel @Inject constructor( return homePresenter.getEntityUpdates(supportedEntities.value)?.collect { updateEntityStates(it) - updateEntityDomains() + if (!isFavoritesOnly) + updateEntityDomains() } } suspend fun areaUpdates() { - if (!homePresenter.isConnected()) + if (!homePresenter.isConnected() || isFavoritesOnly) return homePresenter.getAreaRegistryUpdates()?.collect { areaRegistry = homePresenter.getAreaRegistry() @@ -215,7 +224,7 @@ class MainViewModel @Inject constructor( } suspend fun deviceUpdates() { - if (!homePresenter.isConnected()) + if (!homePresenter.isConnected() || isFavoritesOnly) return homePresenter.getDeviceRegistryUpdates()?.collect { deviceRegistry = homePresenter.getDeviceRegistry() @@ -414,6 +423,13 @@ class MainViewModel @Inject constructor( } } + fun setWearFavoritesOnly(enabled: Boolean) { + viewModelScope.launch { + homePresenter.setWearFavoritesOnly(enabled) + isFavoritesOnly = enabled + } + } + fun setTemplateTileRefreshInterval(interval: Int) { viewModelScope.launch { homePresenter.setTemplateTileRefreshInterval(interval) diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/views/HomeView.kt b/wear/src/main/java/io/homeassistant/companion/android/home/views/HomeView.kt index 1137e2481..f53503e26 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/views/HomeView.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/views/HomeView.kt @@ -59,7 +59,7 @@ fun LoadHomePage( }, onRetryLoadEntitiesClicked = mainViewModel::loadEntities, onSettingsClicked = { swipeDismissableNavController.navigate(SCREEN_SETTINGS) }, - onTestClicked = { lists, order, filter -> + onNavigationClicked = { lists, order, filter -> mainViewModel.entityLists.clear() mainViewModel.entityLists.putAll(lists) mainViewModel.entityListsOrder.clear() @@ -68,8 +68,7 @@ fun LoadHomePage( swipeDismissableNavController.navigate(SCREEN_ENTITY_LIST) }, isHapticEnabled = mainViewModel.isHapticEnabled.value, - isToastEnabled = mainViewModel.isToastEnabled.value, - deleteFavorite = { id -> mainViewModel.removeFavoriteEntity(id) } + isToastEnabled = mainViewModel.isToastEnabled.value ) } composable("$SCREEN_ENTITY_DETAIL/{entityId}") { @@ -141,8 +140,10 @@ fun LoadHomePage( onClickLogout = { mainViewModel.logout() }, isHapticEnabled = mainViewModel.isHapticEnabled.value, isToastEnabled = mainViewModel.isToastEnabled.value, + isFavoritesOnly = mainViewModel.isFavoritesOnly, onHapticEnabled = { mainViewModel.setHapticEnabled(it) }, - onToastEnabled = { mainViewModel.setToastEnabled(it) } + onToastEnabled = { mainViewModel.setToastEnabled(it) }, + setFavoritesOnly = { mainViewModel.setWearFavoritesOnly(it) } ) { swipeDismissableNavController.navigate(SCREEN_SET_TILE_TEMPLATE) } } composable(SCREEN_SET_FAVORITES) { diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/views/MainView.kt b/wear/src/main/java/io/homeassistant/companion/android/home/views/MainView.kt index 09300bb24..f696b0a31 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/views/MainView.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/views/MainView.kt @@ -54,10 +54,9 @@ fun MainView( onEntityLongClicked: (String) -> Unit, onRetryLoadEntitiesClicked: () -> Unit, onSettingsClicked: () -> Unit, - onTestClicked: (entityLists: Map>>, listOrder: List, filter: (Entity<*>) -> (Boolean)) -> Unit, + onNavigationClicked: (entityLists: Map>>, listOrder: List, filter: (Entity<*>) -> Boolean) -> Unit, isHapticEnabled: Boolean, - isToastEnabled: Boolean, - deleteFavorite: (String) -> Unit + isToastEnabled: Boolean ) { val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState() @@ -123,195 +122,218 @@ fun MainView( isHapticEnabled, isToastEnabled ) { entityId -> onEntityLongClicked(entityId) } - } ?: deleteFavorite(favoriteEntityID) + } } } } } - when (mainViewModel.loadingState.value) { - MainViewModel.LoadingState.LOADING -> { - if (favoriteEntityIds.isEmpty()) { - // Add a Spacer to prevent settings being pushed to the screen center - item { Spacer(modifier = Modifier.fillMaxWidth()) } - } - item { - val minHeight = - if (favoriteEntityIds.isEmpty()) LocalConfiguration.current.screenHeightDp - 64 - else 0 - Column( - modifier = Modifier - .heightIn(min = minHeight.dp) - .fillMaxSize() - .padding(vertical = if (favoriteEntityIds.isEmpty()) 0.dp else 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - ListHeader(id = commonR.string.loading) - CircularProgressIndicator() + if (!mainViewModel.isFavoritesOnly) { + when (mainViewModel.loadingState.value) { + MainViewModel.LoadingState.LOADING -> { + if (favoriteEntityIds.isEmpty()) { + // Add a Spacer to prevent settings being pushed to the screen center + item { Spacer(modifier = Modifier.fillMaxWidth()) } + } + item { + val minHeight = + if (favoriteEntityIds.isEmpty()) LocalConfiguration.current.screenHeightDp - 64 + else 0 + Column( + modifier = Modifier + .heightIn(min = minHeight.dp) + .fillMaxSize() + .padding(vertical = if (favoriteEntityIds.isEmpty()) 0.dp else 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + ListHeader(id = commonR.string.loading) + CircularProgressIndicator() + } } } - } - MainViewModel.LoadingState.ERROR -> { - item { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - ListHeader(id = commonR.string.error_loading_entities) - Chip( - label = { - Text( - text = stringResource(commonR.string.retry), - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - }, - onClick = onRetryLoadEntitiesClicked, - colors = ChipDefaults.primaryChipColors() - ) - Spacer(modifier = Modifier.height(32.dp)) - } - } - } - MainViewModel.LoadingState.READY -> { - if (mainViewModel.entities.isEmpty()) { + MainViewModel.LoadingState.ERROR -> { item { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - Text( - text = stringResource(commonR.string.no_supported_entities), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.title3, - modifier = Modifier.fillMaxWidth() - .padding(top = 32.dp) - ) - Text( - text = stringResource(commonR.string.no_supported_entities_summary), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.body2, - modifier = Modifier.fillMaxWidth() - .padding(top = 8.dp) + ListHeader(id = commonR.string.error_loading_entities) + Chip( + label = { + Text( + text = stringResource(commonR.string.retry), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + }, + onClick = onRetryLoadEntitiesClicked, + colors = ChipDefaults.primaryChipColors() ) + Spacer(modifier = Modifier.height(32.dp)) } } } + MainViewModel.LoadingState.READY -> { + if (mainViewModel.entities.isEmpty()) { + item { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(commonR.string.no_supported_entities), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.title3, + modifier = Modifier.fillMaxWidth() + .padding(top = 32.dp) + ) + Text( + text = stringResource(commonR.string.no_supported_entities_summary), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.body2, + modifier = Modifier.fillMaxWidth() + .padding(top = 8.dp) + ) + } + } + } - if (mainViewModel.entitiesByArea.values.any { - it.isNotEmpty() && it.any { entity -> - mainViewModel.getCategoryForEntity(entity.entityId) == null && - mainViewModel.getHiddenByForEntity(entity.entityId) == null + if ( + mainViewModel.entitiesByArea.values.any { + it.isNotEmpty() && it.any { entity -> + mainViewModel.getCategoryForEntity(entity.entityId) == null && + mainViewModel.getHiddenByForEntity(entity.entityId) == null + } + } + ) { + item { + ListHeader(id = commonR.string.areas) + } + for (id in mainViewModel.entitiesByAreaOrder) { + val entities = mainViewModel.entitiesByArea[id] + val entitiesToShow = entities?.filter { + mainViewModel.getCategoryForEntity(it.entityId) == null && + mainViewModel.getHiddenByForEntity(it.entityId) == null + } + if (!entitiesToShow.isNullOrEmpty()) { + val area = mainViewModel.areas.first { it.areaId == id } + item { + Chip( + modifier = Modifier.fillMaxWidth(), + label = { + Text(text = area.name) + }, + onClick = { + onNavigationClicked( + mapOf(area.name to entities), + listOf(area.name) + ) { + mainViewModel.getCategoryForEntity(it.entityId) == null && + mainViewModel.getHiddenByForEntity( + it.entityId + ) == null + } + }, + colors = ChipDefaults.primaryChipColors() + ) + } + } + } } - } - ) { - item { - ListHeader(id = commonR.string.areas) - } - for (id in mainViewModel.entitiesByAreaOrder) { - val entities = mainViewModel.entitiesByArea[id] - val entitiesToShow = entities?.filter { - mainViewModel.getCategoryForEntity(it.entityId) == null && + + val domainEntitiesFilter: (entity: Entity<*>) -> Boolean = + { + mainViewModel.getAreaForEntity(it.entityId) == null && + mainViewModel.getCategoryForEntity(it.entityId) == null && mainViewModel.getHiddenByForEntity(it.entityId) == null } - if (!entitiesToShow.isNullOrEmpty()) { - val area = mainViewModel.areas.first { it.areaId == id } + if (mainViewModel.entities.values.any(domainEntitiesFilter)) { + item { + ListHeader(id = commonR.string.more_entities) + } + } + // Buttons for each existing category + for (domain in mainViewModel.entitiesByDomainOrder) { + val domainEntities = mainViewModel.entitiesByDomain[domain]!! + val domainEntitiesToShow = + domainEntities.filter(domainEntitiesFilter) + if (domainEntitiesToShow.isNotEmpty()) { item { Chip( modifier = Modifier.fillMaxWidth(), + icon = { + getIcon( + "", + domain, + context + )?.let { Image(asset = it) } + }, label = { - Text(text = area.name) + Text(text = mainViewModel.stringForDomain(domain)!!) }, onClick = { - onTestClicked( - mapOf(area.name to entities), - listOf(area.name) - ) { - mainViewModel.getCategoryForEntity(it.entityId) == null && - mainViewModel.getHiddenByForEntity(it.entityId) == null - } + onNavigationClicked( + mapOf( + mainViewModel.stringForDomain(domain)!! to domainEntities + ), + listOf(mainViewModel.stringForDomain(domain)!!), + domainEntitiesFilter + ) }, colors = ChipDefaults.primaryChipColors() ) } } } - } - val domainEntitiesFilter: (entity: Entity<*>) -> Boolean = - { - mainViewModel.getAreaForEntity(it.entityId) == null && - mainViewModel.getCategoryForEntity(it.entityId) == null && - mainViewModel.getHiddenByForEntity(it.entityId) == null - } - if (mainViewModel.entities.values.any(domainEntitiesFilter)) { item { - ListHeader(id = commonR.string.more_entities) + Spacer(modifier = Modifier.height(32.dp)) } - } - // Buttons for each existing category - for (domain in mainViewModel.entitiesByDomainOrder) { - val domainEntities = mainViewModel.entitiesByDomain[domain]!! - val domainEntitiesToShow = domainEntities.filter(domainEntitiesFilter) - if (domainEntitiesToShow.isNotEmpty()) { + // All entities regardless of area + if (mainViewModel.entities.isNotEmpty()) { item { Chip( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth(), icon = { - getIcon("", domain, context)?.let { Image(asset = it) } - }, - label = { - Text(text = mainViewModel.stringForDomain(domain)!!) - }, - onClick = { - onTestClicked( - mapOf( - mainViewModel.stringForDomain(domain)!! to domainEntities - ), - listOf(mainViewModel.stringForDomain(domain)!!), - domainEntitiesFilter + Image( + asset = CommunityMaterial.Icon.cmd_animation, + colorFilter = ColorFilter.tint(Color.White) ) }, - colors = ChipDefaults.primaryChipColors() + label = { + Text(text = stringResource(commonR.string.all_entities)) + }, + onClick = { + onNavigationClicked( + mainViewModel.entitiesByDomain.mapKeys { + mainViewModel.stringForDomain( + it.key + )!! + }, + mainViewModel.entitiesByDomain.keys.map { + mainViewModel.stringForDomain( + it + )!! + }.sorted() + ) { true } + }, + colors = ChipDefaults.secondaryChipColors() ) } } } - - item { - Spacer(modifier = Modifier.height(32.dp)) - } - // All entities regardless of area - if (mainViewModel.entities.isNotEmpty()) { - item { - Chip( - modifier = Modifier - .fillMaxWidth(), - icon = { - Image( - asset = CommunityMaterial.Icon.cmd_animation, - colorFilter = ColorFilter.tint(Color.White) - ) - }, - label = { - Text(text = stringResource(commonR.string.all_entities)) - }, - onClick = { - onTestClicked( - mainViewModel.entitiesByDomain.mapKeys { mainViewModel.stringForDomain(it.key)!! }, - mainViewModel.entitiesByDomain.keys.map { mainViewModel.stringForDomain(it)!! }.sorted() - ) { true } - }, - colors = ChipDefaults.secondaryChipColors() - ) - } - } } } + if (mainViewModel.isFavoritesOnly) + item { + Spacer(Modifier.padding(32.dp)) + } + // Settings item { Chip( diff --git a/wear/src/main/java/io/homeassistant/companion/android/home/views/SettingsView.kt b/wear/src/main/java/io/homeassistant/companion/android/home/views/SettingsView.kt index 1935bf497..4e8df3646 100644 --- a/wear/src/main/java/io/homeassistant/companion/android/home/views/SettingsView.kt +++ b/wear/src/main/java/io/homeassistant/companion/android/home/views/SettingsView.kt @@ -68,8 +68,10 @@ fun SettingsView( onClickLogout: () -> Unit, isHapticEnabled: Boolean, isToastEnabled: Boolean, + isFavoritesOnly: Boolean, onHapticEnabled: (Boolean) -> Unit, onToastEnabled: (Boolean) -> Unit, + setFavoritesOnly: (Boolean) -> Unit, onClickTemplateTile: () -> Unit ) { val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState() @@ -105,6 +107,30 @@ fun SettingsView( onClick = onClearFavorites ) } + item { + ToggleChip( + modifier = Modifier.fillMaxWidth(), + checked = isFavoritesOnly, + onCheckedChange = { setFavoritesOnly(it) }, + label = { Text(stringResource(commonR.string.only_favorites)) }, + enabled = favorites.isNotEmpty(), + toggleControl = { + Icon( + imageVector = ToggleChipDefaults.switchIcon(isFavoritesOnly), + contentDescription = if (isFavoritesOnly) + stringResource(commonR.string.enabled) + else + stringResource(commonR.string.disabled) + ) + }, + appIcon = { + Image( + asset = CommunityMaterial.Icon2.cmd_home_heart, + colorFilter = ColorFilter.tint(wearColorPalette.onSurface) + ) + } + ) + } item { ListHeader( id = commonR.string.feedback_settings @@ -245,7 +271,9 @@ private fun PreviewSettingsView() { onClickLogout = {}, isHapticEnabled = true, isToastEnabled = false, + isFavoritesOnly = false, onHapticEnabled = {}, - onToastEnabled = {} + onToastEnabled = {}, + setFavoritesOnly = {} ) {} }