Major Wear Cleanup (#1895)

* Better state hoisting.

* Broke down compose items and removed dependant state.

* Functional minus favorites....

* Favorites working, not my best solution.

* Breaking more stuff down.

* ktlint.
This commit is contained in:
Justin Bassett 2021-11-10 09:35:10 -05:00 committed by GitHub
parent eb3585b305
commit 4fdbe7214a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 726 additions and 673 deletions

View file

@ -6,69 +6,13 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
import androidx.wear.compose.material.ExperimentalWearMaterialApi
import androidx.wear.compose.material.ListHeader
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.PositionIndicator
import androidx.wear.compose.material.Scaffold
import androidx.wear.compose.material.ScalingLazyColumn
import androidx.wear.compose.material.ScalingLazyListState
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.ToggleChip
import androidx.wear.compose.material.ToggleChipDefaults
import androidx.wear.compose.material.rememberScalingLazyListState
import androidx.wear.compose.navigation.SwipeDismissableNavHost
import androidx.wear.compose.navigation.composable
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
import com.mikepenz.iconics.compose.Image
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.DaggerPresenterComponent
import io.homeassistant.companion.android.PresenterModule
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.home.views.LoadHomePage
import io.homeassistant.companion.android.onboarding.OnboardingActivity
import io.homeassistant.companion.android.onboarding.integration.MobileAppIntegrationActivity
import io.homeassistant.companion.android.settings.ScreenSetFavorites
import io.homeassistant.companion.android.settings.ScreenSettings
import io.homeassistant.companion.android.util.LocalRotaryEventDispatcher
import io.homeassistant.companion.android.util.RotaryEventDispatcher
import io.homeassistant.companion.android.util.RotaryEventHandlerSetup
import io.homeassistant.companion.android.util.RotaryEventState
import io.homeassistant.companion.android.util.SetTitle
import io.homeassistant.companion.android.util.getIcon
import io.homeassistant.companion.android.util.setChipDefaults
import io.homeassistant.companion.android.util.updateFavorites
import io.homeassistant.companion.android.viewModels.EntityViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject
class HomeActivity : ComponentActivity(), HomeView {
@ -76,21 +20,10 @@ class HomeActivity : ComponentActivity(), HomeView {
@Inject
lateinit var presenter: HomePresenter
private val entityViewModel by viewModels<EntityViewModel>()
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
var expandedFavorites: Boolean by mutableStateOf(true)
var expandedInputBooleans: Boolean by mutableStateOf(true)
var expandedLights: Boolean by mutableStateOf(true)
var expandedScenes: Boolean by mutableStateOf(true)
var expandedScripts: Boolean by mutableStateOf(true)
var expandedSwitches: Boolean by mutableStateOf(true)
private val mainViewModel by viewModels<MainViewModel>()
companion object {
private const val TAG = "HomeActivity"
private const val SCREEN_LANDING = "landing"
const val SCREEN_SETTINGS = "settings"
const val SCREEN_SET_FAVORITES = "set_favorites"
fun newInstance(context: Context): Intent {
return Intent(context, HomeActivity::class.java)
@ -109,11 +42,11 @@ class HomeActivity : ComponentActivity(), HomeView {
.inject(this)
presenter.onViewReady()
updateEntities()
updateFavorites(entityViewModel, presenter, mainScope)
setContent {
LoadHomePage(entities = entityViewModel.entitiesResponse, entityViewModel.favoriteEntities)
LoadHomePage(mainViewModel)
}
mainViewModel.init(presenter)
}
override fun onDestroy() {
@ -132,388 +65,4 @@ class HomeActivity : ComponentActivity(), HomeView {
startActivity(intent)
finish()
}
@ExperimentalWearMaterialApi
@Composable
private fun LoadHomePage(entities: List<Entity<Any>>, favorites: MutableSet<String>) {
val rotaryEventDispatcher = RotaryEventDispatcher()
if (entities.isNullOrEmpty() && favorites.isNullOrEmpty()) {
Column {
Spacer(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp)
)
SetTitle(id = R.string.loading)
Chip(
modifier = Modifier
.padding(top = 50.dp, start = 10.dp, end = 10.dp),
label = {
Text(
text = stringResource(R.string.loading_entities),
textAlign = TextAlign.Center
)
},
onClick = { /* No op */ },
colors = setChipDefaults()
)
}
} else {
updateFavorites(entityViewModel, presenter, mainScope)
val validEntities =
entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] in HomePresenterImpl.supportedDomains }
val scenes =
entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "scene" }
val scripts =
entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "script" }
val lights =
entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "light" }
val inputBooleans = entities.sortedBy { it.entityId }
.filter { it.entityId.split(".")[0] == "input_boolean" }
val switches =
entities.sortedBy { it.entityId }.filter { it.entityId.split(".")[0] == "switch" }
val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState()
RotaryEventDispatcher(scalingLazyListState)
val swipeDismissableNavController = rememberSwipeDismissableNavController()
MaterialTheme {
Scaffold(
positionIndicator = {
if (scalingLazyListState.isScrollInProgress)
PositionIndicator(scalingLazyListState = scalingLazyListState)
}
) {
CompositionLocalProvider(
LocalRotaryEventDispatcher provides rotaryEventDispatcher
) {
RotaryEventHandlerSetup(rotaryEventDispatcher)
SwipeDismissableNavHost(
navController = swipeDismissableNavController,
startDestination = SCREEN_LANDING
) {
composable(SCREEN_LANDING) {
RotaryEventState(scrollState = scalingLazyListState)
ScalingLazyColumn(
modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(
top = 10.dp,
start = 10.dp,
end = 10.dp,
bottom = 40.dp
),
horizontalAlignment = Alignment.CenterHorizontally,
state = scalingLazyListState
) {
if (favorites.isNotEmpty()) {
item {
SetListHeader(
id = R.string.favorites,
expanded = expandedFavorites
)
}
val favoriteArray = favorites.toTypedArray()
if (expandedFavorites) {
items(favoriteArray.size) { index ->
val favoriteEntityID =
favoriteArray[index].split(",")[0]
val favoriteName =
favoriteArray[index].split(",")[1]
val favoriteIcon =
favoriteArray[index].split(",")[2]
if (entities.isNullOrEmpty()) {
// Use a normal chip when we don't have the state of the entity
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(top = if (index == 0) 0.dp else 10.dp),
icon = {
Image(
asset = getIcon(
favoriteIcon,
favoriteEntityID.split(".")[0],
baseContext
)
?: CommunityMaterial.Icon.cmd_cellphone
)
},
label = {
Text(
text = favoriteName,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
},
onClick = {
presenter.onEntityClicked(
favoriteEntityID
)
},
colors = ChipDefaults.primaryChipColors(
backgroundColor = colorResource(id = R.color.colorAccent),
contentColor = Color.Black
)
)
} else {
for (entity in entities) {
if (entity.entityId == favoriteEntityID) {
SetEntityUI(
entity = entity,
index = index
)
}
}
}
}
}
}
if (entities.isNullOrEmpty()) {
item {
Column {
SetTitle(id = R.string.loading)
Chip(
modifier = Modifier
.padding(
top = 10.dp,
start = 10.dp,
end = 10.dp
),
label = {
Text(
text = stringResource(R.string.loading_entities),
textAlign = TextAlign.Center
)
},
onClick = { /* No op */ },
colors = setChipDefaults()
)
}
}
}
if (inputBooleans.isNotEmpty()) {
item {
SetListHeader(
id = R.string.input_booleans,
expanded = expandedInputBooleans
)
}
if (expandedInputBooleans) {
items(inputBooleans.size) { index ->
SetEntityUI(inputBooleans[index], index)
}
}
}
if (lights.isNotEmpty()) {
item {
SetListHeader(
id = R.string.lights,
expanded = expandedLights
)
}
if (expandedLights) {
items(lights.size) { index ->
SetEntityUI(lights[index], index)
}
}
}
if (scenes.isNotEmpty()) {
item {
SetListHeader(
id = R.string.scenes,
expanded = expandedScenes
)
}
if (expandedScenes) {
items(scenes.size) { index ->
SetEntityUI(scenes[index], index)
}
}
}
if (scripts.isNotEmpty()) {
item {
SetListHeader(
id = R.string.scripts,
expanded = expandedScripts
)
}
if (expandedScripts) {
items(scripts.size) { index ->
SetEntityUI(scripts[index], index)
}
}
}
if (switches.isNotEmpty()) {
item {
SetListHeader(
id = R.string.switches,
expanded = expandedSwitches
)
}
if (expandedSwitches) {
items(switches.size) { index ->
SetEntityUI(switches[index], index)
}
}
}
item {
LoadOtherSection(swipeDismissableNavController)
}
}
}
composable(SCREEN_SETTINGS) {
ScreenSettings(
swipeDismissableNavController,
entityViewModel,
presenter
)
}
composable(SCREEN_SET_FAVORITES) {
ScreenSetFavorites(
validEntities,
entityViewModel,
baseContext,
presenter
)
}
}
}
}
}
}
}
@Composable
private fun SetEntityUI(entity: Entity<Any>, index: Int) {
val attributes = entity.attributes as Map<String, String>
val iconBitmap = getIcon(attributes["icon"], entity.entityId.split(".")[0], baseContext)
if (entity.entityId.split(".")[0] in HomePresenterImpl.toggleDomains) {
ToggleChip(
checked = entity.state == "on",
onCheckedChange = {
presenter.onEntityClicked(entity.entityId)
updateEntities()
},
modifier = Modifier
.fillMaxWidth()
.padding(top = if (index == 0) 0.dp else 10.dp),
appIcon = { Image(asset = iconBitmap ?: CommunityMaterial.Icon.cmd_cellphone) },
label = {
Text(
text = attributes["friendly_name"].toString(),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
},
enabled = entity.state != "unavailable",
toggleIcon = { ToggleChipDefaults.SwitchIcon(entity.state == "on") },
colors = ToggleChipDefaults.toggleChipColors(
checkedStartBackgroundColor = colorResource(id = R.color.colorAccent),
checkedEndBackgroundColor = colorResource(id = R.color.colorAccent),
uncheckedStartBackgroundColor = colorResource(id = R.color.colorAccent),
uncheckedEndBackgroundColor = colorResource(id = R.color.colorAccent),
checkedContentColor = Color.Black,
uncheckedContentColor = Color.Black,
checkedToggleIconTintColor = Color.Yellow,
uncheckedToggleIconTintColor = Color.DarkGray
)
)
} else {
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(top = if (index == 0) 0.dp else 10.dp),
icon = { Image(asset = iconBitmap ?: CommunityMaterial.Icon.cmd_cellphone) },
label = {
Text(
text = attributes["friendly_name"].toString(),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
},
enabled = entity.state != "unavailable",
onClick = {
presenter.onEntityClicked(entity.entityId)
updateEntities()
},
colors = setChipDefaults()
)
}
}
@Composable
private fun LoadOtherSection(swipeDismissableNavController: NavHostController) {
Column {
SetTitle(R.string.other)
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
icon = {
Image(asset = CommunityMaterial.Icon.cmd_cog)
},
label = {
Text(
text = stringResource(id = R.string.settings)
)
},
onClick = { swipeDismissableNavController.navigate(SCREEN_SETTINGS) },
colors = ChipDefaults.primaryChipColors(
contentColor = Color.Black
)
)
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
icon = {
Image(asset = CommunityMaterial.Icon.cmd_exit_run)
},
label = {
Text(
text = stringResource(id = R.string.logout)
)
},
onClick = { presenter.onLogoutClicked() },
colors = ChipDefaults.primaryChipColors(
backgroundColor = Color.Red,
contentColor = Color.Black
)
)
}
}
private fun updateEntities() {
mainScope.launch {
entityViewModel.entitiesResponse = presenter.getEntities()
delay(5000L)
entityViewModel.entitiesResponse = presenter.getEntities()
}
}
@Composable
private fun SetListHeader(id: Int, expanded: Boolean) {
ListHeader(
modifier = Modifier
.clickable {
when (id) {
R.string.favorites -> expandedFavorites = !expanded
R.string.input_booleans -> expandedInputBooleans = !expanded
R.string.lights -> expandedLights = !expanded
R.string.scenes -> expandedScenes = !expanded
R.string.scripts -> expandedScripts = !expanded
R.string.switches -> expandedSwitches = !expanded
}
}
) {
Row {
Text(
text = stringResource(id = id) + if (expanded) " -" else " +",
color = Color.White
)
}
}
}
}

View file

@ -5,10 +5,10 @@ import io.homeassistant.companion.android.common.data.integration.Entity
interface HomePresenter {
fun onViewReady()
fun onEntityClicked(entityId: String)
suspend fun onEntityClicked(entityId: String)
fun onLogoutClicked()
fun onFinish()
suspend fun getEntities(): List<Entity<Any>>
suspend fun getWearHomeFavorites(): Set<String>
suspend fun setWearHomeFavorites(favorites: Set<String>)
suspend fun getEntities(): List<Entity<*>>
suspend fun getWearHomeFavorites(): List<String>
suspend fun setWearHomeFavorites(favorites: List<String>)
}

View file

@ -46,7 +46,7 @@ class HomePresenterImpl @Inject constructor(
}
}
override suspend fun getEntities(): List<Entity<Any>> {
override suspend fun getEntities(): List<Entity<*>> {
return try {
integrationUseCase.getEntities()
} catch (e: Exception) {
@ -55,24 +55,20 @@ class HomePresenterImpl @Inject constructor(
}
}
override fun onEntityClicked(entityId: String) {
override suspend fun onEntityClicked(entityId: String) {
if (entityId.split(".")[0] in toggleDomains) {
mainScope.launch {
integrationUseCase.callService(
entityId.split(".")[0],
"toggle",
hashMapOf("entity_id" to entityId)
)
}
integrationUseCase.callService(
entityId.split(".")[0],
"toggle",
hashMapOf("entity_id" to entityId)
)
} else {
mainScope.launch {
integrationUseCase.callService(
entityId.split(".")[0],
"turn_on",
hashMapOf("entity_id" to entityId)
)
}
integrationUseCase.callService(
entityId.split(".")[0],
"turn_on",
hashMapOf("entity_id" to entityId)
)
}
}
@ -103,11 +99,11 @@ class HomePresenterImpl @Inject constructor(
}
}
override suspend fun getWearHomeFavorites(): Set<String> {
return integrationUseCase.getWearHomeFavorites()
override suspend fun getWearHomeFavorites(): List<String> {
return integrationUseCase.getWearHomeFavorites().toList()
}
override suspend fun setWearHomeFavorites(favorites: Set<String>) {
integrationUseCase.setWearHomeFavorites(favorites)
override suspend fun setWearHomeFavorites(favorites: List<String>) {
integrationUseCase.setWearHomeFavorites(favorites.toSet())
}
}

View file

@ -0,0 +1,68 @@
package io.homeassistant.companion.android.home
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.homeassistant.companion.android.common.data.integration.Entity
import kotlinx.coroutines.launch
class MainViewModel : ViewModel() {
private lateinit var homePresenter: HomePresenter
// TODO: This is bad, do this instead: https://stackoverflow.com/questions/46283981/android-viewmodel-additional-arguments
fun init(homePresenter: HomePresenter) {
this.homePresenter = homePresenter
loadEntities()
}
var entities = mutableStateListOf<Entity<*>>()
private set
var favoriteEntityIds = mutableStateListOf<String>()
private set
fun loadEntities() {
viewModelScope.launch {
entities.addAll(homePresenter.getEntities())
favoriteEntityIds.addAll(homePresenter.getWearHomeFavorites())
}
}
fun toggleEntity(entityId: String) {
viewModelScope.launch {
homePresenter.onEntityClicked(entityId)
val updatedEntities = homePresenter.getEntities()
// This should be better....
for (i in updatedEntities.indices) {
entities[i] = updatedEntities[i]
}
}
}
fun addFavorite(entityId: String) {
viewModelScope.launch {
favoriteEntityIds.add(entityId)
homePresenter.setWearHomeFavorites(favoriteEntityIds)
}
}
fun removeFavorite(entity: String) {
viewModelScope.launch {
favoriteEntityIds.remove(entity)
homePresenter.setWearHomeFavorites(favoriteEntityIds)
}
}
fun clearFavorites() {
viewModelScope.launch {
favoriteEntityIds.clear()
homePresenter.setWearHomeFavorites(favoriteEntityIds)
}
}
fun logout() {
homePresenter.onLogoutClicked()
}
}

View file

@ -0,0 +1,78 @@
package io.homeassistant.companion.android.home.views
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.ToggleChip
import androidx.wear.compose.material.ToggleChipDefaults
import com.mikepenz.iconics.compose.Image
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.home.HomePresenterImpl
import io.homeassistant.companion.android.util.getIcon
import io.homeassistant.companion.android.util.setChipDefaults
@Composable
fun EntityUi(
entity: Entity<*>,
onEntityClicked: (String) -> Unit
) {
val attributes = entity.attributes as Map<*, *>
val iconBitmap = getIcon(attributes["icon"] as String?, entity.entityId.split(".")[0], LocalContext.current)
if (entity.entityId.split(".")[0] in HomePresenterImpl.toggleDomains) {
ToggleChip(
checked = entity.state == "on",
onCheckedChange = { onEntityClicked(entity.entityId) },
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp),
appIcon = { Image(asset = iconBitmap ?: CommunityMaterial.Icon.cmd_cellphone) },
label = {
Text(
text = attributes["friendly_name"].toString(),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
},
enabled = entity.state != "unavailable",
toggleIcon = { ToggleChipDefaults.SwitchIcon(entity.state == "on") },
colors = ToggleChipDefaults.toggleChipColors(
checkedStartBackgroundColor = colorResource(id = R.color.colorAccent),
checkedEndBackgroundColor = colorResource(id = R.color.colorAccent),
uncheckedStartBackgroundColor = colorResource(id = R.color.colorAccent),
uncheckedEndBackgroundColor = colorResource(id = R.color.colorAccent),
checkedContentColor = Color.Black,
uncheckedContentColor = Color.Black,
checkedToggleIconTintColor = Color.Yellow,
uncheckedToggleIconTintColor = Color.DarkGray
)
)
} else {
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 10.dp),
icon = { Image(asset = iconBitmap ?: CommunityMaterial.Icon.cmd_cellphone) },
label = {
Text(
text = attributes["friendly_name"].toString(),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
},
enabled = entity.state != "unavailable",
onClick = { onEntityClicked(entity.entityId) },
colors = setChipDefaults()
)
}
}

View file

@ -0,0 +1,106 @@
package io.homeassistant.companion.android.home.views
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ExperimentalWearMaterialApi
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.Text
import androidx.wear.compose.navigation.SwipeDismissableNavHost
import androidx.wear.compose.navigation.composable
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.home.HomePresenterImpl
import io.homeassistant.companion.android.home.MainViewModel
import io.homeassistant.companion.android.util.LocalRotaryEventDispatcher
import io.homeassistant.companion.android.util.RotaryEventDispatcher
import io.homeassistant.companion.android.util.RotaryEventHandlerSetup
import io.homeassistant.companion.android.util.SetTitle
import io.homeassistant.companion.android.util.setChipDefaults
private const val SCREEN_LANDING = "landing"
private const val SCREEN_SETTINGS = "settings"
private const val SCREEN_SET_FAVORITES = "set_favorites"
@ExperimentalWearMaterialApi
@Composable
fun LoadHomePage(
mainViewModel: MainViewModel
) {
val rotaryEventDispatcher = RotaryEventDispatcher()
if (mainViewModel.entities.isNullOrEmpty() && mainViewModel.favoriteEntityIds.isNullOrEmpty()) {
Column {
Spacer(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp)
)
SetTitle(id = R.string.loading)
Chip(
modifier = Modifier
.padding(top = 50.dp, start = 10.dp, end = 10.dp),
label = {
Text(
text = stringResource(R.string.loading_entities),
textAlign = TextAlign.Center
)
},
onClick = { /* No op */ },
colors = setChipDefaults()
)
}
} else {
val swipeDismissableNavController = rememberSwipeDismissableNavController()
MaterialTheme {
CompositionLocalProvider(
LocalRotaryEventDispatcher provides rotaryEventDispatcher
) {
RotaryEventHandlerSetup(rotaryEventDispatcher)
SwipeDismissableNavHost(
navController = swipeDismissableNavController,
startDestination = SCREEN_LANDING
) {
composable(SCREEN_LANDING) {
MainView(
mainViewModel.entities,
mainViewModel.favoriteEntityIds,
{ mainViewModel.toggleEntity(it) },
{ swipeDismissableNavController.navigate(SCREEN_SETTINGS) },
{ mainViewModel.logout() }
)
}
composable(SCREEN_SETTINGS) {
SettingsView(
mainViewModel.favoriteEntityIds,
{ swipeDismissableNavController.navigate(SCREEN_SET_FAVORITES) },
{ mainViewModel.clearFavorites() }
)
}
composable(SCREEN_SET_FAVORITES) {
val validEntities = mainViewModel.entities
.filter { it.entityId.split(".")[0] in HomePresenterImpl.supportedDomains }
SetFavoritesView(
validEntities,
mainViewModel.favoriteEntityIds
) { entityId, isSelected ->
if (isSelected) {
mainViewModel.addFavorite(entityId)
} else {
mainViewModel.removeFavorite(entityId)
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,29 @@
package io.homeassistant.companion.android.home.views
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.wear.compose.material.ListHeader
import androidx.wear.compose.material.Text
@Composable
fun ListHeader(
stringId: Int,
expanded: Boolean,
onExpandChanged: (Boolean) -> Unit
) {
ListHeader(
modifier = Modifier
.clickable { onExpandChanged(!expanded) }
) {
Row {
Text(
text = stringResource(id = stringId) + if (expanded) " -" else " +",
color = Color.White
)
}
}
}

View file

@ -0,0 +1,196 @@
package io.homeassistant.companion.android.home.views
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.PositionIndicator
import androidx.wear.compose.material.Scaffold
import androidx.wear.compose.material.ScalingLazyColumn
import androidx.wear.compose.material.ScalingLazyListState
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.rememberScalingLazyListState
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.util.RotaryEventDispatcher
import io.homeassistant.companion.android.util.RotaryEventState
import io.homeassistant.companion.android.util.SetTitle
import io.homeassistant.companion.android.util.setChipDefaults
@Composable
fun MainView(
entities: List<Entity<*>>,
favoriteEntityIds: List<String>,
onEntityClicked: (String) -> Unit,
onSettingsClicked: () -> Unit,
onLogoutClicked: () -> Unit
) {
val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState()
var expandedFavorites: Boolean by rememberSaveable { mutableStateOf(true) }
var expandedInputBooleans: Boolean by rememberSaveable { mutableStateOf(true) }
var expandedLights: Boolean by rememberSaveable { mutableStateOf(true) }
var expandedScenes: Boolean by rememberSaveable { mutableStateOf(true) }
var expandedScripts: Boolean by rememberSaveable { mutableStateOf(true) }
var expandedSwitches: Boolean by rememberSaveable { mutableStateOf(true) }
val entityMap: Map<String, Entity<*>> = entities.map { it.entityId to it }.toMap()
val scenes = entities.filter { it.entityId.split(".")[0] == "scene" }
val scripts = entities.filter { it.entityId.split(".")[0] == "script" }
val lights = entities.filter { it.entityId.split(".")[0] == "light" }
val inputBooleans = entities.filter { it.entityId.split(".")[0] == "input_boolean" }
val switches = entities.filter { it.entityId.split(".")[0] == "switch" }
RotaryEventDispatcher(scalingLazyListState)
RotaryEventState(scrollState = scalingLazyListState)
Scaffold(
positionIndicator = {
if (scalingLazyListState.isScrollInProgress)
PositionIndicator(scalingLazyListState = scalingLazyListState)
}
) {
ScalingLazyColumn(
modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(
top = 10.dp,
start = 10.dp,
end = 10.dp,
bottom = 40.dp
),
horizontalAlignment = Alignment.CenterHorizontally,
state = scalingLazyListState
) {
if (favoriteEntityIds.isNotEmpty()) {
item {
ListHeader(
stringId = R.string.favorites,
expanded = expandedFavorites,
onExpandChanged = { expandedFavorites = it }
)
}
if (expandedFavorites) {
items(favoriteEntityIds.size) { index ->
EntityUi(
// This is here to not break existing favorites.....
entityMap[favoriteEntityIds[index].split(",")[0]]!!,
onEntityClicked
)
}
}
}
if (entities.isNullOrEmpty()) {
item {
Column {
SetTitle(id = R.string.loading)
Chip(
modifier = Modifier
.padding(
top = 10.dp,
start = 10.dp,
end = 10.dp
),
label = {
Text(
text = stringResource(R.string.loading_entities),
textAlign = TextAlign.Center
)
},
onClick = { /* No op */ },
colors = setChipDefaults()
)
}
}
}
if (inputBooleans.isNotEmpty()) {
item {
ListHeader(
stringId = R.string.input_booleans,
expanded = expandedInputBooleans,
onExpandChanged = { expandedInputBooleans = it }
)
}
if (expandedInputBooleans) {
items(inputBooleans.size) { index ->
EntityUi(inputBooleans[index], onEntityClicked)
}
}
}
if (lights.isNotEmpty()) {
item {
ListHeader(
stringId = R.string.lights,
expanded = expandedLights,
onExpandChanged = { expandedLights = it }
)
}
if (expandedLights) {
items(lights.size) { index ->
EntityUi(lights[index], onEntityClicked)
}
}
}
if (scenes.isNotEmpty()) {
item {
ListHeader(
stringId = R.string.scenes,
expanded = expandedScenes,
onExpandChanged = { expandedScenes = it }
)
}
if (expandedScenes) {
items(scenes.size) { index ->
EntityUi(scenes[index], onEntityClicked)
}
}
}
if (scripts.isNotEmpty()) {
item {
ListHeader(
stringId = R.string.scripts,
expanded = expandedScripts,
onExpandChanged = { expandedScripts = it }
)
}
if (expandedScripts) {
items(scripts.size) { index ->
EntityUi(scripts[index], onEntityClicked)
}
}
}
if (switches.isNotEmpty()) {
item {
ListHeader(
stringId = R.string.switches,
expanded = expandedSwitches,
onExpandChanged = { expandedSwitches = it }
)
}
if (expandedSwitches) {
items(switches.size) { index ->
EntityUi(switches[index], onEntityClicked)
}
}
}
item {
OtherSection(
onSettingsClicked = onSettingsClicked,
onLogoutClicked = onLogoutClicked
)
}
}
}
}

View file

@ -0,0 +1,62 @@
package io.homeassistant.companion.android.home.views
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
import androidx.wear.compose.material.Text
import com.mikepenz.iconics.compose.Image
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.util.SetTitle
@Composable
fun OtherSection(
onSettingsClicked: () -> Unit,
onLogoutClicked: () -> Unit
) {
Column {
SetTitle(R.string.other)
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
icon = {
Image(asset = CommunityMaterial.Icon.cmd_cog)
},
label = {
Text(
text = stringResource(id = R.string.settings)
)
},
onClick = onSettingsClicked,
colors = ChipDefaults.primaryChipColors(
contentColor = Color.Black
)
)
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
icon = {
Image(asset = CommunityMaterial.Icon.cmd_exit_run)
},
label = {
Text(
text = stringResource(id = R.string.logout)
)
},
onClick = onLogoutClicked,
colors = ChipDefaults.primaryChipColors(
backgroundColor = Color.Red,
contentColor = Color.Black
)
)
}
}

View file

@ -0,0 +1,91 @@
package io.homeassistant.companion.android.home.views
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.ScalingLazyColumn
import androidx.wear.compose.material.ScalingLazyListState
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.ToggleChip
import androidx.wear.compose.material.ToggleChipDefaults
import androidx.wear.compose.material.rememberScalingLazyListState
import com.mikepenz.iconics.compose.Image
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.util.RotaryEventState
import io.homeassistant.companion.android.util.SetTitle
import io.homeassistant.companion.android.util.getIcon
@Composable
fun SetFavoritesView(
validEntities: List<Entity<*>>,
favoriteEntityIds: List<String>,
onFavoriteSelected: (entityId: String, isSelected: Boolean) -> Unit
) {
val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState()
RotaryEventState(scrollState = scalingLazyListState)
ScalingLazyColumn(
modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(
top = 10.dp,
start = 10.dp,
end = 10.dp,
bottom = 40.dp
),
horizontalAlignment = Alignment.CenterHorizontally,
state = scalingLazyListState
) {
items(validEntities.size) { index ->
val attributes = validEntities[index].attributes as Map<*, *>
val iconBitmap = getIcon(
attributes["icon"] as String?,
validEntities[index].entityId.split(".")[0],
LocalContext.current
)
if (index == 0)
SetTitle(id = R.string.set_favorite)
val entityId = validEntities[index].entityId
val checked = favoriteEntityIds.contains(entityId)
ToggleChip(
checked = checked,
onCheckedChange = {
onFavoriteSelected(entityId, it)
},
modifier = Modifier
.fillMaxWidth()
.padding(top = if (index == 0) 30.dp else 10.dp),
appIcon = { Image(asset = iconBitmap ?: CommunityMaterial.Icon.cmd_cellphone) },
label = {
Text(
text = attributes["friendly_name"].toString(),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
},
toggleIcon = { ToggleChipDefaults.SwitchIcon(checked) },
colors = ToggleChipDefaults.toggleChipColors(
checkedStartBackgroundColor = colorResource(id = R.color.colorAccent),
checkedEndBackgroundColor = colorResource(id = R.color.colorAccent),
uncheckedStartBackgroundColor = colorResource(id = R.color.colorAccent),
uncheckedEndBackgroundColor = colorResource(id = R.color.colorAccent),
checkedContentColor = Color.Black,
uncheckedContentColor = Color.Black,
checkedToggleIconTintColor = Color.Yellow,
uncheckedToggleIconTintColor = Color.DarkGray
)
)
}
}
}

View file

@ -0,0 +1,71 @@
package io.homeassistant.companion.android.home.views
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
import androidx.wear.compose.material.Text
import com.mikepenz.iconics.compose.Image
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.util.SetTitle
@Composable
fun SettingsView(
favorites: List<String>,
onClickSetFavorites: () -> Unit,
onClearFavorites: () -> Unit
) {
Column {
Spacer(modifier = Modifier.height(20.dp))
SetTitle(id = R.string.settings)
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(top = 20.dp),
icon = {
Image(asset = CommunityMaterial.Icon3.cmd_star)
},
label = {
Text(
text = stringResource(id = R.string.favorite)
)
},
onClick = onClickSetFavorites,
colors = ChipDefaults.primaryChipColors(
contentColor = Color.Black
)
)
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
icon = {
Image(asset = CommunityMaterial.Icon.cmd_delete)
},
label = {
Text(
text = stringResource(id = R.string.clear_favorites),
)
},
onClick = onClearFavorites,
colors = ChipDefaults.primaryChipColors(
contentColor = Color.Black
),
secondaryLabel = {
Text(
text = stringResource(id = R.string.irreverisble)
)
},
enabled = favorites.isNotEmpty()
)
}
}

View file

@ -1,168 +0,0 @@
package io.homeassistant.companion.android.settings
import android.content.Context
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
import androidx.wear.compose.material.ScalingLazyColumn
import androidx.wear.compose.material.ScalingLazyListState
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.ToggleChip
import androidx.wear.compose.material.ToggleChipDefaults
import androidx.wear.compose.material.rememberScalingLazyListState
import com.mikepenz.iconics.compose.Image
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.integration.Entity
import io.homeassistant.companion.android.home.HomeActivity
import io.homeassistant.companion.android.home.HomePresenter
import io.homeassistant.companion.android.util.RotaryEventState
import io.homeassistant.companion.android.util.SetTitle
import io.homeassistant.companion.android.util.getIcon
import io.homeassistant.companion.android.util.saveFavorites
import io.homeassistant.companion.android.viewModels.EntityViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
@Composable
fun ScreenSettings(swipeDismissableNavController: NavHostController, entityViewModel: EntityViewModel, presenter: HomePresenter) {
Column {
Spacer(modifier = Modifier.height(20.dp))
SetTitle(id = R.string.settings)
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(top = 20.dp),
icon = {
Image(asset = CommunityMaterial.Icon3.cmd_star)
},
label = {
Text(
text = stringResource(id = R.string.favorite)
)
},
onClick = {
swipeDismissableNavController.navigate(
HomeActivity.SCREEN_SET_FAVORITES
)
},
colors = ChipDefaults.primaryChipColors(
contentColor = Color.Black
)
)
Chip(
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp),
icon = {
Image(asset = CommunityMaterial.Icon.cmd_delete)
},
label = {
Text(
text = stringResource(id = R.string.clear_favorites),
)
},
onClick = {
entityViewModel.favoriteEntities = mutableSetOf()
saveFavorites(entityViewModel.favoriteEntities.toMutableSet(), presenter, mainScope)
},
colors = ChipDefaults.primaryChipColors(
contentColor = Color.Black
),
secondaryLabel = {
Text(
text = stringResource(id = R.string.irreverisble)
)
},
enabled = entityViewModel.favoriteEntities.isNotEmpty()
)
}
}
@Composable
fun ScreenSetFavorites(
validEntities: List<Entity<Any>>,
entityViewModel: EntityViewModel,
context: Context,
presenter: HomePresenter
) {
val scalingLazyListState: ScalingLazyListState = rememberScalingLazyListState()
RotaryEventState(scrollState = scalingLazyListState)
ScalingLazyColumn(
modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(
top = 10.dp,
start = 10.dp,
end = 10.dp,
bottom = 40.dp
),
horizontalAlignment = Alignment.CenterHorizontally,
state = scalingLazyListState
) {
items(validEntities.size) { index ->
val attributes = validEntities[index].attributes as Map<String, String>
val iconBitmap = getIcon(attributes["icon"], validEntities[index].entityId.split(".")[0], context)
if (index == 0)
SetTitle(id = R.string.set_favorite)
val elementString = "${validEntities[index].entityId},${attributes["friendly_name"]},${attributes["icon"]}"
var checked by rememberSaveable { mutableStateOf(entityViewModel.favoriteEntities.contains(elementString)) }
ToggleChip(
checked = checked,
onCheckedChange = {
checked = it
if (it) {
entityViewModel.favoriteEntities.add(elementString)
} else {
entityViewModel.favoriteEntities.remove(elementString)
}
saveFavorites(entityViewModel.favoriteEntities.toMutableSet(), presenter, mainScope)
},
modifier = Modifier
.fillMaxWidth()
.padding(top = if (index == 0) 30.dp else 10.dp),
appIcon = { Image(asset = iconBitmap ?: CommunityMaterial.Icon.cmd_cellphone) },
label = {
Text(
text = attributes["friendly_name"].toString(),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
},
toggleIcon = { ToggleChipDefaults.SwitchIcon(checked) },
colors = ToggleChipDefaults.toggleChipColors(
checkedStartBackgroundColor = colorResource(id = R.color.colorAccent),
checkedEndBackgroundColor = colorResource(id = R.color.colorAccent),
uncheckedStartBackgroundColor = colorResource(id = R.color.colorAccent),
uncheckedEndBackgroundColor = colorResource(id = R.color.colorAccent),
checkedContentColor = Color.Black,
uncheckedContentColor = Color.Black,
checkedToggleIconTintColor = Color.Yellow,
uncheckedToggleIconTintColor = Color.DarkGray
)
)
}
}
}

View file

@ -17,10 +17,6 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.IIcon
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.home.HomePresenter
import io.homeassistant.companion.android.viewModels.EntityViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Composable
fun SetTitle(id: Int) {
@ -42,14 +38,6 @@ fun setChipDefaults(): ChipColors {
)
}
fun updateFavorites(entityViewModel: EntityViewModel, presenter: HomePresenter, mainScope: CoroutineScope) {
mainScope.launch { entityViewModel.favoriteEntities = presenter.getWearHomeFavorites().toMutableSet() }
}
fun saveFavorites(favorites: Set<String>, presenter: HomePresenter, mainScope: CoroutineScope) {
mainScope.launch { presenter.setWearHomeFavorites(favorites.toSet()) }
}
fun getIcon(icon: String?, domain: String, context: Context): IIcon? {
return if (icon?.startsWith("mdi") == true) {
val mdiIcon = icon.split(":")[1]

View file

@ -1,13 +0,0 @@
package io.homeassistant.companion.android.viewModels
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import io.homeassistant.companion.android.common.data.integration.Entity
class EntityViewModel : ViewModel() {
var entitiesResponse: List<Entity<Any>> by mutableStateOf(mutableListOf())
var favoriteEntities: MutableSet<String> by mutableStateOf(mutableSetOf())
}