diff --git a/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt b/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt index aa5fd88..f56844f 100644 --- a/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt +++ b/app/src/main/java/io/timelimit/android/ui/ScreenMultiplexer.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt b/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt index 2b25133..85019ce 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/MainModel.kt @@ -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() { diff --git a/app/src/main/java/io/timelimit/android/ui/model/UpdateStateCommand.kt b/app/src/main/java/io/timelimit/android/ui/model/UpdateStateCommand.kt index fa416c2..6e96793 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/UpdateStateCommand.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/UpdateStateCommand.kt @@ -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.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 } diff --git a/app/src/main/java/io/timelimit/android/ui/model/main/OverviewHandling.kt b/app/src/main/java/io/timelimit/android/ui/model/main/OverviewHandling.kt index 6b4adb4..9854b3d 100644 --- a/app/src/main/java/io/timelimit/android/ui/model/main/OverviewHandling.kt +++ b/app/src/main/java/io/timelimit/android/ui/model/main/OverviewHandling.kt @@ -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,17 +180,31 @@ object OverviewHandling { } } - stateLive.update { state -> - if (state is State.Overview) State.ManageChild.Main(state, user.id, fromRedirect = false) - else state - } - } - UserType.Parent -> stateLive.update { state -> - if (state is State.Overview) State.ManageParent.Main(state, user.id) - else state + UpdateStateCommand.Overview.ManageChild(user.id).applyTo(stateLive) } + UserType.Parent -> UpdateStateCommand.Overview.ManageParent(user.id).applyTo(stateLive) } } + }, + 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, diff --git a/app/src/main/java/io/timelimit/android/ui/overview/overview/Device.kt b/app/src/main/java/io/timelimit/android/ui/overview/overview/Device.kt index ac38645..5580924 100644 --- a/app/src/main/java/io/timelimit/android/ui/overview/overview/Device.kt +++ b/app/src/main/java/io/timelimit/android/ui/overview/overview/Device.kt @@ -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, diff --git a/app/src/main/java/io/timelimit/android/ui/overview/overview/Intro.kt b/app/src/main/java/io/timelimit/android/ui/overview/overview/Intro.kt new file mode 100644 index 0000000..8099c13 --- /dev/null +++ b/app/src/main/java/io/timelimit/android/ui/overview/overview/Intro.kt @@ -0,0 +1,196 @@ +/* + * TimeLimit Copyright 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 . + */ +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 + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewScreen.kt b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewScreen.kt index 206e6d6..087d884 100644 --- a/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewScreen.kt +++ b/app/src/main/java/io/timelimit/android/ui/overview/overview/OverviewScreen.kt @@ -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) } } diff --git a/app/src/main/java/io/timelimit/android/ui/overview/overview/User.kt b/app/src/main/java/io/timelimit/android/ui/overview/overview/User.kt index e312bc6..7cd85d5 100644 --- a/app/src/main/java/io/timelimit/android/ui/overview/overview/User.kt +++ b/app/src/main/java/io/timelimit/android/ui/overview/overview/User.kt @@ -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(