Add new option to only show favorites on home screen (#3276)

* Add new option to only show favorites on home screen

* Review comments

* Review comments
This commit is contained in:
Daniel Shokouhi 2023-02-01 09:04:24 -08:00 committed by GitHub
parent ef1f386ad0
commit 047abe93ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 250 additions and 159 deletions

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -1015,4 +1015,5 @@
<string name="assist">Assist</string>
<string name="not_registered">Please launch the Home Assistant app and login before you can use the assist feature.</string>
<string name="ha_assist">HA: Assist</string>
<string name="only_favorites">Only Show Favorites</string>
</resources>

View file

@ -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() }
}
}
}
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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<String> = 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)

View file

@ -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) {

View file

@ -54,10 +54,9 @@ fun MainView(
onEntityLongClicked: (String) -> Unit,
onRetryLoadEntitiesClicked: () -> Unit,
onSettingsClicked: () -> Unit,
onTestClicked: (entityLists: Map<String, List<Entity<*>>>, listOrder: List<String>, filter: (Entity<*>) -> (Boolean)) -> Unit,
onNavigationClicked: (entityLists: Map<String, List<Entity<*>>>, listOrder: List<String>, 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(

View file

@ -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 = {}
) {}
}