Refactor OverviewScreen

This commit is contained in:
Jonas Lochmann 2023-02-06 01:00:00 +01:00
parent 6b9aebbeaf
commit 2971e3f55d
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
8 changed files with 315 additions and 242 deletions

View file

@ -32,7 +32,7 @@ fun ScreenMultiplexer(
) {
when (screen) {
null -> {/* nothing to do */ }
is Screen.OverviewScreen -> OverviewScreen(screen.content, executeCommand, modifier = modifier)
is Screen.OverviewScreen -> OverviewScreen(screen.content, modifier = modifier)
is Screen.FragmentScreen -> FragmentScreen(screen, fragmentManager, fragmentIds, modifier = modifier)
}
}

View file

@ -16,11 +16,9 @@
package io.timelimit.android.ui.model
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.asFlow
import androidx.lifecycle.viewModelScope
import io.timelimit.android.BuildConfig
import io.timelimit.android.data.model.UserType
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.main.ActivityViewModel
@ -34,10 +32,6 @@ import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.flow.*
class MainModel(application: Application): AndroidViewModel(application) {
companion object {
private const val LOG_TAG = "MainModel"
}
val activityModel = ActivityViewModel(application)
private val logic = DefaultAppLogic.with(application)
@ -109,13 +103,7 @@ class MainModel(application: Application): AndroidViewModel(application) {
}
fun execute(command: UpdateStateCommand) {
state.update { oldState ->
command.transform(oldState) ?: oldState.also {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "execute($command) did not transform state")
}
}
}
command.applyTo(state)
}
fun reportAuthenticationScreenClosed() {

View file

@ -15,11 +15,29 @@
*/
package io.timelimit.android.ui.model
import android.util.Log
import io.timelimit.android.BuildConfig
import io.timelimit.android.ui.model.main.OverviewHandling
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
sealed class UpdateStateCommand {
companion object {
private const val LOG_TAG = "UpdateStateCommand"
}
abstract fun transform(state: State): State?
fun applyTo(state: MutableStateFlow<State>) {
state.update { oldState ->
transform(oldState) ?: oldState.also {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "$this.transform() did not transform state")
}
}
}
}
object Reset: UpdateStateCommand() {
override fun transform(state: State) = State.LaunchState
}

View file

@ -40,7 +40,6 @@ import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.sync.Mutex
import java.util.*
object OverviewHandling {
fun processState(
@ -104,6 +103,11 @@ object OverviewHandling {
}
}
},
addUser = {
launch {
UpdateStateCommand.Overview.AddUser.applyTo(stateLive)
}
},
skipTaskReview = { task ->
launch {
lock.tryWithLock {
@ -176,16 +180,30 @@ object OverviewHandling {
}
}
stateLive.update { state ->
if (state is State.Overview) State.ManageChild.Main(state, user.id, fromRedirect = false)
else state
UpdateStateCommand.Overview.ManageChild(user.id).applyTo(stateLive)
}
UserType.Parent -> UpdateStateCommand.Overview.ManageParent(user.id).applyTo(stateLive)
}
}
UserType.Parent -> stateLive.update { state ->
if (state is State.Overview) State.ManageParent.Main(state, user.id)
else state
},
openDevice = { device ->
launch {
UpdateStateCommand.Overview.ManageDevice(device.device.id).applyTo(stateLive)
}
},
setupDevice = {
launch {
UpdateStateCommand.Overview.SetupDevice.applyTo(stateLive)
}
},
showMoreDevices = {
launch {
UpdateStateCommand.Overview.ShowMoreDevices(it).applyTo(stateLive)
}
},
showMoreUsers = {
launch {
UpdateStateCommand.Overview.ShowAllUsers.applyTo(stateLive)
}
}
)
@ -367,10 +385,15 @@ object OverviewHandling {
data class Actions(
val hideIntro: () -> Unit,
val addDevice: () -> Unit,
val addUser: () -> Unit,
val skipTaskReview: (TaskToReview) -> Unit,
val reviewReject: (TaskToReview) -> Unit,
val reviewAccept: (TaskToReview) -> Unit,
val openUser: (UserItem) -> Unit
val openUser: (UserItem) -> Unit,
val openDevice: (DeviceItem) -> Unit,
val setupDevice: () -> Unit,
val showMoreDevices: (OverviewState.DeviceList) -> Unit,
val showMoreUsers: () -> Unit
)
data class IntroFlags(
val showSetupOption: Boolean,

View file

@ -19,6 +19,8 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
@ -28,24 +30,50 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.timelimit.android.BuildConfig
import io.timelimit.android.R
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.main.OverviewHandling
@OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.deviceItems(screen: OverviewHandling.OverviewScreen) {
item (key = Pair("devices", "header")) {
ListCommon.SectionHeader(stringResource(R.string.overview_header_devices), Modifier.animateItemPlacement())
}
items(screen.devices.list, key = { Pair("device", it.device.id) }) {
DeviceItem(it, screen.actions.openDevice)
}
if (screen.devices.canAdd) {
item (key = Pair("devices", "add")) {
ListCommon.ActionListItem(
icon = Icons.Default.Add,
label = stringResource(R.string.add_device),
action = screen.actions.addDevice,
modifier = Modifier.animateItemPlacement()
)
}
}
if (screen.devices.canShowMore != null) {
item (key = Pair("devices", "more")) {
ListCommon.ShowMoreItem(
modifier = Modifier.animateItemPlacement(),
action = { screen.actions.showMoreDevices(screen.devices.canShowMore) }
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyItemScope.DeviceItem(
item: OverviewHandling.DeviceItem,
executeCommand: (UpdateStateCommand) -> Unit
openAction: (OverviewHandling.DeviceItem) -> Unit
) {
ListCardCommon.Card(
Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
.clickable(
onClick = {
executeCommand(UpdateStateCommand.Overview.ManageDevice(item.device.id))
}
)
.clickable(onClick = { openAction(item) })
) {
ListCardCommon.TextWithIcon(
icon = Icons.Default.Smartphone,

View file

@ -0,0 +1,196 @@
/*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.overview.overview
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material.*
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.timelimit.android.R
import io.timelimit.android.ui.model.main.OverviewHandling
import io.timelimit.android.ui.util.DateUtil
import io.timelimit.android.util.TimeTextUtil
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
fun LazyListScope.introItems(
screen: OverviewHandling.OverviewScreen,
) {
if (screen.intro.showSetupOption) {
item (key = Pair("intro", "finish setup")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_finish_setup_title),
style = MaterialTheme.typography.h6
)
Text(stringResource(R.string.overview_finish_setup_text))
ListCardCommon.ActionButton(
label = stringResource(R.string.generic_go),
action = screen.actions.setupDevice
)
}
}
}
if (screen.intro.showOutdatedServer) {
item (key = Pair("intro", "outdated server")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_server_outdated_title),
style = MaterialTheme.typography.h6
)
Text(stringResource(R.string.overview_server_outdated_text))
}
}
}
if (screen.intro.showServerMessage != null) {
item (key = Pair("intro", "server message")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_server_message),
style = MaterialTheme.typography.h6
)
Text(screen.intro.showServerMessage)
}
}
}
if (screen.intro.showIntro) {
item (key = Pair("intro", "intro")) {
val state = remember {
DismissState(
initialValue = DismissValue.Default,
confirmStateChange = {
screen.actions.hideIntro()
true
}
)
}
SwipeToDismiss(
state = state,
background = {},
modifier = Modifier.animateItemPlacement()
) {
ListCardCommon.Card(
modifier = Modifier.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_intro_title),
style = MaterialTheme.typography.h6
)
Text(stringResource(R.string.overview_intro_text))
Text(
stringResource(R.string.generic_swipe_to_dismiss),
style = MaterialTheme.typography.subtitle1
)
}
}
}
}
if (screen.taskToReview != null) {
item (key = Pair("intro", "task review")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.task_review_title),
style = MaterialTheme.typography.h6
)
Text(
stringResource(R.string.task_review_text, screen.taskToReview.task.childName, screen.taskToReview.task.childTask.taskTitle)
)
Text(
stringResource(
R.string.task_review_category,
TimeTextUtil.time(screen.taskToReview.task.childTask.extraTimeDuration, LocalContext.current),
screen.taskToReview.task.categoryTitle
),
style = MaterialTheme.typography.subtitle1
)
screen.taskToReview.task.childTask.lastGrantTimestamp.let { lastGrantTimestamp ->
if (lastGrantTimestamp != 0L) {
Text(
stringResource(
R.string.task_review_last_grant,
DateUtil.formatAbsoluteDate(LocalContext.current, lastGrantTimestamp)
),
style = MaterialTheme.typography.subtitle1
)
}
}
Row {
TextButton(onClick = {
screen.actions.skipTaskReview(screen.taskToReview)
}) {
Text(stringResource(R.string.generic_skip))
}
Spacer(Modifier.weight(1.0f))
OutlinedButton(onClick = { screen.actions.reviewReject(screen.taskToReview) }) {
Text(stringResource(R.string.generic_no))
}
Spacer(Modifier.width(8.dp))
OutlinedButton(onClick = { screen.actions.reviewAccept(screen.taskToReview) }) {
Text(stringResource(R.string.generic_yes))
}
}
Text(
stringResource(R.string.purchase_required_info_local_mode_free),
style = MaterialTheme.typography.subtitle1
)
}
}
}
}

View file

@ -15,30 +15,16 @@
*/
package io.timelimit.android.ui.overview.overview
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.timelimit.android.R
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.main.OverviewHandling
import io.timelimit.android.ui.util.DateUtil
import io.timelimit.android.util.TimeTextUtil
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable
fun OverviewScreen(
screen: OverviewHandling.OverviewScreen,
executeCommand: (UpdateStateCommand) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn (
@ -46,198 +32,8 @@ fun OverviewScreen(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier
) {
if (screen.intro.showSetupOption) {
item (key = Pair("intro", "finish setup")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_finish_setup_title),
style = MaterialTheme.typography.h6
)
Text(stringResource(R.string.overview_finish_setup_text))
ListCardCommon.ActionButton(
label = stringResource(R.string.generic_go),
action = {
executeCommand(UpdateStateCommand.Overview.SetupDevice)
}
)
}
}
}
if (screen.intro.showOutdatedServer) {
item (key = Pair("intro", "outdated server")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_server_outdated_title),
style = MaterialTheme.typography.h6
)
Text(stringResource(R.string.overview_server_outdated_text))
}
}
}
if (screen.intro.showServerMessage != null) {
item (key = Pair("intro", "servermessage")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_server_message),
style = MaterialTheme.typography.h6
)
Text(screen.intro.showServerMessage)
}
}
}
if (screen.intro.showIntro) {
item (key = Pair("intro", "intro")) {
val state = remember {
DismissState(
initialValue = DismissValue.Default,
confirmStateChange = {
screen.actions.hideIntro()
true
}
)
}
SwipeToDismiss(
state = state,
background = {},
modifier = Modifier.animateItemPlacement()
) {
ListCardCommon.Card(
modifier = Modifier.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.overview_intro_title),
style = MaterialTheme.typography.h6
)
Text(stringResource(R.string.overview_intro_text))
Text(
stringResource(R.string.generic_swipe_to_dismiss),
style = MaterialTheme.typography.subtitle1
)
}
}
}
}
if (screen.taskToReview != null) {
item (key = Pair("task", "review")) {
ListCardCommon.Card(
modifier = Modifier
.animateItemPlacement()
.padding(horizontal = 8.dp)
) {
Text(
stringResource(R.string.task_review_title),
style = MaterialTheme.typography.h6
)
Text(
stringResource(R.string.task_review_text, screen.taskToReview.task.childName, screen.taskToReview.task.childTask.taskTitle)
)
Text(
stringResource(
R.string.task_review_category,
TimeTextUtil.time(screen.taskToReview.task.childTask.extraTimeDuration, LocalContext.current),
screen.taskToReview.task.categoryTitle
),
style = MaterialTheme.typography.subtitle1
)
screen.taskToReview.task.childTask.lastGrantTimestamp.let { lastGrantTimestamp ->
if (lastGrantTimestamp != 0L) {
Text(
stringResource(
R.string.task_review_last_grant,
DateUtil.formatAbsoluteDate(LocalContext.current, lastGrantTimestamp)
),
style = MaterialTheme.typography.subtitle1
)
}
}
Row {
TextButton(onClick = {
screen.actions.skipTaskReview(screen.taskToReview)
}) {
Text(stringResource(R.string.generic_skip))
}
Spacer(Modifier.weight(1.0f))
OutlinedButton(onClick = { screen.actions.reviewReject(screen.taskToReview) }) {
Text(stringResource(R.string.generic_no))
}
Spacer(Modifier.width(8.dp))
OutlinedButton(onClick = { screen.actions.reviewAccept(screen.taskToReview) }) {
Text(stringResource(R.string.generic_yes))
}
}
Text(
stringResource(R.string.purchase_required_info_local_mode_free),
style = MaterialTheme.typography.subtitle1
)
}
}
}
item (key = Pair("devices", "header")) { ListCommon.SectionHeader(stringResource(R.string.overview_header_devices), Modifier.animateItemPlacement()) }
items(screen.devices.list, key = { Pair("device", it.device.id) }) {
DeviceItem(it, executeCommand)
}
if (screen.devices.canAdd) {
item (key = Pair("devices", "add")) {
ListCommon.ActionListItem(
icon = Icons.Default.Add,
label = stringResource(R.string.add_device),
action = screen.actions.addDevice,
modifier = Modifier.animateItemPlacement()
)
}
}
if (screen.devices.canShowMore != null) {
item (key = Pair("devices", "show more")) { ListCommon.ShowMoreItem(modifier = Modifier.animateItemPlacement()) {
executeCommand(UpdateStateCommand.Overview.ShowMoreDevices(screen.devices.canShowMore))
}}
}
item (key = Pair("header", "users")) { ListCommon.SectionHeader(stringResource(R.string.overview_header_users), Modifier.animateItemPlacement()) }
items(screen.users.list, key = { Pair("user", it.id) }) { UserItem(it, screen.actions) }
if (screen.users.canAdd) item (key = Pair("header", "user.create")) {
ListCommon.ActionListItem(
icon = Icons.Default.Add,
label = stringResource(R.string.add_user_title),
action = { executeCommand(UpdateStateCommand.Overview.AddUser) },
modifier = Modifier.animateItemPlacement()
)
}
if (screen.users.canShowMore) item (key = Pair("header", "user.more")) {
ListCommon.ShowMoreItem (modifier = Modifier.animateItemPlacement()) { executeCommand(UpdateStateCommand.Overview.ShowAllUsers) }
}
introItems(screen)
deviceItems(screen)
userItems(screen)
}
}

View file

@ -19,12 +19,11 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.AlarmOff
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@ -33,6 +32,31 @@ import io.timelimit.android.R
import io.timelimit.android.data.model.UserType
import io.timelimit.android.ui.model.main.OverviewHandling
@OptIn(ExperimentalFoundationApi::class)
fun LazyListScope.userItems(screen: OverviewHandling.OverviewScreen) {
item (key = Pair("users", "header")) {
ListCommon.SectionHeader(stringResource(R.string.overview_header_users), Modifier.animateItemPlacement())
}
items(screen.users.list, key = { Pair("user", it.id) }) { UserItem(it, screen.actions) }
if (screen.users.canAdd) item (key = Pair("users", "create")) {
ListCommon.ActionListItem(
icon = Icons.Default.Add,
label = stringResource(R.string.add_user_title),
action = screen.actions.addUser,
modifier = Modifier.animateItemPlacement()
)
}
if (screen.users.canShowMore) item (key = Pair("users", "more")) {
ListCommon.ShowMoreItem (
modifier = Modifier.animateItemPlacement(),
action = screen.actions.showMoreUsers
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyItemScope.UserItem(