mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +02:00
Add Jetpack Compose
This commit is contained in:
parent
16d23273f5
commit
19aa402014
75 changed files with 2545 additions and 2276 deletions
|
@ -48,6 +48,7 @@ android {
|
||||||
buildConfigField 'int', 'minimumRecommendServerVersion', '5'
|
buildConfigField 'int', 'minimumRecommendServerVersion', '5'
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
|
compose true
|
||||||
viewBinding true
|
viewBinding true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,6 +152,10 @@ android {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "1.8"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = "1.4.0-alpha02"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wire {
|
wire {
|
||||||
|
@ -170,6 +175,9 @@ dependencies {
|
||||||
implementation 'androidx.cardview:cardview:1.0.0'
|
implementation 'androidx.cardview:cardview:1.0.0'
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
implementation "com.google.android.material:material:1.7.0"
|
implementation "com.google.android.material:material:1.7.0"
|
||||||
|
implementation 'androidx.compose.material:material:1.3.1'
|
||||||
|
implementation 'androidx.activity:activity-compose:1.6.1'
|
||||||
|
implementation 'androidx.compose.material:material-icons-extended:1.3.1'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.5.5'
|
implementation 'androidx.fragment:fragment-ktx:1.5.5'
|
||||||
|
|
||||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -21,6 +21,7 @@ import androidx.room.*
|
||||||
import io.timelimit.android.data.model.ChildTask
|
import io.timelimit.android.data.model.ChildTask
|
||||||
import io.timelimit.android.data.model.derived.ChildTaskWithCategoryTitle
|
import io.timelimit.android.data.model.derived.ChildTaskWithCategoryTitle
|
||||||
import io.timelimit.android.data.model.derived.FullChildTask
|
import io.timelimit.android.data.model.derived.FullChildTask
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface ChildTaskDao {
|
interface ChildTaskDao {
|
||||||
|
@ -45,9 +46,12 @@ interface ChildTaskDao {
|
||||||
@Query("SELECT child_task.* FROM child_task JOIN category ON (child_task.category_id = category.id) WHERE category.child_id = :userId")
|
@Query("SELECT child_task.* FROM child_task JOIN category ON (child_task.category_id = category.id) WHERE category.child_id = :userId")
|
||||||
fun getTasksByUserIdSync(userId: String): List<ChildTask>
|
fun getTasksByUserIdSync(userId: String): List<ChildTask>
|
||||||
|
|
||||||
@Query("SELECT child_task.*, category.title as category_title, user.name as child_name, user.timezone AS child_timezone FROM child_task JOIN category ON (child_task.category_id = category.id) JOIN user ON (category.child_id = user.id) WHERE child_task.pending_request = 1")
|
@Query("SELECT child_task.*, category.title as category_title, user.id AS child_id, user.name as child_name, user.timezone AS child_timezone FROM child_task JOIN category ON (child_task.category_id = category.id) JOIN user ON (category.child_id = user.id) WHERE child_task.pending_request = 1")
|
||||||
fun getPendingTasks(): LiveData<List<FullChildTask>>
|
fun getPendingTasks(): LiveData<List<FullChildTask>>
|
||||||
|
|
||||||
|
@Query("SELECT child_task.*, category.title as category_title, user.id AS child_id, user.name as child_name, user.timezone AS child_timezone FROM child_task JOIN category ON (child_task.category_id = category.id) JOIN user ON (category.child_id = user.id) WHERE child_task.pending_request = 1")
|
||||||
|
fun getPendingTasksFlow(): Flow<List<FullChildTask>>
|
||||||
|
|
||||||
@Query("SELECT * FROM child_task WHERE category_id = :categoryId")
|
@Query("SELECT * FROM child_task WHERE category_id = :categoryId")
|
||||||
fun getTasksByCategoryId(categoryId: String): LiveData<List<ChildTask>>
|
fun getTasksByCategoryId(categoryId: String): LiveData<List<ChildTask>>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -32,6 +32,8 @@ import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
import io.timelimit.android.livedata.map
|
import io.timelimit.android.livedata.map
|
||||||
import io.timelimit.android.sync.network.ServerDhKey
|
import io.timelimit.android.sync.network.ServerDhKey
|
||||||
import io.timelimit.android.update.UpdateStatus
|
import io.timelimit.android.update.UpdateStatus
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
|
@ -59,6 +61,13 @@ abstract class ConfigDao {
|
||||||
@Query("SELECT * FROM config WHERE id = :key")
|
@Query("SELECT * FROM config WHERE id = :key")
|
||||||
protected abstract suspend fun getRowCoroutine(key: ConfigurationItemType): ConfigurationItem?
|
protected abstract suspend fun getRowCoroutine(key: ConfigurationItemType): ConfigurationItem?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM config WHERE id = :key")
|
||||||
|
protected abstract fun getRowFlow(key: ConfigurationItemType): Flow<ConfigurationItem?>
|
||||||
|
|
||||||
|
private fun getValueOfKeyFlow(key: ConfigurationItemType): Flow<String?> {
|
||||||
|
return getRowFlow(key).map { it?.value }
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun getValueOfKeyCoroutine(key: ConfigurationItemType): String? {
|
private suspend fun getValueOfKeyCoroutine(key: ConfigurationItemType): String? {
|
||||||
return getRowCoroutine(key)?.value
|
return getRowCoroutine(key)?.value
|
||||||
}
|
}
|
||||||
|
@ -81,6 +90,10 @@ abstract class ConfigDao {
|
||||||
return getValueOfKeyAsync(ConfigurationItemType.OwnDeviceId)
|
return getValueOfKeyAsync(ConfigurationItemType.OwnDeviceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getOwnDeviceIdFlow(): Flow<String?> {
|
||||||
|
return getValueOfKeyFlow(ConfigurationItemType.OwnDeviceId)
|
||||||
|
}
|
||||||
|
|
||||||
fun getOwnDeviceIdSync(): String? {
|
fun getOwnDeviceIdSync(): String? {
|
||||||
return getValueOfKeySync(ConfigurationItemType.OwnDeviceId)
|
return getValueOfKeySync(ConfigurationItemType.OwnDeviceId)
|
||||||
}
|
}
|
||||||
|
@ -213,6 +226,7 @@ abstract class ConfigDao {
|
||||||
|
|
||||||
fun setServerMessage(message: String?) = updateValueSync(ConfigurationItemType.ServerMessage, message ?: "")
|
fun setServerMessage(message: String?) = updateValueSync(ConfigurationItemType.ServerMessage, message ?: "")
|
||||||
fun getServerMessage() = getValueOfKeyAsync(ConfigurationItemType.ServerMessage).map { if (it.isNullOrBlank()) null else it }
|
fun getServerMessage() = getValueOfKeyAsync(ConfigurationItemType.ServerMessage).map { if (it.isNullOrBlank()) null else it }
|
||||||
|
fun getServerMessageFlow() = getValueOfKeyFlow(ConfigurationItemType.ServerMessage).map { if (it.isNullOrBlank()) null else it }
|
||||||
|
|
||||||
fun getCustomServerUrlSync() = getValueOfKeySync(ConfigurationItemType.CustomServerUrl) ?: ""
|
fun getCustomServerUrlSync() = getValueOfKeySync(ConfigurationItemType.CustomServerUrl) ?: ""
|
||||||
fun getCustomServerUrlAsync() = getValueOfKeyAsync(ConfigurationItemType.CustomServerUrl).map { it ?: "" }
|
fun getCustomServerUrlAsync() = getValueOfKeyAsync(ConfigurationItemType.CustomServerUrl).map { it ?: "" }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -21,13 +21,15 @@ import io.timelimit.android.data.model.*
|
||||||
import io.timelimit.android.integration.platform.NewPermissionStatusConverter
|
import io.timelimit.android.integration.platform.NewPermissionStatusConverter
|
||||||
import io.timelimit.android.integration.platform.ProtectionLevelConverter
|
import io.timelimit.android.integration.platform.ProtectionLevelConverter
|
||||||
import io.timelimit.android.integration.platform.RuntimePermissionStatusConverter
|
import io.timelimit.android.integration.platform.RuntimePermissionStatusConverter
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@TypeConverters(
|
@TypeConverters(
|
||||||
NetworkTimeAdapter::class,
|
NetworkTimeAdapter::class,
|
||||||
ProtectionLevelConverter::class,
|
ProtectionLevelConverter::class,
|
||||||
RuntimePermissionStatusConverter::class,
|
RuntimePermissionStatusConverter::class,
|
||||||
NewPermissionStatusConverter::class
|
NewPermissionStatusConverter::class,
|
||||||
|
UserTypeConverter::class
|
||||||
)
|
)
|
||||||
abstract class DeviceDao {
|
abstract class DeviceDao {
|
||||||
@Query("SELECT * FROM device WHERE id = :deviceId")
|
@Query("SELECT * FROM device WHERE id = :deviceId")
|
||||||
|
@ -45,6 +47,9 @@ abstract class DeviceDao {
|
||||||
@Query("SELECT * FROM device")
|
@Query("SELECT * FROM device")
|
||||||
abstract fun getAllDevicesSync(): List<Device>
|
abstract fun getAllDevicesSync(): List<Device>
|
||||||
|
|
||||||
|
@Query("SELECT device.*, user.name AS current_user_name, user.type AS current_user_type FROM device LEFT JOIN user ON (user.id = device.current_user_id)")
|
||||||
|
abstract fun getAllDevicesWithUserInfoFlow(): Flow<List<DeviceWithUserInfo>>
|
||||||
|
|
||||||
@Insert
|
@Insert
|
||||||
abstract fun addDeviceSync(device: Device)
|
abstract fun addDeviceSync(device: Device)
|
||||||
|
|
||||||
|
@ -117,3 +122,12 @@ data class DeviceDetailDataBase(
|
||||||
@ColumnInfo(name = "app_diff_version")
|
@ColumnInfo(name = "app_diff_version")
|
||||||
val appDiffVersion: String?
|
val appDiffVersion: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class DeviceWithUserInfo (
|
||||||
|
@Embedded
|
||||||
|
val device: Device,
|
||||||
|
@ColumnInfo(name = "current_user_name")
|
||||||
|
val currentUserName: String?,
|
||||||
|
@ColumnInfo(name = "current_user_type")
|
||||||
|
val currentUserType: UserType?
|
||||||
|
)
|
|
@ -21,6 +21,7 @@ import androidx.room.Insert
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
import io.timelimit.android.data.model.User
|
import io.timelimit.android.data.model.User
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
abstract class UserDao {
|
abstract class UserDao {
|
||||||
|
@ -45,6 +46,9 @@ abstract class UserDao {
|
||||||
@Query("SELECT * FROM user ORDER by type DESC, name ASC")
|
@Query("SELECT * FROM user ORDER by type DESC, name ASC")
|
||||||
abstract fun getAllUsersLive(): LiveData<List<User>>
|
abstract fun getAllUsersLive(): LiveData<List<User>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM user ORDER by type DESC, name ASC")
|
||||||
|
abstract fun getAllUsersFlow(): Flow<List<User>>
|
||||||
|
|
||||||
@Query("SELECT * FROM user")
|
@Query("SELECT * FROM user")
|
||||||
abstract fun getAllUsersSync(): List<User>
|
abstract fun getAllUsersSync(): List<User>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -25,6 +25,8 @@ data class FullChildTask(
|
||||||
val childTask: ChildTask,
|
val childTask: ChildTask,
|
||||||
@ColumnInfo(name = "category_title")
|
@ColumnInfo(name = "category_title")
|
||||||
val categoryTitle: String,
|
val categoryTitle: String,
|
||||||
|
@ColumnInfo(name = "child_id")
|
||||||
|
val childId: String,
|
||||||
@ColumnInfo(name = "child_name")
|
@ColumnInfo(name = "child_name")
|
||||||
val childName: String,
|
val childName: String,
|
||||||
@ColumnInfo(name = "child_timezone")
|
@ColumnInfo(name = "child_timezone")
|
||||||
|
|
38
app/src/main/java/io/timelimit/android/extensions/Flow.kt
Normal file
38
app/src/main/java/io/timelimit/android/extensions/Flow.kt
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* 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.extensions
|
||||||
|
|
||||||
|
import io.timelimit.android.util.Option
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
|
fun <T> Flow<T?>.takeWhileNotNull(): Flow<T> = this.transformWhile { value ->
|
||||||
|
if (value != null) {
|
||||||
|
emit(value)
|
||||||
|
|
||||||
|
true
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun <T> Flow<Boolean>.whileTrue(producer: suspend () -> Flow<T>): Flow<T> =
|
||||||
|
distinctUntilChanged()
|
||||||
|
.transformLatest {
|
||||||
|
if (it) emitAll(producer().map { Option.Some(it) })
|
||||||
|
else emit(null)
|
||||||
|
}
|
||||||
|
.takeWhileNotNull()
|
||||||
|
.map { it.value }
|
|
@ -0,0 +1,11 @@
|
||||||
|
package io.timelimit.android.extensions
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentContainerView
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
|
||||||
|
private val onContainerAvailable = FragmentManager::class.java.getDeclaredMethod(
|
||||||
|
"onContainerAvailable",
|
||||||
|
FragmentContainerView::class.java
|
||||||
|
).also { it.isAccessible = true }
|
||||||
|
|
||||||
|
fun FragmentManager.onContainerAvailable(view: FragmentContainerView) = onContainerAvailable.invoke(this, view)
|
89
app/src/main/java/io/timelimit/android/ui/FragmentScreen.kt
Normal file
89
app/src/main/java/io/timelimit/android/ui/FragmentScreen.kt
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.fragment.app.FragmentContainerView
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import io.timelimit.android.BuildConfig
|
||||||
|
import io.timelimit.android.extensions.onContainerAvailable
|
||||||
|
import io.timelimit.android.ui.model.Screen
|
||||||
|
|
||||||
|
private const val LOG_TAG = "FragmentScreen"
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FragmentScreen(
|
||||||
|
screen: Screen.FragmentScreen,
|
||||||
|
fragmentManager: FragmentManager,
|
||||||
|
fragmentIds: MutableSet<Int>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
factory = { context ->
|
||||||
|
FragmentContainerView(context).also {
|
||||||
|
val containerId = screen.fragment.containerId
|
||||||
|
val fragment = fragmentManager.findFragmentById(containerId)
|
||||||
|
|
||||||
|
it.id = containerId
|
||||||
|
|
||||||
|
if (fragment == null) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "connect $screen")
|
||||||
|
}
|
||||||
|
|
||||||
|
fragmentManager.beginTransaction()
|
||||||
|
.replace(
|
||||||
|
containerId,
|
||||||
|
screen.fragment.fragmentClass,
|
||||||
|
screen.fragment.arguments
|
||||||
|
)
|
||||||
|
.commitAllowingStateLoss()
|
||||||
|
} else {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "already bound $screen")
|
||||||
|
}
|
||||||
|
|
||||||
|
fragmentManager.beginTransaction()
|
||||||
|
.attach(fragment)
|
||||||
|
.commitAllowingStateLoss()
|
||||||
|
}
|
||||||
|
|
||||||
|
fragmentManager.onContainerAvailable(it)
|
||||||
|
fragmentIds.add(containerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
DisposableEffect(true) {
|
||||||
|
onDispose {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "detach $screen")
|
||||||
|
}
|
||||||
|
|
||||||
|
fragmentManager.findFragmentById(screen.fragment.containerId)?.also { fragment ->
|
||||||
|
fragmentManager.beginTransaction()
|
||||||
|
.detach(fragment)
|
||||||
|
.commitAllowingStateLoss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -20,48 +20,61 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.view.MenuItem
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.*
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.lifecycle.Transformations
|
|
||||||
import androidx.lifecycle.ViewModelProviders
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.NavDestination
|
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
|
||||||
import io.timelimit.android.Application
|
import io.timelimit.android.Application
|
||||||
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.IdGenerator
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.model.UserType
|
||||||
import io.timelimit.android.extensions.showSafe
|
import io.timelimit.android.extensions.showSafe
|
||||||
import io.timelimit.android.integration.platform.android.NotificationChannels
|
import io.timelimit.android.integration.platform.android.NotificationChannels
|
||||||
import io.timelimit.android.livedata.ignoreUnchanged
|
import io.timelimit.android.livedata.ignoreUnchanged
|
||||||
import io.timelimit.android.livedata.liveDataFromNullableValue
|
import io.timelimit.android.livedata.liveDataFromNullableValue
|
||||||
import io.timelimit.android.livedata.map
|
import io.timelimit.android.livedata.map
|
||||||
import io.timelimit.android.livedata.switchMap
|
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.u2f.U2fManager
|
import io.timelimit.android.u2f.U2fManager
|
||||||
import io.timelimit.android.u2f.protocol.U2FDevice
|
import io.timelimit.android.u2f.protocol.U2FDevice
|
||||||
|
import io.timelimit.android.ui.animation.Transition
|
||||||
import io.timelimit.android.ui.login.AuthTokenLoginProcessor
|
import io.timelimit.android.ui.login.AuthTokenLoginProcessor
|
||||||
import io.timelimit.android.ui.login.NewLoginFragment
|
import io.timelimit.android.ui.login.NewLoginFragment
|
||||||
import io.timelimit.android.ui.main.ActivityViewModel
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
import io.timelimit.android.ui.main.AuthenticatedUser
|
import io.timelimit.android.ui.main.AuthenticatedUser
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
import io.timelimit.android.ui.manage.parent.ManageParentFragmentArgs
|
import io.timelimit.android.ui.model.*
|
||||||
import io.timelimit.android.ui.payment.ActivityPurchaseModel
|
import io.timelimit.android.ui.payment.ActivityPurchaseModel
|
||||||
import io.timelimit.android.ui.util.SyncStatusModel
|
import io.timelimit.android.ui.util.SyncStatusModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.DeviceFoundListener {
|
class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.DeviceFoundListener, MainModelActivity {
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val LOG_TAG = "MainActivity"
|
||||||
private const val AUTH_DIALOG_TAG = "adt"
|
private const val AUTH_DIALOG_TAG = "adt"
|
||||||
const val ACTION_USER_OPTIONS = "OPEN_USER_OPTIONS"
|
const val ACTION_USER_OPTIONS = "OPEN_USER_OPTIONS"
|
||||||
const val EXTRA_USER_ID = "userId"
|
const val EXTRA_USER_ID = "userId"
|
||||||
private const val EXTRA_AUTH_HANDOVER = "authHandover"
|
private const val EXTRA_AUTH_HANDOVER = "authHandover"
|
||||||
|
private const val MAIN_MODEL_STATE = "mainModelState"
|
||||||
|
private const val FRAGMENT_IDS_STATE = "fragmentIds"
|
||||||
|
|
||||||
private var authHandover: Triple<Long, Long, AuthenticatedUser>? = null
|
private var authHandover: Triple<Long, Long, AuthenticatedUser>? = null
|
||||||
|
|
||||||
|
@ -95,7 +108,8 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val currentNavigatorFragment = MutableLiveData<Fragment?>()
|
private val mainModel by viewModels<MainModel>()
|
||||||
|
private var fragmentIds = mutableSetOf<Int>()
|
||||||
private val syncModel: SyncStatusModel by lazy {
|
private val syncModel: SyncStatusModel by lazy {
|
||||||
ViewModelProviders.of(this).get(SyncStatusModel::class.java)
|
ViewModelProviders.of(this).get(SyncStatusModel::class.java)
|
||||||
}
|
}
|
||||||
|
@ -103,114 +117,162 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
|
||||||
override var ignoreStop: Boolean = false
|
override var ignoreStop: Boolean = false
|
||||||
override val showPasswordRecovery: Boolean = true
|
override val showPasswordRecovery: Boolean = true
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
|
||||||
|
supportActionBar!!.hide()
|
||||||
|
|
||||||
U2fManager.setupActivity(this)
|
U2fManager.setupActivity(this)
|
||||||
|
|
||||||
NotificationChannels.createNotificationChannels(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager, this)
|
NotificationChannels.createNotificationChannels(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager, this)
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState != null) {
|
||||||
NavHostFragment.create(R.navigation.nav_graph).let { navhost ->
|
mainModel.state.value = savedInstanceState.getSerializable(MAIN_MODEL_STATE) as State
|
||||||
supportFragmentManager.beginTransaction()
|
fragmentIds.addAll(savedInstanceState.getIntegerArrayList(FRAGMENT_IDS_STATE) ?: emptyList())
|
||||||
.replace(R.id.nav_host, navhost)
|
|
||||||
.setPrimaryNavigationFragment(navhost)
|
|
||||||
.commitNow()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// init the purchaseModel
|
// init the purchaseModel
|
||||||
purchaseModel.getApplication<Application>()
|
purchaseModel.getApplication<Application>()
|
||||||
|
|
||||||
// prepare livedata
|
|
||||||
val customTitle = currentNavigatorFragment.switchMap {
|
|
||||||
if (it != null && it is FragmentWithCustomTitle) {
|
|
||||||
it.getCustomTitle()
|
|
||||||
} else {
|
|
||||||
liveDataFromNullableValue(null as String?)
|
|
||||||
}
|
|
||||||
}.ignoreUnchanged()
|
|
||||||
|
|
||||||
val title = Transformations.map(customTitle) {
|
|
||||||
if (it == null) {
|
|
||||||
getString(R.string.app_name)
|
|
||||||
} else {
|
|
||||||
it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// up button
|
|
||||||
getNavController().addOnDestinationChangedListener(object: NavController.OnDestinationChangedListener {
|
|
||||||
override fun onDestinationChanged(controller: NavController, destination: NavDestination, arguments: Bundle?) {
|
|
||||||
supportActionBar!!.setDisplayHomeAsUpEnabled(controller.previousBackStackEntry != null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// init if not yet done
|
// init if not yet done
|
||||||
DefaultAppLogic.with(this)
|
DefaultAppLogic.with(this)
|
||||||
|
|
||||||
val fragmentContainer = supportFragmentManager.findFragmentById(R.id.nav_host)!!
|
val fragments = MutableStateFlow(emptyMap<Int, Fragment>())
|
||||||
val fragmentContainerManager = fragmentContainer.childFragmentManager
|
|
||||||
|
|
||||||
fragmentContainerManager.registerFragmentLifecycleCallbacks(object: FragmentManager.FragmentLifecycleCallbacks() {
|
supportFragmentManager.registerFragmentLifecycleCallbacks(object: FragmentManager.FragmentLifecycleCallbacks() {
|
||||||
override fun onFragmentStarted(fm: FragmentManager, f: Fragment) {
|
override fun onFragmentStarted(fm: FragmentManager, f: Fragment) {
|
||||||
super.onFragmentStarted(fm, f)
|
super.onFragmentStarted(fm, f)
|
||||||
|
|
||||||
if (!(f is DialogFragment)) {
|
fragments.update {
|
||||||
currentNavigatorFragment.value = f
|
it + Pair(f.id, f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFragmentStopped(fm: FragmentManager, f: Fragment) {
|
override fun onFragmentStopped(fm: FragmentManager, f: Fragment) {
|
||||||
super.onFragmentStopped(fm, f)
|
super.onFragmentStopped(fm, f)
|
||||||
|
|
||||||
if (currentNavigatorFragment.value === f) {
|
fragments.update {
|
||||||
currentNavigatorFragment.value = null
|
it - f.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanupFragments()
|
||||||
}
|
}
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
title.observe(this, Observer { setTitle(it) })
|
|
||||||
syncModel.statusText.observe(this, Observer { supportActionBar!!.subtitle = it })
|
|
||||||
|
|
||||||
handleParameters(intent)
|
handleParameters(intent)
|
||||||
|
|
||||||
val hasDeviceId = getActivityViewModel().logic.deviceId.map { it != null }.ignoreUnchanged()
|
val hasDeviceId = getActivityViewModel().logic.deviceId.map { it != null }.ignoreUnchanged()
|
||||||
val hasParentKey = getActivityViewModel().logic.database.config().getParentModeKeyLive().map { it != null }.ignoreUnchanged()
|
val hasParentKey = getActivityViewModel().logic.database.config().getParentModeKeyLive().map { it != null }.ignoreUnchanged()
|
||||||
|
|
||||||
hasDeviceId.observe(this) {
|
hasDeviceId.observe(this) {
|
||||||
val rootDestination = getNavController().backQueue.getOrNull(1)?.destination?.id
|
val rootDestination = mainModel.state.value.first()
|
||||||
|
|
||||||
if (!it) getActivityViewModel().logOut()
|
if (!it) getActivityViewModel().logOut()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
it && rootDestination != R.id.overviewFragment ||
|
it && rootDestination !is State.Overview ||
|
||||||
!it && rootDestination == R.id.overviewFragment
|
!it && rootDestination is State.Overview
|
||||||
) {
|
) {
|
||||||
restartContent()
|
restartContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasParentKey.observe(this) {
|
hasParentKey.observe(this) {
|
||||||
val rootDestination = getNavController().backQueue.getOrNull(1)?.destination?.id
|
val rootDestination = mainModel.state.value.first()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
it && rootDestination != R.id.parentModeFragment ||
|
it && rootDestination !is State.ParentMode ||
|
||||||
!it && rootDestination == R.id.parentModeFragment
|
!it && rootDestination is State.ParentMode
|
||||||
) {
|
) {
|
||||||
restartContent()
|
restartContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
Theme {
|
||||||
|
val screenLive by mainModel.screen.collectAsState(initial = null)
|
||||||
|
val subtitleLive by syncModel.statusText.asFlow().collectAsState(initial = null)
|
||||||
|
|
||||||
|
val isAuthenticated by getActivityViewModel().authenticatedUser
|
||||||
|
.map { it?.second?.type == UserType.Parent }
|
||||||
|
.asFlow().collectAsState(initial = false)
|
||||||
|
|
||||||
|
BackHandler(enabled = screenLive?.state?.previous != null) {
|
||||||
|
execute(UpdateStateCommand.BackToPreviousScreen)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem) = when {
|
updateTransition(
|
||||||
item.itemId == android.R.id.home -> {
|
targetState = screenLive,
|
||||||
onBackPressed()
|
label = "AnimatedContent"
|
||||||
|
).AnimatedContent(
|
||||||
true
|
modifier = Modifier.background(Color.Black),
|
||||||
|
contentKey = { screen ->
|
||||||
|
when (screen) {
|
||||||
|
is Screen.FragmentScreen -> screen.fragment.containerId
|
||||||
|
is Screen.OverviewScreen -> "overview"
|
||||||
|
null -> null
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
},
|
||||||
|
transitionSpec = {
|
||||||
|
val from = initialState
|
||||||
|
val to = targetState
|
||||||
|
|
||||||
|
if (from == null || to == null) Transition.none
|
||||||
|
else {
|
||||||
|
val isOpening = to.state.hasPrevious(from.state)
|
||||||
|
val isClosing = from.state.hasPrevious(to.state)
|
||||||
|
|
||||||
|
if (isOpening) Transition.openScreen
|
||||||
|
else if (isClosing) Transition.closeScreen
|
||||||
|
else Transition.none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { screen ->
|
||||||
|
val showAuthenticationDialog =
|
||||||
|
if (screen is ScreenWithAuthenticationFab && !isAuthenticated) ::showAuthenticationScreen
|
||||||
|
else null
|
||||||
|
|
||||||
|
val fragmentsLive by fragments.collectAsState()
|
||||||
|
|
||||||
|
val customTitle by when (screen) {
|
||||||
|
is Screen.FragmentScreen -> {
|
||||||
|
when (val fragment = fragmentsLive[screen.fragment.containerId]) {
|
||||||
|
is FragmentWithCustomTitle -> fragment.getCustomTitle()
|
||||||
|
else -> liveDataFromNullableValue(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> liveDataFromNullableValue(null)
|
||||||
|
}.asFlow().collectAsState(initial = null)
|
||||||
|
|
||||||
|
ScreenScaffold(
|
||||||
|
screen = screen,
|
||||||
|
title = customTitle ?: stringResource(R.string.app_name),
|
||||||
|
subtitle = subtitleLive,
|
||||||
|
executeCommand = ::execute,
|
||||||
|
content = { paddingValues ->
|
||||||
|
ScreenMultiplexer(
|
||||||
|
screen = screen,
|
||||||
|
executeCommand = ::execute,
|
||||||
|
fragmentManager = supportFragmentManager,
|
||||||
|
fragmentIds = fragmentIds,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
showAuthenticationDialog = showAuthenticationDialog
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
|
outState.putSerializable(MAIN_MODEL_STATE, mainModel.state.value)
|
||||||
|
outState.putIntegerArrayList(FRAGMENT_IDS_STATE, ArrayList(fragmentIds))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
@ -252,17 +314,7 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
|
||||||
val valid = userId != null && try { IdGenerator.assertIdValid(userId); true } catch (ex: IllegalArgumentException) {false}
|
val valid = userId != null && try { IdGenerator.assertIdValid(userId); true } catch (ex: IllegalArgumentException) {false}
|
||||||
|
|
||||||
if (userId != null && valid) {
|
if (userId != null && valid) {
|
||||||
getNavController().popBackStack(R.id.overviewFragment, true)
|
execute(UpdateStateCommand.RecoverPassword(userId))
|
||||||
getNavController().handleDeepLink(
|
|
||||||
getNavController().createDeepLink()
|
|
||||||
.setDestination(R.id.manageParentFragment)
|
|
||||||
.setArguments(ManageParentFragmentArgs(userId).toBundle())
|
|
||||||
.createTaskStackBuilder()
|
|
||||||
.intents
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,47 +329,53 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleParameters(intent)) return
|
if (handleParameters(intent)) return
|
||||||
|
|
||||||
// at these screens, some users restart the App
|
|
||||||
// if they want to continue after opening the mail
|
|
||||||
// because they don't understand how to use the list of running Apps ...
|
|
||||||
// Due to that, on the relevant screens, the App does not
|
|
||||||
// go back to the start when opening it again
|
|
||||||
val isImportantScreen = when (getNavController().currentDestination?.id) {
|
|
||||||
R.id.setupParentModeFragment -> true
|
|
||||||
R.id.restoreParentPasswordFragment -> true
|
|
||||||
R.id.linkParentMailFragment -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isImportantScreen) restartContent()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restartContent() {
|
private fun restartContent() {
|
||||||
while (getNavController().popBackStack()) {/* do nothing */}
|
mainModel.execute(UpdateStateCommand.Reset)
|
||||||
|
|
||||||
getNavController().clearBackStack(R.id.launchFragment)
|
|
||||||
getNavController().navigate(R.id.launchFragment)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getActivityViewModel(): ActivityViewModel {
|
override fun getActivityViewModel(): ActivityViewModel {
|
||||||
return ViewModelProviders.of(this).get(ActivityViewModel::class.java)
|
return ViewModelProviders.of(this).get(ActivityViewModel::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNavHostFragment(): NavHostFragment {
|
|
||||||
return supportFragmentManager.findFragmentById(R.id.nav_host) as NavHostFragment
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNavController(): NavController {
|
|
||||||
return getNavHostFragment().navController
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showAuthenticationScreen() {
|
override fun showAuthenticationScreen() {
|
||||||
if (supportFragmentManager.findFragmentByTag(AUTH_DIALOG_TAG) == null) {
|
if (supportFragmentManager.findFragmentByTag(AUTH_DIALOG_TAG) == null) {
|
||||||
NewLoginFragment().showSafe(supportFragmentManager, AUTH_DIALOG_TAG)
|
NewLoginFragment().showSafe(supportFragmentManager, AUTH_DIALOG_TAG)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun cleanupFragments() {
|
||||||
|
fragmentIds
|
||||||
|
.filter { fragmentId ->
|
||||||
|
var v = mainModel.state.value as State?
|
||||||
|
|
||||||
|
while (v != null) {
|
||||||
|
if (v is FragmentState && v.containerId == fragmentId) return@filter false
|
||||||
|
|
||||||
|
v = v.previous
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
.map { supportFragmentManager.findFragmentById(it) }
|
||||||
|
.filterNotNull()
|
||||||
|
.filter { it.isDetached }
|
||||||
|
.forEach {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(LOG_TAG, "remove fragment $it")
|
||||||
|
}
|
||||||
|
|
||||||
|
val id = it.id
|
||||||
|
|
||||||
|
supportFragmentManager.beginTransaction()
|
||||||
|
.remove(it)
|
||||||
|
.commitAllowingStateLoss()
|
||||||
|
|
||||||
|
fragmentIds.remove(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
@ -331,4 +389,6 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeviceFound(device: U2FDevice) = AuthTokenLoginProcessor.process(device, getActivityViewModel())
|
override fun onDeviceFound(device: U2FDevice) = AuthTokenLoginProcessor.process(device, getActivityViewModel())
|
||||||
|
|
||||||
|
override fun execute(command: UpdateStateCommand) = mainModel.execute(command)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import io.timelimit.android.ui.model.Screen
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.overview.overview.OverviewScreen
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScreenMultiplexer(
|
||||||
|
screen: Screen?,
|
||||||
|
executeCommand: (UpdateStateCommand) -> Unit,
|
||||||
|
fragmentManager: FragmentManager,
|
||||||
|
fragmentIds: MutableSet<Int>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
when (screen) {
|
||||||
|
null -> {/* nothing to do */ }
|
||||||
|
is Screen.OverviewScreen -> OverviewScreen(screen.content, executeCommand, modifier = modifier)
|
||||||
|
is Screen.FragmentScreen -> FragmentScreen(screen, fragmentManager, fragmentIds, modifier = modifier)
|
||||||
|
}
|
||||||
|
}
|
111
app/src/main/java/io/timelimit/android/ui/ScreenScaffold.kt
Normal file
111
app/src/main/java/io/timelimit/android/ui/ScreenScaffold.kt
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.LockOpen
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.ui.model.Screen
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScreenScaffold(
|
||||||
|
screen: Screen?,
|
||||||
|
title: String,
|
||||||
|
subtitle: String?,
|
||||||
|
content: @Composable (PaddingValues) -> Unit,
|
||||||
|
executeCommand: (UpdateStateCommand) -> Unit,
|
||||||
|
showAuthenticationDialog: (() -> Unit)?
|
||||||
|
) {
|
||||||
|
var expandDropdown by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
if (subtitle != null) {
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
style = MaterialTheme.typography.subtitle1,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = if (screen?.state?.previous != null) ({
|
||||||
|
IconButton(onClick = { executeCommand(UpdateStateCommand.BackToPreviousScreen) }) {
|
||||||
|
Icon(Icons.Default.ArrowBack, stringResource(R.string.generic_back))
|
||||||
|
}
|
||||||
|
}) else null,
|
||||||
|
actions = {
|
||||||
|
for (icon in screen?.toolbarIcons ?: emptyList()) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
executeCommand(icon.action)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(icon.icon, stringResource(icon.labelResource))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screen?.toolbarOptions?.isEmpty() == false) {
|
||||||
|
IconButton(onClick = { expandDropdown = true }) {
|
||||||
|
Icon(Icons.Default.MoreVert, stringResource(R.string.generic_menu))
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expandDropdown,
|
||||||
|
onDismissRequest = { expandDropdown = false }
|
||||||
|
) {
|
||||||
|
for (option in screen.toolbarOptions) {
|
||||||
|
DropdownMenuItem(onClick = {
|
||||||
|
executeCommand(option.action)
|
||||||
|
expandDropdown = false
|
||||||
|
}) {
|
||||||
|
Text(stringResource(option.labelResource))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
if (showAuthenticationDialog != null) {
|
||||||
|
FloatingActionButton(onClick = showAuthenticationDialog) {
|
||||||
|
Icon(Icons.Default.LockOpen, stringResource(R.string.authentication_action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
39
app/src/main/java/io/timelimit/android/ui/Theme.kt
Normal file
39
app/src/main/java/io/timelimit/android/ui/Theme.kt
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import io.timelimit.android.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Theme(
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val resources = LocalContext.current.resources
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
content = content,
|
||||||
|
colors = MaterialTheme.colors.copy(
|
||||||
|
primary = Color(resources.getColor(R.color.colorPrimary)),
|
||||||
|
primaryVariant = Color(resources.getColor(R.color.colorPrimaryDark)),
|
||||||
|
secondary = Color(resources.getColor(R.color.colorAccent)),
|
||||||
|
onSecondary = Color.White
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package io.timelimit.android.ui.animation
|
||||||
|
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
|
object Transition {
|
||||||
|
val openScreen = ContentTransform(
|
||||||
|
targetContentEnter = slideInHorizontally { it },
|
||||||
|
initialContentExit = slideOutHorizontally() + fadeOut(targetAlpha = .5f)
|
||||||
|
)
|
||||||
|
|
||||||
|
val closeScreen = ContentTransform(
|
||||||
|
targetContentEnter = slideInHorizontally { -it / 2 } + fadeIn(initialAlpha = .5f),
|
||||||
|
initialContentExit = slideOutHorizontally { it },
|
||||||
|
targetContentZIndex = -1f
|
||||||
|
)
|
||||||
|
|
||||||
|
val none = ContentTransform(
|
||||||
|
targetContentEnter = EnterTransition.None,
|
||||||
|
initialContentExit = ExitTransition.None
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -22,72 +22,50 @@ import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.databinding.FragmentDiagnoseMainBinding
|
import io.timelimit.android.databinding.FragmentDiagnoseMainBinding
|
||||||
import io.timelimit.android.extensions.safeNavigate
|
|
||||||
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
||||||
import io.timelimit.android.livedata.liveDataFromNullableValue
|
import io.timelimit.android.livedata.liveDataFromNullableValue
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
import io.timelimit.android.ui.main.AuthenticationFab
|
import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
class DiagnoseMainFragment : Fragment(), FragmentWithCustomTitle {
|
class DiagnoseMainFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
val binding = FragmentDiagnoseMainBinding.inflate(inflater, container, false)
|
val binding = FragmentDiagnoseMainBinding.inflate(inflater, container, false)
|
||||||
val navigation = Navigation.findNavController(container!!)
|
|
||||||
val logic = DefaultAppLogic.with(requireContext())
|
val logic = DefaultAppLogic.with(requireContext())
|
||||||
val activity: ActivityViewModelHolder = activity as ActivityViewModelHolder
|
val activity: ActivityViewModelHolder = activity as ActivityViewModelHolder
|
||||||
val auth = activity.getActivityViewModel()
|
val auth = activity.getActivityViewModel()
|
||||||
|
|
||||||
binding.diagnoseClockButton.setOnClickListener {
|
binding.diagnoseClockButton.setOnClickListener {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.Diagnose.Clock)
|
||||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseClockFragment(),
|
|
||||||
R.id.diagnoseMainFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.diagnoseConnectionButton.setOnClickListener {
|
binding.diagnoseConnectionButton.setOnClickListener {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.Diagnose.Connection)
|
||||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseConnectionFragment(),
|
|
||||||
R.id.diagnoseMainFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.diagnoseSyncButton.setOnClickListener {
|
binding.diagnoseSyncButton.setOnClickListener {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.Diagnose.Sync)
|
||||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseSyncFragment(),
|
|
||||||
R.id.diagnoseMainFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.diagnoseCryButton.setOnClickListener {
|
binding.diagnoseCryButton.setOnClickListener {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.Diagnose.Crypto)
|
||||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseCryptoFragment(),
|
|
||||||
R.id.diagnoseMainFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.diagnoseBatteryButton.setOnClickListener {
|
binding.diagnoseBatteryButton.setOnClickListener {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.Diagnose.Battery)
|
||||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseBatteryFragment(),
|
|
||||||
R.id.diagnoseMainFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.diagnoseFgaButton.setOnClickListener {
|
binding.diagnoseFgaButton.setOnClickListener {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.Diagnose.ForegroundApp)
|
||||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseForegroundAppFragment(),
|
|
||||||
R.id.diagnoseMainFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.diagnoseExfButton.setOnClickListener {
|
binding.diagnoseExfButton.setOnClickListener {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.Diagnose.ExperimentalFlags)
|
||||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseExperimentalFlagFragment(),
|
|
||||||
R.id.diagnoseMainFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logic.backgroundTaskLogic.lastLoopException.observe(this, Observer { ex ->
|
logic.backgroundTaskLogic.lastLoopException.observe(this, Observer { ex ->
|
||||||
|
@ -108,10 +86,7 @@ class DiagnoseMainFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.diagnoseExitReasonsButton.setOnClickListener {
|
binding.diagnoseExitReasonsButton.setOnClickListener {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.Diagnose.ExitReasons)
|
||||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseExitReasonFragment(),
|
|
||||||
R.id.diagnoseMainFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationFab.manageAuthenticationFab(
|
AuthenticationFab.manageAuthenticationFab(
|
||||||
|
|
|
@ -27,6 +27,8 @@ import io.timelimit.android.livedata.switchMap
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
import io.timelimit.android.ui.manage.category.blocked_times.BlockedTimeAreasFragment
|
import io.timelimit.android.ui.manage.category.blocked_times.BlockedTimeAreasFragment
|
||||||
import io.timelimit.android.ui.manage.category.settings.CategorySettingsFragment
|
import io.timelimit.android.ui.manage.category.settings.CategorySettingsFragment
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
abstract class CategoryFragmentWrapper: SingleFragmentWrapper(), FragmentWithCustomTitle {
|
abstract class CategoryFragmentWrapper: SingleFragmentWrapper(), FragmentWithCustomTitle {
|
||||||
abstract val childId: String
|
abstract val childId: String
|
||||||
|
@ -44,7 +46,7 @@ abstract class CategoryFragmentWrapper: SingleFragmentWrapper(), FragmentWithCus
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
category.observe(viewLifecycleOwner) {
|
category.observe(viewLifecycleOwner) {
|
||||||
if (it == null) navigation.popBackStack()
|
if (it == null) requireActivity().execute(UpdateStateCommand.ManageChild.LeaveCategory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,8 @@ import io.timelimit.android.ui.manage.category.usagehistory.UsageHistoryFragment
|
||||||
import io.timelimit.android.ui.manage.child.advanced.ManageChildAdvancedFragment
|
import io.timelimit.android.ui.manage.child.advanced.ManageChildAdvancedFragment
|
||||||
import io.timelimit.android.ui.manage.child.apps.ChildAppsFragment
|
import io.timelimit.android.ui.manage.child.apps.ChildAppsFragment
|
||||||
import io.timelimit.android.ui.manage.child.tasks.ManageChildTasksFragment
|
import io.timelimit.android.ui.manage.child.tasks.ManageChildTasksFragment
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
abstract class ChildFragmentWrapper: SingleFragmentWrapper() {
|
abstract class ChildFragmentWrapper: SingleFragmentWrapper() {
|
||||||
abstract val childId: String
|
abstract val childId: String
|
||||||
|
@ -37,7 +39,7 @@ abstract class ChildFragmentWrapper: SingleFragmentWrapper() {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
child.observe(viewLifecycleOwner) {
|
child.observe(viewLifecycleOwner) {
|
||||||
if (it == null) navigation.popBackStack()
|
if (it == null) requireActivity().execute(UpdateStateCommand.ManageChild.LeaveChild)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -20,8 +20,6 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.databinding.SingleFragmentWrapperBinding
|
import io.timelimit.android.databinding.SingleFragmentWrapperBinding
|
||||||
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
||||||
|
@ -30,14 +28,9 @@ import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
|
|
||||||
abstract class SingleFragmentWrapper: Fragment() {
|
abstract class SingleFragmentWrapper: Fragment() {
|
||||||
val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||||
private lateinit var navController: NavController
|
|
||||||
protected lateinit var binding: SingleFragmentWrapperBinding
|
protected lateinit var binding: SingleFragmentWrapperBinding
|
||||||
|
|
||||||
protected val navigation get() = navController
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
navController = Navigation.findNavController(container!!)
|
|
||||||
|
|
||||||
binding = SingleFragmentWrapperBinding.inflate(inflater, container, false)
|
binding = SingleFragmentWrapperBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
AuthenticationFab.manageAuthenticationFab(
|
AuthenticationFab.manageAuthenticationFab(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -17,8 +17,8 @@
|
||||||
package io.timelimit.android.ui.fragment
|
package io.timelimit.android.ui.fragment
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
import io.timelimit.android.extensions.safeNavigate
|
import io.timelimit.android.ui.model.execute
|
||||||
import io.timelimit.android.ui.overview.about.AboutFragment
|
import io.timelimit.android.ui.overview.about.AboutFragment
|
||||||
import io.timelimit.android.ui.overview.about.AboutFragmentParentHandlers
|
import io.timelimit.android.ui.overview.about.AboutFragmentParentHandlers
|
||||||
|
|
||||||
|
@ -27,23 +27,14 @@ class AboutFragmentWrapped: SingleFragmentWrapper(), AboutFragmentParentHandlers
|
||||||
override fun createChildFragment(): Fragment = AboutFragment()
|
override fun createChildFragment(): Fragment = AboutFragment()
|
||||||
|
|
||||||
override fun onShowDiagnoseScreen() {
|
override fun onShowDiagnoseScreen() {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.About.Diagnose)
|
||||||
AboutFragmentWrappedDirections.actionAboutFragmentWrappedToDiagnoseMainFragment(),
|
|
||||||
R.id.aboutFragmentWrapped
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShowPurchaseScreen() {
|
override fun onShowPurchaseScreen() {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.About.Purchase)
|
||||||
AboutFragmentWrappedDirections.actionAboutFragmentWrappedToPurchaseFragment(),
|
|
||||||
R.id.aboutFragmentWrapped
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onShowStayAwesomeScreen() {
|
override fun onShowStayAwesomeScreen() {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.About.StayAwesome)
|
||||||
AboutFragmentWrappedDirections.actionAboutFragmentWrappedToStayAwesomeFragment(),
|
|
||||||
R.id.aboutFragmentWrapped
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,64 +0,0 @@
|
||||||
/*
|
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 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.launch
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
|
||||||
import io.timelimit.android.extensions.safeNavigate
|
|
||||||
import io.timelimit.android.ui.obsolete.ObsoleteDialogFragment
|
|
||||||
import io.timelimit.android.ui.overview.main.MainFragmentDirections
|
|
||||||
|
|
||||||
class LaunchFragment: Fragment() {
|
|
||||||
private val model by viewModels<LaunchModel>()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
ObsoleteDialogFragment.show(requireActivity(), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
return inflater.inflate(R.layout.circular_progress_indicator, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
val navigation = Navigation.findNavController(view)
|
|
||||||
|
|
||||||
model.action.observe(viewLifecycleOwner) {
|
|
||||||
when (it) {
|
|
||||||
LaunchModel.Action.Setup -> navigation.safeNavigate(LaunchFragmentDirections.actionLaunchFragmentToSetupTermsFragment(), R.id.launchFragment)
|
|
||||||
LaunchModel.Action.Overview -> navigation.safeNavigate(LaunchFragmentDirections.actionLaunchFragmentToOverviewFragment(), R.id.launchFragment)
|
|
||||||
is LaunchModel.Action.Child -> {
|
|
||||||
navigation.safeNavigate(LaunchFragmentDirections.actionLaunchFragmentToOverviewFragment(), R.id.launchFragment)
|
|
||||||
navigation.safeNavigate(MainFragmentDirections.actionOverviewFragmentToManageChildFragment(it.id, fromRedirect = true), R.id.overviewFragment)
|
|
||||||
}
|
|
||||||
LaunchModel.Action.DeviceSetup -> {
|
|
||||||
navigation.safeNavigate(LaunchFragmentDirections.actionLaunchFragmentToOverviewFragment(), R.id.launchFragment)
|
|
||||||
navigation.safeNavigate(MainFragmentDirections.actionOverviewFragmentToSetupDeviceFragment(), R.id.overviewFragment)
|
|
||||||
}
|
|
||||||
LaunchModel.Action.ParentMode -> navigation.safeNavigate(LaunchFragmentDirections.actionLaunchFragmentToParentModeFragment(), R.id.launchFragment)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
/*
|
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 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.launch
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import io.timelimit.android.async.Threads
|
|
||||||
import io.timelimit.android.coroutines.executeAndWait
|
|
||||||
import io.timelimit.android.data.model.UserType
|
|
||||||
import io.timelimit.android.livedata.castDown
|
|
||||||
import io.timelimit.android.livedata.waitUntilValueMatches
|
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class LaunchModel(application: Application): AndroidViewModel(application) {
|
|
||||||
private val actionInternal = MutableLiveData<Action>()
|
|
||||||
private val logic = DefaultAppLogic.with(application)
|
|
||||||
|
|
||||||
val action = actionInternal.castDown()
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
logic.isInitialized.waitUntilValueMatches { it == true }
|
|
||||||
|
|
||||||
actionInternal.value = Threads.database.executeAndWait {
|
|
||||||
val hasDeviceId = logic.database.config().getOwnDeviceIdSync() != null
|
|
||||||
val hasParentKey = logic.database.config().getParentModeKeySync() != null
|
|
||||||
|
|
||||||
if (hasDeviceId) {
|
|
||||||
val config = logic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
|
|
||||||
|
|
||||||
if (config?.userRelatedData?.user?.type == UserType.Child) Action.Child(config.userRelatedData.user.id)
|
|
||||||
else if (config?.userRelatedData == null) Action.DeviceSetup
|
|
||||||
else Action.Overview
|
|
||||||
}
|
|
||||||
else if (hasParentKey) Action.ParentMode
|
|
||||||
else Action.Setup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class Action {
|
|
||||||
object Setup: Action()
|
|
||||||
object Overview: Action()
|
|
||||||
data class Child(val id: String): Action()
|
|
||||||
object DeviceSetup: Action()
|
|
||||||
object ParentMode: Action()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -15,13 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.ui.manage.category
|
package io.timelimit.android.ui.manage.category
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import io.timelimit.android.R
|
|
||||||
import io.timelimit.android.extensions.safeNavigate
|
|
||||||
import io.timelimit.android.ui.fragment.CategoryFragmentWrapper
|
import io.timelimit.android.ui.fragment.CategoryFragmentWrapper
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
import io.timelimit.android.ui.manage.category.appsandrules.CombinedAppsAndRulesFragment
|
import io.timelimit.android.ui.manage.category.appsandrules.CombinedAppsAndRulesFragment
|
||||||
|
@ -35,42 +29,4 @@ class ManageCategoryFragment : CategoryFragmentWrapper(), FragmentWithCustomTitl
|
||||||
childId = childId,
|
childId = childId,
|
||||||
categoryId = categoryId
|
categoryId = categoryId
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
|
||||||
|
|
||||||
inflater.inflate(R.menu.fragment_manage_category_menu, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
|
||||||
R.id.menu_manage_category_blocked_time_areas -> {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
ManageCategoryFragmentDirections.actionManageCategoryFragmentToBlockedTimeAreasFragmentWrapper(
|
|
||||||
childId = params.childId,
|
|
||||||
categoryId = params.categoryId
|
|
||||||
),
|
|
||||||
R.id.manageCategoryFragment
|
|
||||||
)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.menu_manage_category_settings -> {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
ManageCategoryFragmentDirections.actionManageCategoryFragmentToCategoryAdvancedFragmentWrapper(
|
|
||||||
childId = params.childId,
|
|
||||||
categoryId = params.categoryId
|
|
||||||
),
|
|
||||||
R.id.manageCategoryFragment
|
|
||||||
)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,14 +16,10 @@
|
||||||
package io.timelimit.android.ui.manage.child
|
package io.timelimit.android.ui.manage.child
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.extensions.safeNavigate
|
|
||||||
import io.timelimit.android.livedata.map
|
import io.timelimit.android.livedata.map
|
||||||
import io.timelimit.android.ui.fragment.ChildFragmentWrapper
|
import io.timelimit.android.ui.fragment.ChildFragmentWrapper
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
@ -49,55 +45,5 @@ class ManageChildFragment : ChildFragmentWrapper(), FragmentWithCustomTitle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
|
||||||
|
|
||||||
inflater.inflate(R.menu.fragment_manage_child_menu, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
|
||||||
R.id.menu_manage_child_apps -> {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
ManageChildFragmentDirections.actionManageChildFragmentToChildAppsFragmentWrapper(childId = childId),
|
|
||||||
R.id.manageChildFragment
|
|
||||||
)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.menu_manage_child_advanced -> {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
ManageChildFragmentDirections.actionManageChildFragmentToChildAdvancedFragmentWrapper(childId = childId),
|
|
||||||
R.id.manageChildFragment
|
|
||||||
)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.menu_manage_child_phone -> {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
ManageChildFragmentDirections.actionManageChildFragmentToContactsFragment(),
|
|
||||||
R.id.manageChildFragment
|
|
||||||
)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.menu_manage_child_usage_history -> {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
ManageChildFragmentDirections.actionManageChildFragmentToChildUsageHistoryFragmentWrapper(childId = childId),
|
|
||||||
R.id.manageChildFragment
|
|
||||||
)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.menu_manage_child_tasks -> {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
ManageChildFragmentDirections.actionManageChildFragmentToManageChildTasksFragment(childId = childId),
|
|
||||||
R.id.manageChildFragment
|
|
||||||
)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCustomTitle() = child.map { "${it?.name} < ${getString(R.string.main_tab_overview)}" as String? }
|
override fun getCustomTitle() = child.map { "${it?.name} < ${getString(R.string.main_tab_overview)}" as String? }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -22,16 +22,13 @@ import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.timelimit.android.R
|
|
||||||
import io.timelimit.android.async.Threads
|
import io.timelimit.android.async.Threads
|
||||||
import io.timelimit.android.data.model.Category
|
import io.timelimit.android.data.model.Category
|
||||||
import io.timelimit.android.data.model.HintsToShow
|
import io.timelimit.android.data.model.HintsToShow
|
||||||
import io.timelimit.android.databinding.RecyclerFragmentBinding
|
import io.timelimit.android.databinding.RecyclerFragmentBinding
|
||||||
import io.timelimit.android.extensions.safeNavigate
|
|
||||||
import io.timelimit.android.logic.AppLogic
|
import io.timelimit.android.logic.AppLogic
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.sync.actions.UpdateCategoryDisableLimitsAction
|
import io.timelimit.android.sync.actions.UpdateCategoryDisableLimitsAction
|
||||||
|
@ -41,10 +38,11 @@ import io.timelimit.android.ui.consent.SyncAppListConsentDialogFragment
|
||||||
import io.timelimit.android.ui.main.ActivityViewModel
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
import io.timelimit.android.ui.main.getActivityViewModel
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
|
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
|
||||||
import io.timelimit.android.ui.manage.child.ManageChildFragmentDirections
|
|
||||||
import io.timelimit.android.ui.manage.child.category.create.CreateCategoryDialogFragment
|
import io.timelimit.android.ui.manage.child.category.create.CreateCategoryDialogFragment
|
||||||
import io.timelimit.android.ui.manage.child.category.specialmode.SetCategorySpecialModeFragment
|
import io.timelimit.android.ui.manage.child.category.specialmode.SetCategorySpecialModeFragment
|
||||||
import io.timelimit.android.ui.manage.child.category.specialmode.SpecialModeDialogMode
|
import io.timelimit.android.ui.manage.child.category.specialmode.SpecialModeDialogMode
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
class ManageChildCategoriesFragment : Fragment() {
|
class ManageChildCategoriesFragment : Fragment() {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -69,17 +67,13 @@ class ManageChildCategoriesFragment : Fragment() {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
val adapter = Adapter()
|
val adapter = Adapter()
|
||||||
val navigation = Navigation.findNavController(view)
|
|
||||||
|
|
||||||
adapter.handlers = object: Handlers {
|
adapter.handlers = object: Handlers {
|
||||||
override fun onCategoryClicked(category: Category) {
|
override fun onCategoryClicked(category: Category) {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.ManageChild.Category(
|
||||||
ManageChildFragmentDirections.actionManageChildFragmentToManageCategoryFragment(
|
childId = params.childId,
|
||||||
params.childId,
|
categoryId = category.id
|
||||||
category.id
|
))
|
||||||
),
|
|
||||||
R.id.manageChildFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateCategoryClicked() {
|
override fun onCreateCategoryClicked() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -25,13 +25,11 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.lifecycle.switchMap
|
import androidx.lifecycle.switchMap
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.crypto.Curve25519
|
import io.timelimit.android.crypto.Curve25519
|
||||||
import io.timelimit.android.crypto.HexString
|
import io.timelimit.android.crypto.HexString
|
||||||
import io.timelimit.android.data.model.Device
|
import io.timelimit.android.data.model.Device
|
||||||
import io.timelimit.android.databinding.FragmentManageDeviceBinding
|
import io.timelimit.android.databinding.FragmentManageDeviceBinding
|
||||||
import io.timelimit.android.extensions.safeNavigate
|
|
||||||
import io.timelimit.android.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
import io.timelimit.android.logic.AppLogic
|
import io.timelimit.android.logic.AppLogic
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
@ -42,6 +40,8 @@ import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
import io.timelimit.android.ui.manage.device.manage.feature.ManageDeviceFeaturesFragment
|
import io.timelimit.android.ui.manage.device.manage.feature.ManageDeviceFeaturesFragment
|
||||||
import io.timelimit.android.ui.manage.device.manage.permission.ManageDevicePermissionsFragment
|
import io.timelimit.android.ui.manage.device.manage.permission.ManageDevicePermissionsFragment
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||||
|
@ -52,8 +52,7 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
logic.database.device().getDeviceById(args.deviceId)
|
logic.database.device().getDeviceById(args.deviceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
val navigation = Navigation.findNavController(container!!)
|
|
||||||
val binding = FragmentManageDeviceBinding.inflate(inflater, container, false)
|
val binding = FragmentManageDeviceBinding.inflate(inflater, container, false)
|
||||||
val userEntries = logic.database.user().getAllUsersLive()
|
val userEntries = logic.database.user().getAllUsersLive()
|
||||||
|
|
||||||
|
@ -82,39 +81,19 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
|
||||||
binding.handlers = object: ManageDeviceFragmentHandlers {
|
binding.handlers = object: ManageDeviceFragmentHandlers {
|
||||||
override fun showUserScreen() {
|
override fun showUserScreen() {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.ManageDevice.User(args.deviceId))
|
||||||
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDeviceUserFragment(
|
|
||||||
deviceId = args.deviceId
|
|
||||||
),
|
|
||||||
R.id.manageDeviceFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showPermissionsScreen() {
|
override fun showPermissionsScreen() {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.ManageDevice.Permissions(args.deviceId))
|
||||||
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDevicePermissionsFragment(
|
|
||||||
deviceId = args.deviceId
|
|
||||||
),
|
|
||||||
R.id.manageDeviceFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showFeaturesScreen() {
|
override fun showFeaturesScreen() {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.ManageDevice.Features(args.deviceId))
|
||||||
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDeviceFeaturesFragment(
|
|
||||||
deviceId = args.deviceId
|
|
||||||
),
|
|
||||||
R.id.manageDeviceFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showManageScreen() {
|
override fun showManageScreen() {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.ManageDevice.Advanced(args.deviceId))
|
||||||
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDeviceAdvancedFragment(
|
|
||||||
deviceId = args.deviceId
|
|
||||||
),
|
|
||||||
R.id.manageDeviceFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showAuthenticationScreen() {
|
override fun showAuthenticationScreen() {
|
||||||
|
@ -126,7 +105,7 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
device ->
|
device ->
|
||||||
|
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
navigation.popBackStack()
|
requireActivity().execute(UpdateStateCommand.ManageDevice.Leave)
|
||||||
} else {
|
} else {
|
||||||
val now = RealTime.newInstance()
|
val now = RealTime.newInstance()
|
||||||
logic.realTimeLogic.getRealTime(now)
|
logic.realTimeLogic.getRealTime(now)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -23,7 +23,6 @@ import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.model.Device
|
import io.timelimit.android.data.model.Device
|
||||||
import io.timelimit.android.data.model.User
|
import io.timelimit.android.data.model.User
|
||||||
|
@ -35,6 +34,8 @@ import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
import io.timelimit.android.ui.main.AuthenticationFab
|
import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
class ManageDeviceAdvancedFragment : Fragment(), FragmentWithCustomTitle {
|
class ManageDeviceAdvancedFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||||
|
@ -47,7 +48,6 @@ class ManageDeviceAdvancedFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val binding = ManageDeviceAdvancedFragmentBinding.inflate(inflater, container, false)
|
val binding = ManageDeviceAdvancedFragmentBinding.inflate(inflater, container, false)
|
||||||
val navigation = Navigation.findNavController(container!!)
|
|
||||||
val isThisDevice = logic.deviceId.map { ownDeviceId -> ownDeviceId == args.deviceId }.ignoreUnchanged()
|
val isThisDevice = logic.deviceId.map { ownDeviceId -> ownDeviceId == args.deviceId }.ignoreUnchanged()
|
||||||
|
|
||||||
val userEntry = deviceEntry.switchMap { device ->
|
val userEntry = deviceEntry.switchMap { device ->
|
||||||
|
@ -102,7 +102,7 @@ class ManageDeviceAdvancedFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
|
||||||
deviceEntry.observe(this, Observer { device ->
|
deviceEntry.observe(this, Observer { device ->
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
navigation.popBackStack(R.id.overviewFragment, false)
|
requireActivity().execute(UpdateStateCommand.ManageDevice.Leave)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -24,7 +24,6 @@ import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.model.Device
|
import io.timelimit.android.data.model.Device
|
||||||
import io.timelimit.android.data.model.NetworkTime
|
import io.timelimit.android.data.model.NetworkTime
|
||||||
|
@ -40,6 +39,8 @@ import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
import io.timelimit.android.ui.main.AuthenticationFab
|
import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
class ManageDeviceFeaturesFragment : Fragment(), FragmentWithCustomTitle {
|
class ManageDeviceFeaturesFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -79,7 +80,6 @@ class ManageDeviceFeaturesFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val navigation = Navigation.findNavController(container!!)
|
|
||||||
val binding = ManageDeviceFeaturesFragmentBinding.inflate(inflater, container, false)
|
val binding = ManageDeviceFeaturesFragmentBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
|
@ -129,7 +129,7 @@ class ManageDeviceFeaturesFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
device ->
|
device ->
|
||||||
|
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
navigation.popBackStack(R.id.overviewFragment, false)
|
requireActivity().execute(UpdateStateCommand.ManageDevice.Leave)
|
||||||
} else {
|
} else {
|
||||||
val now = RealTime.newInstance()
|
val now = RealTime.newInstance()
|
||||||
logic.realTimeLogic.getRealTime(now)
|
logic.realTimeLogic.getRealTime(now)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -23,7 +23,6 @@ import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.model.Device
|
import io.timelimit.android.data.model.Device
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
|
@ -41,6 +40,8 @@ import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
import io.timelimit.android.ui.main.AuthenticationFab
|
import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
class ManageDevicePermissionsFragment : Fragment(), FragmentWithCustomTitle {
|
class ManageDevicePermissionsFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -84,7 +85,6 @@ class ManageDevicePermissionsFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val navigation = Navigation.findNavController(container!!)
|
|
||||||
val binding = ManageDevicePermissionsFragmentBinding.inflate(inflater, container, false)
|
val binding = ManageDevicePermissionsFragmentBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
// auth
|
// auth
|
||||||
|
@ -180,7 +180,7 @@ class ManageDevicePermissionsFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
device ->
|
device ->
|
||||||
|
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
navigation.popBackStack(R.id.overviewFragment, false)
|
requireActivity().execute(UpdateStateCommand.ManageDevice.Leave)
|
||||||
} else {
|
} else {
|
||||||
binding.usageStatsAccess = device.currentUsageStatsPermission
|
binding.usageStatsAccess = device.currentUsageStatsPermission
|
||||||
binding.notificationAccessPermission = device.currentNotificationAccessPermission
|
binding.notificationAccessPermission = device.currentNotificationAccessPermission
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -24,7 +24,6 @@ import android.widget.RadioButton
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.model.Device
|
import io.timelimit.android.data.model.Device
|
||||||
import io.timelimit.android.databinding.ManageDeviceUserFragmentBinding
|
import io.timelimit.android.databinding.ManageDeviceUserFragmentBinding
|
||||||
|
@ -40,6 +39,8 @@ import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
import io.timelimit.android.ui.main.AuthenticationFab
|
import io.timelimit.android.ui.main.AuthenticationFab
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
import io.timelimit.android.ui.manage.device.manage.defaultuser.ManageDeviceDefaultUser
|
import io.timelimit.android.ui.manage.device.manage.defaultuser.ManageDeviceDefaultUser
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
class ManageDeviceUserFragment : Fragment(), FragmentWithCustomTitle {
|
class ManageDeviceUserFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||||
|
@ -51,7 +52,6 @@ class ManageDeviceUserFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val navigation = Navigation.findNavController(container!!)
|
|
||||||
val binding = ManageDeviceUserFragmentBinding.inflate(inflater, container, false)
|
val binding = ManageDeviceUserFragmentBinding.inflate(inflater, container, false)
|
||||||
val userEntries = logic.database.user().getAllUsersLive()
|
val userEntries = logic.database.user().getAllUsersLive()
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ class ManageDeviceUserFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
device ->
|
device ->
|
||||||
|
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
navigation.popBackStack(R.id.overviewFragment, false)
|
requireActivity().execute(UpdateStateCommand.ManageDevice.Leave)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -24,11 +24,9 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.model.User
|
import io.timelimit.android.data.model.User
|
||||||
import io.timelimit.android.databinding.FragmentManageParentBinding
|
import io.timelimit.android.databinding.FragmentManageParentBinding
|
||||||
import io.timelimit.android.extensions.safeNavigate
|
|
||||||
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
||||||
import io.timelimit.android.livedata.map
|
import io.timelimit.android.livedata.map
|
||||||
import io.timelimit.android.logic.AppLogic
|
import io.timelimit.android.logic.AppLogic
|
||||||
|
@ -40,6 +38,8 @@ import io.timelimit.android.ui.manage.child.advanced.timezone.UserTimezoneView
|
||||||
import io.timelimit.android.ui.manage.parent.delete.DeleteParentView
|
import io.timelimit.android.ui.manage.parent.delete.DeleteParentView
|
||||||
import io.timelimit.android.ui.manage.parent.key.ManageUserKeyView
|
import io.timelimit.android.ui.manage.parent.key.ManageUserKeyView
|
||||||
import io.timelimit.android.ui.manage.parent.limitlogin.ParentLimitLoginView
|
import io.timelimit.android.ui.manage.parent.limitlogin.ParentLimitLoginView
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||||
|
@ -50,7 +50,6 @@ class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val binding = FragmentManageParentBinding.inflate(inflater, container, false)
|
val binding = FragmentManageParentBinding.inflate(inflater, container, false)
|
||||||
val navigation = Navigation.findNavController(container!!)
|
|
||||||
val model = ViewModelProviders.of(this).get(ManageParentModel::class.java)
|
val model = ViewModelProviders.of(this).get(ManageParentModel::class.java)
|
||||||
|
|
||||||
AuthenticationFab.manageAuthenticationFab(
|
AuthenticationFab.manageAuthenticationFab(
|
||||||
|
@ -83,9 +82,7 @@ class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
parentUser.observe(this, Observer {
|
parentUser.observe(this, Observer {
|
||||||
user ->
|
user ->
|
||||||
|
|
||||||
if (user == null) {
|
if (user == null) requireActivity().execute(UpdateStateCommand.ManageParent.Leave)
|
||||||
navigation.popBackStack()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,45 +137,21 @@ class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
|
||||||
binding.handlers = object: ManageParentFragmentHandlers {
|
binding.handlers = object: ManageParentFragmentHandlers {
|
||||||
override fun onChangePasswordClicked() {
|
override fun onChangePasswordClicked() {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.ManageParent.ChangePassword)
|
||||||
ManageParentFragmentDirections.
|
|
||||||
actionManageParentFragmentToChangeParentPasswordFragment(
|
|
||||||
params.parentId
|
|
||||||
),
|
|
||||||
R.id.manageParentFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestorePasswordClicked() {
|
override fun onRestorePasswordClicked() {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.ManageParent.RestorePassword)
|
||||||
ManageParentFragmentDirections.
|
|
||||||
actionManageParentFragmentToRestoreParentPasswordFragment(
|
|
||||||
params.parentId
|
|
||||||
),
|
|
||||||
R.id.manageParentFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLinkMailClicked() {
|
override fun onLinkMailClicked() {
|
||||||
if (activity.getActivityViewModel().requestAuthenticationOrReturnTrue()) {
|
if (activity.getActivityViewModel().requestAuthenticationOrReturnTrue()) {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.ManageParent.LinkMail)
|
||||||
ManageParentFragmentDirections.
|
|
||||||
actionManageParentFragmentToLinkParentMailFragment(
|
|
||||||
params.parentId
|
|
||||||
),
|
|
||||||
R.id.manageParentFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onManageU2FClicked() {
|
override fun onManageU2FClicked() {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.ManageParent.U2F)
|
||||||
ManageParentFragmentDirections.
|
|
||||||
actionManageParentFragmentToManageParentU2FKeyFragment(
|
|
||||||
params.parentId
|
|
||||||
),
|
|
||||||
R.id.manageParentFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -23,7 +23,6 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.model.User
|
import io.timelimit.android.data.model.User
|
||||||
import io.timelimit.android.databinding.LinkParentMailFragmentBinding
|
import io.timelimit.android.databinding.LinkParentMailFragmentBinding
|
||||||
|
@ -32,6 +31,8 @@ import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.ui.authentication.AuthenticateByMailFragment
|
import io.timelimit.android.ui.authentication.AuthenticateByMailFragment
|
||||||
import io.timelimit.android.ui.authentication.AuthenticateByMailFragmentListener
|
import io.timelimit.android.ui.authentication.AuthenticateByMailFragmentListener
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
class LinkParentMailFragment : Fragment(), AuthenticateByMailFragmentListener, FragmentWithCustomTitle {
|
class LinkParentMailFragment : Fragment(), AuthenticateByMailFragmentListener, FragmentWithCustomTitle {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -57,7 +58,6 @@ class LinkParentMailFragment : Fragment(), AuthenticateByMailFragmentListener, F
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val binding = LinkParentMailFragmentBinding.inflate(inflater, container, false)
|
val binding = LinkParentMailFragmentBinding.inflate(inflater, container, false)
|
||||||
val navigation = Navigation.findNavController(container!!)
|
|
||||||
|
|
||||||
model.status.observe(this, Observer {
|
model.status.observe(this, Observer {
|
||||||
status ->
|
status ->
|
||||||
|
@ -66,7 +66,7 @@ class LinkParentMailFragment : Fragment(), AuthenticateByMailFragmentListener, F
|
||||||
LinkParentMailViewModelStatus.WaitForAuthentication -> binding.flipper.displayedChild = PAGE_LOGIN
|
LinkParentMailViewModelStatus.WaitForAuthentication -> binding.flipper.displayedChild = PAGE_LOGIN
|
||||||
LinkParentMailViewModelStatus.WaitForConfirmationWithPassword -> binding.flipper.displayedChild = PAGE_READY
|
LinkParentMailViewModelStatus.WaitForConfirmationWithPassword -> binding.flipper.displayedChild = PAGE_READY
|
||||||
LinkParentMailViewModelStatus.ShouldLeaveScreen -> {
|
LinkParentMailViewModelStatus.ShouldLeaveScreen -> {
|
||||||
navigation.popBackStack()
|
requireActivity().execute(UpdateStateCommand.ManageParent.LeaveLinkMail)
|
||||||
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -24,7 +24,6 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.model.User
|
import io.timelimit.android.data.model.User
|
||||||
|
@ -33,6 +32,8 @@ import io.timelimit.android.livedata.map
|
||||||
import io.timelimit.android.logic.AppLogic
|
import io.timelimit.android.logic.AppLogic
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
class ChangeParentPasswordFragment : Fragment(), FragmentWithCustomTitle {
|
class ChangeParentPasswordFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||||
|
@ -46,14 +47,13 @@ class ChangeParentPasswordFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val navigation = Navigation.findNavController(container!!)
|
|
||||||
val binding = ChangeParentPasswordFragmentBinding.inflate(inflater, container, false)
|
val binding = ChangeParentPasswordFragmentBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
parentUser.observe(this, Observer {
|
parentUser.observe(this, Observer {
|
||||||
parentUser ->
|
parentUser ->
|
||||||
|
|
||||||
if (parentUser == null) {
|
if (parentUser == null) {
|
||||||
navigation.popBackStack(R.id.overviewFragment, false)
|
requireActivity().execute(UpdateStateCommand.ManageParent.Leave)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ class ChangeParentPasswordFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
ChangeParentPasswordViewModelStatus.Done -> {
|
ChangeParentPasswordViewModelStatus.Done -> {
|
||||||
Toast.makeText(context!!, R.string.manage_parent_change_password_toast_success, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context!!, R.string.manage_parent_change_password_toast_success, Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
navigation.popBackStack()
|
requireActivity().execute(UpdateStateCommand.ManageParent.LeaveChangePassword)
|
||||||
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -24,7 +24,6 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.model.User
|
import io.timelimit.android.data.model.User
|
||||||
import io.timelimit.android.databinding.RestoreParentPasswordFragmentBinding
|
import io.timelimit.android.databinding.RestoreParentPasswordFragmentBinding
|
||||||
|
@ -34,6 +33,8 @@ import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.ui.authentication.AuthenticateByMailFragment
|
import io.timelimit.android.ui.authentication.AuthenticateByMailFragment
|
||||||
import io.timelimit.android.ui.authentication.AuthenticateByMailFragmentListener
|
import io.timelimit.android.ui.authentication.AuthenticateByMailFragmentListener
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
class RestoreParentPasswordFragment : Fragment(), AuthenticateByMailFragmentListener, FragmentWithCustomTitle {
|
class RestoreParentPasswordFragment : Fragment(), AuthenticateByMailFragmentListener, FragmentWithCustomTitle {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -56,7 +57,6 @@ class RestoreParentPasswordFragment : Fragment(), AuthenticateByMailFragmentList
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val binding = RestoreParentPasswordFragmentBinding.inflate(inflater, container, false)
|
val binding = RestoreParentPasswordFragmentBinding.inflate(inflater, container, false)
|
||||||
val navigation = Navigation.findNavController(container!!)
|
|
||||||
|
|
||||||
model.status.observe(this, Observer {
|
model.status.observe(this, Observer {
|
||||||
status ->
|
status ->
|
||||||
|
@ -74,14 +74,14 @@ class RestoreParentPasswordFragment : Fragment(), AuthenticateByMailFragmentList
|
||||||
RestoreParentPasswordStatus.NetworkError -> {
|
RestoreParentPasswordStatus.NetworkError -> {
|
||||||
Toast.makeText(context!!, R.string.error_network, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context!!, R.string.error_network, Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
navigation.popBackStack()
|
requireActivity().execute(UpdateStateCommand.ManageParent.LeaveRestorePassword)
|
||||||
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
RestoreParentPasswordStatus.Done -> {
|
RestoreParentPasswordStatus.Done -> {
|
||||||
Toast.makeText(context!!, R.string.manage_parent_change_password_toast_success, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context!!, R.string.manage_parent_change_password_toast_success, Toast.LENGTH_SHORT).show()
|
||||||
|
|
||||||
navigation.popBackStack()
|
requireActivity().execute(UpdateStateCommand.ManageParent.LeaveRestorePassword)
|
||||||
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -22,7 +22,6 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.databinding.ManageParentU2fKeyFragmentBinding
|
import io.timelimit.android.databinding.ManageParentU2fKeyFragmentBinding
|
||||||
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
||||||
|
@ -31,12 +30,13 @@ import io.timelimit.android.ui.main.*
|
||||||
import io.timelimit.android.ui.manage.parent.u2fkey.add.AddU2FDialogFragment
|
import io.timelimit.android.ui.manage.parent.u2fkey.add.AddU2FDialogFragment
|
||||||
import io.timelimit.android.ui.manage.parent.u2fkey.remove.RemoveU2FKeyDialogFragment
|
import io.timelimit.android.ui.manage.parent.u2fkey.remove.RemoveU2FKeyDialogFragment
|
||||||
import io.timelimit.android.ui.manage.parent.u2fkey.remove.U2FRequiresPasswordForRemovalDialogFragment
|
import io.timelimit.android.ui.manage.parent.u2fkey.remove.U2FRequiresPasswordForRemovalDialogFragment
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
class ManageParentU2FKeyFragment : Fragment(), FragmentWithCustomTitle {
|
class ManageParentU2FKeyFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
val model: ManageParentU2FKeyModel by viewModels()
|
val model: ManageParentU2FKeyModel by viewModels()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
val navigation = Navigation.findNavController(container!!)
|
|
||||||
val params = ManageParentU2FKeyFragmentArgs.fromBundle(requireArguments())
|
val params = ManageParentU2FKeyFragmentArgs.fromBundle(requireArguments())
|
||||||
val binding = ManageParentU2fKeyFragmentBinding.inflate(inflater, container, false)
|
val binding = ManageParentU2fKeyFragmentBinding.inflate(inflater, container, false)
|
||||||
val activityModel = getActivityViewModel(requireActivity())
|
val activityModel = getActivityViewModel(requireActivity())
|
||||||
|
@ -93,7 +93,9 @@ class ManageParentU2FKeyFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
model.user.observe(viewLifecycleOwner) { if (it == null) navigation.popBackStack(R.id.overviewFragment, false) }
|
model.user.observe(viewLifecycleOwner) {
|
||||||
|
if (it == null) requireActivity().execute(UpdateStateCommand.ManageParent.Leave)
|
||||||
|
}
|
||||||
|
|
||||||
model.listItems.observe(viewLifecycleOwner) { adapter.items = it }
|
model.listItems.observe(viewLifecycleOwner) { adapter.items = it }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
|
interface FragmentState {
|
||||||
|
val containerId: Int
|
||||||
|
val fragmentClass: Class<out Fragment>
|
||||||
|
val arguments: Bundle get() = Bundle()
|
||||||
|
val toolbarIcons: List<Menu.Icon> get() = emptyList()
|
||||||
|
val toolbarOptions: List<Menu.Dropdown> get() = emptyList()
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package io.timelimit.android.ui.model
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
|
||||||
|
abstract class FragmentStateLegacy(
|
||||||
|
previous: State?,
|
||||||
|
@Transient override val fragmentClass: Class<out Fragment>,
|
||||||
|
override val containerId: Int = View.generateViewId()
|
||||||
|
): State(previous), FragmentState, java.io.Serializable {
|
||||||
|
override fun toString(): String = fragmentClass.name
|
||||||
|
}
|
63
app/src/main/java/io/timelimit/android/ui/model/MainModel.kt
Normal file
63
app/src/main/java/io/timelimit/android/ui/model/MainModel.kt
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import io.timelimit.android.BuildConfig
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.ui.model.launch.LaunchHandling
|
||||||
|
import io.timelimit.android.ui.model.main.OverviewHandling
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
|
class MainModel(application: Application): AndroidViewModel(application) {
|
||||||
|
companion object {
|
||||||
|
private const val LOG_TAG = "MainModel"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val logic = DefaultAppLogic.with(application)
|
||||||
|
|
||||||
|
val state = MutableStateFlow(State.LaunchState as State)
|
||||||
|
|
||||||
|
val screen: Flow<Screen> = flow {
|
||||||
|
while (true) {
|
||||||
|
when (state.value) {
|
||||||
|
is State.LaunchState -> LaunchHandling.processLaunchState(state, logic)
|
||||||
|
is State.Overview -> emitAll(OverviewHandling.processState(logic, viewModelScope, state))
|
||||||
|
is FragmentState -> emitAll(state.transformWhile {
|
||||||
|
if (it is FragmentState && it !is State.Overview) {
|
||||||
|
emit(Screen.FragmentScreen(it, it.toolbarIcons, it.toolbarOptions, it))
|
||||||
|
|
||||||
|
true
|
||||||
|
} else false
|
||||||
|
})
|
||||||
|
else -> throw IllegalStateException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -13,13 +13,14 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.extensions
|
package io.timelimit.android.ui.model
|
||||||
|
|
||||||
import androidx.navigation.NavController
|
import android.app.Activity
|
||||||
import androidx.navigation.NavDirections
|
|
||||||
|
|
||||||
fun NavController.safeNavigate(directions: NavDirections, currentScreen: Int) {
|
interface MainModelActivity {
|
||||||
if (this.currentDestination?.id == currentScreen) {
|
fun execute(command: UpdateStateCommand)
|
||||||
navigate(directions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Activity.execute(command: UpdateStateCommand) {
|
||||||
|
(this as MainModelActivity).execute(command)
|
||||||
}
|
}
|
8
app/src/main/java/io/timelimit/android/ui/model/Menu.kt
Normal file
8
app/src/main/java/io/timelimit/android/ui/model/Menu.kt
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package io.timelimit.android.ui.model
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
|
object Menu {
|
||||||
|
data class Icon(val icon: ImageVector, val labelResource: Int, val action: UpdateStateCommand)
|
||||||
|
data class Dropdown(val labelResource: Int, val action: UpdateStateCommand)
|
||||||
|
}
|
52
app/src/main/java/io/timelimit/android/ui/model/Screen.kt
Normal file
52
app/src/main/java/io/timelimit/android/ui/model/Screen.kt
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.ui.model.main.OverviewHandling
|
||||||
|
|
||||||
|
sealed class Screen(
|
||||||
|
val state: State,
|
||||||
|
val toolbarIcons: List<Menu.Icon> = emptyList(),
|
||||||
|
val toolbarOptions: List<Menu.Dropdown> = emptyList()
|
||||||
|
) {
|
||||||
|
open class FragmentScreen(
|
||||||
|
state: State,
|
||||||
|
toolbarIcons: List<Menu.Icon>,
|
||||||
|
toolbarOptions: List<Menu.Dropdown>,
|
||||||
|
val fragment: FragmentState
|
||||||
|
): Screen(state, toolbarIcons, toolbarOptions)
|
||||||
|
|
||||||
|
class OverviewScreen(
|
||||||
|
state: State,
|
||||||
|
val content: OverviewHandling.OverviewScreen
|
||||||
|
): Screen(
|
||||||
|
state,
|
||||||
|
listOf(Menu.Icon(
|
||||||
|
Icons.Outlined.Info,
|
||||||
|
R.string.main_tab_about,
|
||||||
|
UpdateStateCommand.Overview.LaunchAbout
|
||||||
|
)),
|
||||||
|
listOf(Menu.Dropdown(
|
||||||
|
R.string.main_tab_uninstall,
|
||||||
|
UpdateStateCommand.Overview.Uninstall
|
||||||
|
))
|
||||||
|
), ScreenWithAuthenticationFab
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScreenWithAuthenticationFab
|
254
app/src/main/java/io/timelimit/android/ui/model/State.kt
Normal file
254
app/src/main/java/io/timelimit/android/ui/model/State.kt
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.DirectionsBike
|
||||||
|
import androidx.compose.material.icons.filled.Phone
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.ui.contacts.ContactsFragment
|
||||||
|
import io.timelimit.android.ui.diagnose.*
|
||||||
|
import io.timelimit.android.ui.diagnose.exitreason.DiagnoseExitReasonFragment
|
||||||
|
import io.timelimit.android.ui.fragment.*
|
||||||
|
import io.timelimit.android.ui.manage.category.ManageCategoryFragment
|
||||||
|
import io.timelimit.android.ui.manage.category.ManageCategoryFragmentArgs
|
||||||
|
import io.timelimit.android.ui.manage.child.ManageChildFragment
|
||||||
|
import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
|
||||||
|
import io.timelimit.android.ui.manage.device.manage.ManageDeviceFragment
|
||||||
|
import io.timelimit.android.ui.manage.device.manage.ManageDeviceFragmentArgs
|
||||||
|
import io.timelimit.android.ui.manage.device.manage.advanced.ManageDeviceAdvancedFragment
|
||||||
|
import io.timelimit.android.ui.manage.device.manage.advanced.ManageDeviceAdvancedFragmentArgs
|
||||||
|
import io.timelimit.android.ui.manage.device.manage.feature.ManageDeviceFeaturesFragment
|
||||||
|
import io.timelimit.android.ui.manage.device.manage.feature.ManageDeviceFeaturesFragmentArgs
|
||||||
|
import io.timelimit.android.ui.manage.device.manage.permission.ManageDevicePermissionsFragment
|
||||||
|
import io.timelimit.android.ui.manage.device.manage.permission.ManageDevicePermissionsFragmentArgs
|
||||||
|
import io.timelimit.android.ui.manage.device.manage.user.ManageDeviceUserFragment
|
||||||
|
import io.timelimit.android.ui.manage.device.manage.user.ManageDeviceUserFragmentArgs
|
||||||
|
import io.timelimit.android.ui.manage.parent.ManageParentFragment
|
||||||
|
import io.timelimit.android.ui.manage.parent.ManageParentFragmentArgs
|
||||||
|
import io.timelimit.android.ui.manage.parent.link.LinkParentMailFragment
|
||||||
|
import io.timelimit.android.ui.manage.parent.link.LinkParentMailFragmentArgs
|
||||||
|
import io.timelimit.android.ui.manage.parent.password.change.ChangeParentPasswordFragment
|
||||||
|
import io.timelimit.android.ui.manage.parent.password.change.ChangeParentPasswordFragmentArgs
|
||||||
|
import io.timelimit.android.ui.manage.parent.password.restore.RestoreParentPasswordFragment
|
||||||
|
import io.timelimit.android.ui.manage.parent.password.restore.RestoreParentPasswordFragmentArgs
|
||||||
|
import io.timelimit.android.ui.manage.parent.u2fkey.ManageParentU2FKeyFragment
|
||||||
|
import io.timelimit.android.ui.manage.parent.u2fkey.ManageParentU2FKeyFragmentArgs
|
||||||
|
import io.timelimit.android.ui.model.main.OverviewHandling
|
||||||
|
import io.timelimit.android.ui.overview.uninstall.UninstallFragment
|
||||||
|
import io.timelimit.android.ui.parentmode.ParentModeFragment
|
||||||
|
import io.timelimit.android.ui.payment.PurchaseFragment
|
||||||
|
import io.timelimit.android.ui.payment.StayAwesomeFragment
|
||||||
|
import io.timelimit.android.ui.setup.*
|
||||||
|
import io.timelimit.android.ui.setup.child.SetupRemoteChildFragment
|
||||||
|
import io.timelimit.android.ui.setup.device.SetupDeviceFragment
|
||||||
|
import io.timelimit.android.ui.setup.parent.SetupParentModeFragment
|
||||||
|
import io.timelimit.android.ui.user.create.AddUserFragment
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
sealed class State (val previous: State?): Serializable {
|
||||||
|
fun hasPrevious(other: State): Boolean = this.previous == other || this.previous?.hasPrevious(other) ?: false
|
||||||
|
fun find(predicate: (State) -> Boolean): State? =
|
||||||
|
if (predicate(this)) this
|
||||||
|
else previous?.find(predicate)
|
||||||
|
fun first(): State = previous?.first() ?: this
|
||||||
|
object LaunchState: State(previous = null)
|
||||||
|
data class Overview(
|
||||||
|
val state: OverviewHandling.OverviewState = OverviewHandling.OverviewState.empty
|
||||||
|
): State(previous = null)
|
||||||
|
class About(previous: Overview): FragmentStateLegacy(previous = previous, fragmentClass = AboutFragmentWrapped::class.java)
|
||||||
|
class AddUser(previous: Overview): FragmentStateLegacy(previous = previous, fragmentClass = AddUserFragment::class.java)
|
||||||
|
sealed class ManageChild(previous: State, fragmentClass: Class<out Fragment>): FragmentStateLegacy(previous, fragmentClass) {
|
||||||
|
class Main(
|
||||||
|
previous: Overview,
|
||||||
|
val childId: String,
|
||||||
|
fromRedirect: Boolean
|
||||||
|
): ManageChild(previous = previous, ManageChildFragment::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments = ManageChildFragmentArgs(childId = childId, fromRedirect = fromRedirect).toBundle()
|
||||||
|
|
||||||
|
override val toolbarIcons: List<Menu.Icon> = listOf(
|
||||||
|
Menu.Icon(
|
||||||
|
Icons.Default.DirectionsBike,
|
||||||
|
R.string.manage_child_tasks,
|
||||||
|
UpdateStateCommand.ManageChild.Tasks
|
||||||
|
),
|
||||||
|
Menu.Icon(
|
||||||
|
Icons.Default.Phone,
|
||||||
|
R.string.contacts_title_long,
|
||||||
|
UpdateStateCommand.ManageChild.Contacts
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override val toolbarOptions: List<Menu.Dropdown> = listOf(
|
||||||
|
Menu.Dropdown(R.string.child_apps_title, UpdateStateCommand.ManageChild.Apps),
|
||||||
|
Menu.Dropdown(R.string.usage_history_title, UpdateStateCommand.ManageChild.UsageHistory),
|
||||||
|
Menu.Dropdown(R.string.manage_child_tab_other, UpdateStateCommand.ManageChild.Advanced)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Apps(val previousChild: Main): ManageChild(previousChild, ChildAppsFragmentWrapper::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = ChildAppsFragmentWrapperArgs(previousChild.childId).toBundle()
|
||||||
|
}
|
||||||
|
class Advanced(val previousChild: Main): ManageChild(previousChild, ChildAdvancedFragmentWrapper::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = ChildAdvancedFragmentWrapperArgs(previousChild.childId).toBundle()
|
||||||
|
}
|
||||||
|
class Contacts(val previousChild: Main): ManageChild(previousChild, ContactsFragment::class.java)
|
||||||
|
class UsageHistory(val previousChild: Main): ManageChild(previousChild, ChildUsageHistoryFragmentWrapper::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = ChildUsageHistoryFragmentWrapperArgs(previousChild.childId).toBundle()
|
||||||
|
}
|
||||||
|
class Tasks(val previousChild: Main): ManageChild(previousChild, ChildTasksFragmentWrapper::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = ChildTasksFragmentWrapperArgs(previousChild.childId).toBundle()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ManageCategory(previous: State, fragmentClass: Class<out Fragment>): ManageChild(previous, fragmentClass) {
|
||||||
|
class Main(
|
||||||
|
val previousChild: ManageChild.Main,
|
||||||
|
val categoryId: String
|
||||||
|
): ManageCategory(previous = previousChild, fragmentClass = ManageCategoryFragment::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = ManageCategoryFragmentArgs(
|
||||||
|
childId = previousChild.childId,
|
||||||
|
categoryId = categoryId
|
||||||
|
).toBundle()
|
||||||
|
|
||||||
|
override val toolbarOptions: List<Menu.Dropdown> = listOf(
|
||||||
|
Menu.Dropdown(R.string.blocked_time_areas, UpdateStateCommand.ManageChild.BlockedTimes),
|
||||||
|
Menu.Dropdown(R.string.category_settings, UpdateStateCommand.ManageChild.CategoryAdvanced)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlockedTimes(
|
||||||
|
val previousCategory: Main
|
||||||
|
): ManageCategory(previous = previousCategory, fragmentClass = BlockedTimeAreasFragmentWrapper::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = BlockedTimeAreasFragmentWrapperArgs(
|
||||||
|
childId = previousCategory.previousChild.childId,
|
||||||
|
categoryId = previousCategory.categoryId
|
||||||
|
).toBundle()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Advanced(
|
||||||
|
val previousCategory: Main
|
||||||
|
): ManageCategory(previous = previousCategory, fragmentClass = CategoryAdvancedFragmentWrapper::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = CategoryAdvancedFragmentWrapperArgs(
|
||||||
|
childId = previousCategory.previousChild.childId,
|
||||||
|
categoryId = previousCategory.categoryId
|
||||||
|
).toBundle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sealed class ManageParent(previous: State, fragmentClass: Class<out Fragment>): FragmentStateLegacy(previous = previous, fragmentClass = fragmentClass) {
|
||||||
|
class Main(
|
||||||
|
previous: Overview,
|
||||||
|
val parentId: String
|
||||||
|
): ManageParent(previous = previous, fragmentClass = ManageParentFragment::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments = ManageParentFragmentArgs(parentId).toBundle()
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChangePassword(val previousParent: Main): ManageParent(previousParent, ChangeParentPasswordFragment::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = ChangeParentPasswordFragmentArgs(previousParent.parentId).toBundle()
|
||||||
|
}
|
||||||
|
class RestorePassword(val previousParent: Main): ManageParent(previousParent, RestoreParentPasswordFragment::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = RestoreParentPasswordFragmentArgs(previousParent.parentId).toBundle()
|
||||||
|
}
|
||||||
|
class LinkMail(val previousParent: Main): ManageParent(previousParent, LinkParentMailFragment::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = LinkParentMailFragmentArgs(previousParent.parentId).toBundle()
|
||||||
|
}
|
||||||
|
class U2F(val previousParent: Main): ManageParent(previousParent, ManageParentU2FKeyFragment::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = ManageParentU2FKeyFragmentArgs(previousParent.parentId).toBundle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sealed class ManageDevice(
|
||||||
|
previous: State,
|
||||||
|
fragmentClass: Class<out Fragment>
|
||||||
|
): FragmentStateLegacy(previous, fragmentClass) {
|
||||||
|
class Main(
|
||||||
|
val previousOverview: Overview,
|
||||||
|
deviceId: String
|
||||||
|
): ManageDevice(previousOverview, ManageDeviceFragment::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = ManageDeviceFragmentArgs(deviceId).toBundle()
|
||||||
|
}
|
||||||
|
class User(
|
||||||
|
val previousMain: Main,
|
||||||
|
deviceId: String
|
||||||
|
): ManageDevice(previousMain, ManageDeviceUserFragment::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = ManageDeviceUserFragmentArgs(deviceId).toBundle()
|
||||||
|
}
|
||||||
|
class Permissions(
|
||||||
|
val previousMain: Main,
|
||||||
|
deviceId: String
|
||||||
|
): ManageDevice(previousMain, ManageDevicePermissionsFragment::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = ManageDevicePermissionsFragmentArgs(deviceId).toBundle()
|
||||||
|
}
|
||||||
|
class Features(
|
||||||
|
val previousMain: Main,
|
||||||
|
deviceId: String
|
||||||
|
): ManageDevice(previousMain, ManageDeviceFeaturesFragment::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = ManageDeviceFeaturesFragmentArgs(deviceId).toBundle()
|
||||||
|
}
|
||||||
|
class Advanced(
|
||||||
|
val previousMain: Main,
|
||||||
|
deviceId: String
|
||||||
|
): ManageDevice(previousMain, ManageDeviceAdvancedFragment::class.java) {
|
||||||
|
@Transient
|
||||||
|
override val arguments: Bundle = ManageDeviceAdvancedFragmentArgs(deviceId).toBundle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class SetupDevice(val previousOverview: Overview): FragmentStateLegacy(previous = previousOverview, fragmentClass = SetupDeviceFragment::class.java)
|
||||||
|
class Uninstall(previous: Overview): FragmentStateLegacy(previous = previous, fragmentClass = UninstallFragment::class.java)
|
||||||
|
object DiagnoseScreen {
|
||||||
|
class Main(previous: About): FragmentStateLegacy(previous, DiagnoseMainFragment::class.java)
|
||||||
|
class Battery(previous: Main): FragmentStateLegacy(previous, DiagnoseBatteryFragment::class.java)
|
||||||
|
class Clock(previous: Main): FragmentStateLegacy(previous, DiagnoseClockFragment::class.java)
|
||||||
|
class Connection(previous: Main): FragmentStateLegacy(previous, DiagnoseConnectionFragment::class.java)
|
||||||
|
class ExperimentalFlags(previous: Main): FragmentStateLegacy(previous, DiagnoseExperimentalFlagFragment::class.java)
|
||||||
|
class ExitReasons(previous: Main): FragmentStateLegacy(previous, DiagnoseExitReasonFragment::class.java)
|
||||||
|
class Crypto(previous: Main): FragmentStateLegacy(previous, DiagnoseCryptoFragment::class.java)
|
||||||
|
class ForegroundApp(previous: Main): FragmentStateLegacy(previous, DiagnoseForegroundAppFragment::class.java)
|
||||||
|
class Sync(previous: Main): FragmentStateLegacy(previous, DiagnoseSyncFragment::class.java)
|
||||||
|
}
|
||||||
|
object Setup {
|
||||||
|
class SetupTerms: FragmentStateLegacy(previous = null, fragmentClass = SetupTermsFragment::class.java)
|
||||||
|
class SetupHelpInfo(previous: SetupTerms): FragmentStateLegacy(previous = previous, fragmentClass = SetupHelpInfoFragment::class.java)
|
||||||
|
class SelectMode(previous: SetupHelpInfo): FragmentStateLegacy(previous = previous, fragmentClass = SetupSelectModeFragment::class.java)
|
||||||
|
class DevicePermissions(previous: SelectMode): FragmentStateLegacy(previous = previous, fragmentClass = SetupDevicePermissionsFragment::class.java)
|
||||||
|
class LocalMode(previous: DevicePermissions): FragmentStateLegacy(previous = previous, fragmentClass = SetupLocalModeFragment::class.java)
|
||||||
|
class RemoteChild(previous: SelectMode): FragmentStateLegacy(previous = previous, fragmentClass = SetupRemoteChildFragment::class.java)
|
||||||
|
class ParentMode(previous: SelectMode): FragmentStateLegacy(previous = previous, fragmentClass = SetupParentModeFragment::class.java)
|
||||||
|
}
|
||||||
|
class ParentMode: FragmentStateLegacy(previous = null, fragmentClass = ParentModeFragment::class.java)
|
||||||
|
object Purchase {
|
||||||
|
class Purchase(previous: About): FragmentStateLegacy(previous, PurchaseFragment::class.java)
|
||||||
|
class StayAwesome(previous: About): FragmentStateLegacy(previous, StayAwesomeFragment::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,395 @@
|
||||||
|
/*
|
||||||
|
* 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.model
|
||||||
|
|
||||||
|
import io.timelimit.android.ui.model.main.OverviewHandling
|
||||||
|
|
||||||
|
sealed class UpdateStateCommand {
|
||||||
|
abstract fun transform(state: State): State?
|
||||||
|
|
||||||
|
object Reset: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State) = State.LaunchState
|
||||||
|
}
|
||||||
|
object BackToPreviousScreen: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? = state.previous
|
||||||
|
}
|
||||||
|
object Launch {
|
||||||
|
object LaunchOverview: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.LaunchState) State.Overview()
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
class LaunchChild(val childId: String): UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.LaunchState) State.ManageChild.Main(
|
||||||
|
childId = childId,
|
||||||
|
fromRedirect = true,
|
||||||
|
previous = State.Overview()
|
||||||
|
)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object LaunchDeviceSetup: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.LaunchState) State.SetupDevice(State.Overview())
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object LaunchTerms: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.LaunchState) State.Setup.SetupTerms()
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object LaunchParentMode: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.LaunchState) State.ParentMode()
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object Overview {
|
||||||
|
object LaunchAbout: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.Overview) State.About(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object AddUser: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.Overview) State.AddUser(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
data class ManageChild(val childId: String): UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.Overview) State.ManageChild.Main(state, childId, fromRedirect = false)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
data class ManageParent(val childId: String): UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.Overview) State.ManageParent.Main(state, childId)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
data class ManageDevice(val childId: String): UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.Overview) State.ManageDevice.Main(state, childId)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object SetupDevice: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.Overview) State.SetupDevice(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object Uninstall: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.Overview) State.Uninstall(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object ShowAllUsers: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.Overview) state.copy(
|
||||||
|
state = state.state.copy(showAllUsers = true)
|
||||||
|
)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShowMoreDevices(val deviceList: OverviewHandling.OverviewState.DeviceList): UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.Overview) state.copy(
|
||||||
|
state = state.state.copy(visibleDevices = deviceList)
|
||||||
|
)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object About {
|
||||||
|
object Diagnose: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.About) State.DiagnoseScreen.Main(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object Purchase: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.About) State.Purchase.Purchase(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object StayAwesome: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.About) State.Purchase.StayAwesome(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object AddUser {
|
||||||
|
object Leave: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.AddUser) state.previous
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ManageDevice {
|
||||||
|
data class User(val childId: String): UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageDevice.Main) State.ManageDevice.User(state, childId)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
data class Permissions(val childId: String): UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageDevice.Main) State.ManageDevice.Permissions(state, childId)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
data class Features(val childId: String): UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageDevice.Main) State.ManageDevice.Features(state, childId)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
data class Advanced(val childId: String): UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageDevice.Main) State.ManageDevice.Advanced(state, childId)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object Leave: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageDevice) when (state) {
|
||||||
|
is State.ManageDevice.Main -> state.previousOverview
|
||||||
|
is State.ManageDevice.User -> state.previousMain.previousOverview
|
||||||
|
is State.ManageDevice.Permissions -> state.previousMain.previousOverview
|
||||||
|
is State.ManageDevice.Features -> state.previousMain.previousOverview
|
||||||
|
is State.ManageDevice.Advanced -> state.previousMain.previousOverview
|
||||||
|
}
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
class EnterFromDeviceSetup(val deviceId: String): UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.SetupDevice) State.ManageDevice.Main(
|
||||||
|
deviceId = deviceId,
|
||||||
|
previousOverview = state.previousOverview
|
||||||
|
)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object ManageChild {
|
||||||
|
object Apps: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageChild.Main) State.ManageChild.Apps(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object Advanced: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageChild.Main) State.ManageChild.Advanced(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object Contacts: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageChild.Main) State.ManageChild.Contacts(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object UsageHistory: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageChild.Main) State.ManageChild.UsageHistory(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object Tasks: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageChild.Main) State.ManageChild.Tasks(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
class Category(val childId: String, val categoryId: String): UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageChild.Main && state.childId == childId) State.ManageChild.ManageCategory.Main(previousChild = state, categoryId = categoryId)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object BlockedTimes: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageChild.ManageCategory.Main) State.ManageChild.ManageCategory.BlockedTimes(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object CategoryAdvanced: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageChild.ManageCategory.Main) State.ManageChild.ManageCategory.Advanced(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object LeaveCategory: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageChild.ManageCategory) when (state) {
|
||||||
|
is State.ManageChild.ManageCategory.Main -> state.previous
|
||||||
|
is State.ManageChild.ManageCategory.Advanced -> state.previousCategory.previous
|
||||||
|
is State.ManageChild.ManageCategory.BlockedTimes -> state.previousCategory.previous
|
||||||
|
}
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object LeaveChild: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageChild) when (state) {
|
||||||
|
is State.ManageChild.Main -> state.previous
|
||||||
|
is State.ManageChild.Apps -> state.previousChild.previous
|
||||||
|
is State.ManageChild.Advanced -> state.previousChild.previous
|
||||||
|
is State.ManageChild.Contacts -> state.previousChild.previous
|
||||||
|
is State.ManageChild.Tasks -> state.previousChild.previous
|
||||||
|
is State.ManageChild.UsageHistory -> state.previousChild.previous
|
||||||
|
is State.ManageChild.ManageCategory.Main -> state.previousChild.previous
|
||||||
|
is State.ManageChild.ManageCategory.Advanced -> state.previousCategory.previousChild.previous
|
||||||
|
is State.ManageChild.ManageCategory.BlockedTimes -> state.previousCategory.previousChild.previous
|
||||||
|
}
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object ManageParent {
|
||||||
|
object ChangePassword: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageParent.Main) State.ManageParent.ChangePassword(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object RestorePassword: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageParent.Main) State.ManageParent.RestorePassword(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object LinkMail: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageParent.Main) State.ManageParent.LinkMail(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object U2F: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageParent.Main) State.ManageParent.U2F(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object Leave: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageParent) when (state) {
|
||||||
|
is State.ManageParent.Main -> state.previous
|
||||||
|
is State.ManageParent.ChangePassword -> state.previousParent.previous
|
||||||
|
is State.ManageParent.U2F -> state.previousParent.previous
|
||||||
|
is State.ManageParent.LinkMail -> state.previousParent.previous
|
||||||
|
is State.ManageParent.RestorePassword -> state.previousParent.previous
|
||||||
|
}
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object LeaveRestorePassword: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageParent.RestorePassword) state.previous
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object LeaveChangePassword: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageParent.ChangePassword) state.previous
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object LeaveLinkMail: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.ManageParent.LinkMail) state.previous
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object Diagnose {
|
||||||
|
object Battery: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.Battery(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object Clock: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.Clock(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object Connection: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.Connection(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object Crypto: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.Crypto(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object ExperimentalFlags: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.ExperimentalFlags(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object ExitReasons: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.ExitReasons(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object ForegroundApp: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.ForegroundApp(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
object Sync: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.DiagnoseScreen.Main) State.DiagnoseScreen.Sync(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object Setup {
|
||||||
|
object Help: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.Setup.SetupTerms) State.Setup.SetupHelpInfo(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object SelectMode: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.Setup.SetupHelpInfo) State.Setup.SelectMode(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object DevicePermissions: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.Setup.SelectMode) State.Setup.DevicePermissions(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object LocalMode: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.Setup.DevicePermissions) State.Setup.LocalMode(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object RemoteChild: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.Setup.SelectMode) State.Setup.RemoteChild(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
object ParentMode: UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
if (state is State.Setup.SelectMode) State.Setup.ParentMode(state)
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecoverPassword(val parentId: String): UpdateStateCommand() {
|
||||||
|
override fun transform(state: State): State? =
|
||||||
|
state.find { it is State.Overview }?.let { overview ->
|
||||||
|
State.ManageParent.Main(
|
||||||
|
parentId = parentId,
|
||||||
|
previous = overview as State.Overview
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package io.timelimit.android.ui.model.launch
|
||||||
|
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
|
import io.timelimit.android.data.model.UserType
|
||||||
|
import io.timelimit.android.livedata.waitUntilValueMatches
|
||||||
|
import io.timelimit.android.logic.AppLogic
|
||||||
|
import io.timelimit.android.ui.model.State
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
object LaunchHandling {
|
||||||
|
suspend fun processLaunchState(
|
||||||
|
state: MutableStateFlow<State>,
|
||||||
|
logic: AppLogic
|
||||||
|
) {
|
||||||
|
val oldValue = state.value
|
||||||
|
|
||||||
|
if (oldValue is State.LaunchState) {
|
||||||
|
state.compareAndSet(oldValue, getInitialState(logic))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getInitialState(logic: AppLogic): State {
|
||||||
|
logic.isInitialized.waitUntilValueMatches { it == true }
|
||||||
|
|
||||||
|
// TODO: readd the obsolete dialog fragment
|
||||||
|
return Threads.database.executeAndWait {
|
||||||
|
val hasDeviceId = logic.database.config().getOwnDeviceIdSync() != null
|
||||||
|
val hasParentKey = logic.database.config().getParentModeKeySync() != null
|
||||||
|
|
||||||
|
if (hasDeviceId) {
|
||||||
|
val config = logic.database.derivedDataDao().getUserAndDeviceRelatedDataSync()
|
||||||
|
val overview = State.Overview()
|
||||||
|
|
||||||
|
if (config?.userRelatedData?.user?.type == UserType.Child) State.ManageChild.Main(
|
||||||
|
previous = overview,
|
||||||
|
childId = config.userRelatedData.user.id,
|
||||||
|
fromRedirect = true
|
||||||
|
)
|
||||||
|
else if (config?.userRelatedData == null) State.SetupDevice(overview)
|
||||||
|
else overview
|
||||||
|
} else if (hasParentKey) State.ParentMode()
|
||||||
|
else State.Setup.SetupTerms()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,299 @@
|
||||||
|
/*
|
||||||
|
* 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.model.main
|
||||||
|
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
|
import io.timelimit.android.BuildConfig
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
|
import io.timelimit.android.data.model.Device
|
||||||
|
import io.timelimit.android.data.model.HintsToShow
|
||||||
|
import io.timelimit.android.data.model.UserType
|
||||||
|
import io.timelimit.android.data.model.derived.FullChildTask
|
||||||
|
import io.timelimit.android.extensions.whileTrue
|
||||||
|
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.logic.AppLogic
|
||||||
|
import io.timelimit.android.logic.ServerApiLevelInfo
|
||||||
|
import io.timelimit.android.ui.model.Screen
|
||||||
|
import io.timelimit.android.ui.model.State
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object OverviewHandling {
|
||||||
|
fun processState(logic: AppLogic, scope: CoroutineScope, stateLive: MutableStateFlow<State>): Flow<Screen> {
|
||||||
|
val actions: Actions = getActions(logic, scope, stateLive)
|
||||||
|
val overviewStateLive: Flow<OverviewState> = stateLive.transform { if (it is State.Overview) emit(it.state) }
|
||||||
|
val overviewState2Live: Flow<State.Overview> = stateLive.transform { if (it is State.Overview) emit(it) }
|
||||||
|
val overviewScreenLive: Flow<OverviewScreen> = getScreen(logic, actions, overviewStateLive)
|
||||||
|
val hasMatchingStateLive = stateLive.map { it is State.Overview }.distinctUntilChanged()
|
||||||
|
|
||||||
|
return hasMatchingStateLive.whileTrue {
|
||||||
|
overviewState2Live.combine(overviewScreenLive) { state, overviewScreen ->
|
||||||
|
Screen.OverviewScreen(state, overviewScreen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getActions(logic: AppLogic, scope: CoroutineScope, stateLive: MutableStateFlow<State>): Actions = Actions(
|
||||||
|
hideIntro = {
|
||||||
|
scope.launch {
|
||||||
|
Threads.database.executeAndWait {
|
||||||
|
logic.database.config().setHintsShownSync(HintsToShow.OVERVIEW_INTRODUCTION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
skipTaskReview = { task ->
|
||||||
|
stateLive.update { oldState ->
|
||||||
|
if (oldState is State.Overview) oldState.copy(
|
||||||
|
state = oldState.state.copy(
|
||||||
|
hiddenTaskIds = oldState.state.hiddenTaskIds + task.task.childTask.taskId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else oldState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun getScreen(logic: AppLogic, actions: Actions, state: Flow<OverviewState>): Flow<OverviewScreen> {
|
||||||
|
val introLive = getIntroFlags(logic)
|
||||||
|
val taskToReviewLive = getTaskToReview(logic, state.map { it.hiddenTaskIds })
|
||||||
|
val usersLive = getUserList(logic, state.map { it.showAllUsers })
|
||||||
|
val devicesLive = getDeviceList(logic, state.map { it.visibleDevices })
|
||||||
|
|
||||||
|
return combine(
|
||||||
|
introLive, taskToReviewLive, usersLive, devicesLive
|
||||||
|
) { intro, taskToReview, users, devices ->
|
||||||
|
OverviewScreen(
|
||||||
|
intro = intro,
|
||||||
|
taskToReview = taskToReview,
|
||||||
|
users = users,
|
||||||
|
devices = devices,
|
||||||
|
actions = actions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUsers(logic: AppLogic): Flow<List<UserItem>> {
|
||||||
|
val userFlow = logic.database.user().getAllUsersFlow()
|
||||||
|
|
||||||
|
val timeFlow = flow {
|
||||||
|
while (true) {
|
||||||
|
emit(logic.realTimeLogic.getCurrentTimeInMillis())
|
||||||
|
delay(5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userFlow.combine(timeFlow) { users, time ->
|
||||||
|
users.map { user ->
|
||||||
|
UserItem(
|
||||||
|
id = user.id,
|
||||||
|
name = user.name,
|
||||||
|
type = user.type,
|
||||||
|
areLimitsTemporarilyDisabled = user.disableLimitsUntil >= time,
|
||||||
|
viewingNeedsAuthentication = user.restrictViewingToParents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUserList(logic: AppLogic, showAllUsersLive: Flow<Boolean>): Flow<UserList> {
|
||||||
|
val usersLive = getUsers(logic)
|
||||||
|
|
||||||
|
return usersLive.combine(showAllUsersLive) { users, showAllUsers ->
|
||||||
|
if (showAllUsers || users.none { it.type != UserType.Parent }) UserList(
|
||||||
|
list = users,
|
||||||
|
canAdd = true,
|
||||||
|
canShowMore = false
|
||||||
|
)
|
||||||
|
else UserList(
|
||||||
|
list = users.filter { it.type == UserType.Child },
|
||||||
|
canAdd = false,
|
||||||
|
canShowMore = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDevices(logic: AppLogic): Flow<List<DeviceItem>> {
|
||||||
|
val ownDeviceIdLive = logic.database.config().getOwnDeviceIdFlow()
|
||||||
|
val devicesWithUserNameLive = logic.database.device().getAllDevicesWithUserInfoFlow()
|
||||||
|
val connectedDevicesLive = logic.websocket.connectedDevices.asFlow()
|
||||||
|
|
||||||
|
return combine (
|
||||||
|
ownDeviceIdLive, devicesWithUserNameLive, connectedDevicesLive
|
||||||
|
) { ownDeviceId, devicesWithUserName, connectedDevices ->
|
||||||
|
devicesWithUserName.map { deviceWithUserName ->
|
||||||
|
DeviceItem(
|
||||||
|
device = deviceWithUserName.device,
|
||||||
|
userName = deviceWithUserName.currentUserName,
|
||||||
|
userType = deviceWithUserName.currentUserType,
|
||||||
|
isCurrentDevice = deviceWithUserName.device.id == ownDeviceId,
|
||||||
|
isConnected = connectedDevices.contains(deviceWithUserName.device.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDeviceList(logic: AppLogic, deviceListLive: Flow<OverviewState.DeviceList>): Flow<DeviceList> {
|
||||||
|
val devicesLive = getDevices(logic)
|
||||||
|
|
||||||
|
return devicesLive.combine(deviceListLive) { allDevices, deviceList ->
|
||||||
|
val bareMinimum = allDevices.filter { it.isCurrentDevice || it.device.isImportant }
|
||||||
|
val allChildDevices = allDevices.filter { it.isCurrentDevice || it.device.isImportant || it.userType == UserType.Child }
|
||||||
|
|
||||||
|
val list = when (deviceList) {
|
||||||
|
OverviewState.DeviceList.BareMinimum -> bareMinimum
|
||||||
|
OverviewState.DeviceList.AllChildDevices -> allChildDevices
|
||||||
|
OverviewState.DeviceList.AllDevices -> allDevices
|
||||||
|
}
|
||||||
|
|
||||||
|
val canShowMore = when (deviceList) {
|
||||||
|
OverviewState.DeviceList.BareMinimum ->
|
||||||
|
if (bareMinimum.size != allChildDevices.size) OverviewState.DeviceList.AllChildDevices
|
||||||
|
else if (bareMinimum.size != allDevices.size) OverviewState.DeviceList.AllDevices
|
||||||
|
else null
|
||||||
|
OverviewState.DeviceList.AllChildDevices ->
|
||||||
|
if (allChildDevices.size != allDevices.size) OverviewState.DeviceList.AllDevices
|
||||||
|
else null
|
||||||
|
OverviewState.DeviceList.AllDevices -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
val canAdd = list.size == allDevices.size
|
||||||
|
|
||||||
|
DeviceList(
|
||||||
|
list = list,
|
||||||
|
canAdd = canAdd,
|
||||||
|
canShowMore = canShowMore
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getIntroFlags(logic: AppLogic): Flow<IntroFlags> {
|
||||||
|
val showSetupOptionLive = logic.deviceUserEntry.map { it == null }.asFlow()
|
||||||
|
|
||||||
|
val serverVersionLive = logic.serverApiLevelLogic.infoLive.asFlow()
|
||||||
|
val showOutdatedServerLive = serverVersionLive.map { serverVersion ->
|
||||||
|
!serverVersion.hasLevelOrIsOffline(BuildConfig.minimumRecommendServerVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
val showServerMessageLive = logic.database.config().getServerMessageFlow()
|
||||||
|
|
||||||
|
val hasShownIntroductionLive = logic.database.config().wereHintsShown(HintsToShow.OVERVIEW_INTRODUCTION).asFlow()
|
||||||
|
val showIntroLive = hasShownIntroductionLive.map { !it }
|
||||||
|
|
||||||
|
return combine(
|
||||||
|
showSetupOptionLive, showOutdatedServerLive, showServerMessageLive, showIntroLive
|
||||||
|
) { showSetupOption, showOutdatedServer, showServerMessage, showIntro ->
|
||||||
|
IntroFlags(
|
||||||
|
showSetupOption = showSetupOption,
|
||||||
|
showOutdatedServer = showOutdatedServer,
|
||||||
|
showServerMessage = showServerMessage,
|
||||||
|
showIntro = showIntro
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private fun getTaskToReview(logic: AppLogic, hiddenTaskIdsLive: Flow<Set<String>>): Flow<TaskToReview?> {
|
||||||
|
val pendingTasksLive = logic.database.childTasks().getPendingTasksFlow()
|
||||||
|
val serverApiLevelLive = logic.serverApiLevelLogic.infoLive.asFlow()
|
||||||
|
val hasPremiumLive = logic.fullVersion.shouldProvideFullVersionFunctions.asFlow()
|
||||||
|
|
||||||
|
val taskWithChildLive = pendingTasksLive.combine(hiddenTaskIdsLive) { pendingTasks, hiddenTaskIds ->
|
||||||
|
pendingTasks
|
||||||
|
.filterNot { hiddenTaskIds.contains(it.childTask.taskId) }
|
||||||
|
.firstOrNull()
|
||||||
|
}.transformLatest {
|
||||||
|
if (it != null) {
|
||||||
|
emitAll(logic.database.user().getChildUserByIdLive(it.childId).asFlow().map { childInfo ->
|
||||||
|
if (childInfo == null) null
|
||||||
|
else Pair(it, childInfo.timeZone)
|
||||||
|
})
|
||||||
|
} else emit(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return combine(
|
||||||
|
serverApiLevelLive, hasPremiumLive, taskWithChildLive
|
||||||
|
) { serverApiLevel, hasPremium, taskWithChild ->
|
||||||
|
if (taskWithChild == null) null
|
||||||
|
else TaskToReview(
|
||||||
|
task = taskWithChild.first,
|
||||||
|
childTimezone = TimeZone.getTimeZone(taskWithChild.second),
|
||||||
|
serverApiLevel = serverApiLevel,
|
||||||
|
hasPremium = hasPremium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class OverviewState(
|
||||||
|
val hiddenTaskIds: Set<String>,
|
||||||
|
val visibleDevices: DeviceList,
|
||||||
|
val showAllUsers: Boolean
|
||||||
|
): java.io.Serializable {
|
||||||
|
companion object {
|
||||||
|
val empty = OverviewState(
|
||||||
|
hiddenTaskIds = emptySet(),
|
||||||
|
visibleDevices = DeviceList.BareMinimum,
|
||||||
|
showAllUsers = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class DeviceList {
|
||||||
|
BareMinimum, // current device + devices with warnings
|
||||||
|
AllChildDevices,
|
||||||
|
AllDevices
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class OverviewScreen(
|
||||||
|
val intro: IntroFlags,
|
||||||
|
val taskToReview: TaskToReview?,
|
||||||
|
val users: UserList,
|
||||||
|
val devices: DeviceList,
|
||||||
|
val actions: Actions
|
||||||
|
)
|
||||||
|
data class Actions(
|
||||||
|
val hideIntro: () -> Unit,
|
||||||
|
val skipTaskReview: (TaskToReview) -> Unit
|
||||||
|
)
|
||||||
|
data class IntroFlags(
|
||||||
|
val showSetupOption: Boolean,
|
||||||
|
val showOutdatedServer: Boolean,
|
||||||
|
val showServerMessage: String?,
|
||||||
|
val showIntro: Boolean
|
||||||
|
)
|
||||||
|
data class TaskToReview(
|
||||||
|
val task: FullChildTask,
|
||||||
|
val hasPremium: Boolean,
|
||||||
|
val childTimezone: TimeZone,
|
||||||
|
val serverApiLevel: ServerApiLevelInfo
|
||||||
|
)
|
||||||
|
data class UserItem(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val type: UserType,
|
||||||
|
val areLimitsTemporarilyDisabled: Boolean,
|
||||||
|
val viewingNeedsAuthentication: Boolean
|
||||||
|
)
|
||||||
|
data class UserList(val list: List<UserItem>, val canAdd: Boolean, val canShowMore: Boolean)
|
||||||
|
data class DeviceItem(val device: Device, val userName: String?, val userType: UserType?, val isCurrentDevice: Boolean, val isConnected: Boolean) {
|
||||||
|
val isMissingRequiredPermission = userType == UserType.Child && (
|
||||||
|
device.currentUsageStatsPermission == RuntimePermissionStatus.NotGranted || device.missingPermissionAtQOrLater)
|
||||||
|
}
|
||||||
|
data class DeviceList(val list: List<DeviceItem>, val canAdd: Boolean, val canShowMore: OverviewState.DeviceList?)
|
||||||
|
}
|
|
@ -1,132 +0,0 @@
|
||||||
/*
|
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 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.main
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import io.timelimit.android.R
|
|
||||||
import io.timelimit.android.extensions.safeNavigate
|
|
||||||
import io.timelimit.android.livedata.*
|
|
||||||
import io.timelimit.android.ui.fragment.SingleFragmentWrapper
|
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
|
||||||
import io.timelimit.android.ui.manage.device.add.AddDeviceFragment
|
|
||||||
import io.timelimit.android.ui.overview.about.AboutFragmentParentHandlers
|
|
||||||
import io.timelimit.android.ui.overview.overview.OverviewFragment
|
|
||||||
import io.timelimit.android.ui.overview.overview.OverviewFragmentParentHandlers
|
|
||||||
|
|
||||||
class MainFragment : SingleFragmentWrapper(), OverviewFragmentParentHandlers, AboutFragmentParentHandlers, FragmentWithCustomTitle {
|
|
||||||
override val showAuthButton: Boolean = true
|
|
||||||
|
|
||||||
override fun createChildFragment(): Fragment = OverviewFragment()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openAddDeviceScreen() {
|
|
||||||
AddDeviceFragment().show(parentFragmentManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openAddUserScreen() {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
MainFragmentDirections.actionOverviewFragmentToAddUserFragment(),
|
|
||||||
R.id.overviewFragment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openManageChildScreen(childId: String) {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
MainFragmentDirections.actionOverviewFragmentToManageChildFragment(childId = childId, fromRedirect = false),
|
|
||||||
R.id.overviewFragment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openManageDeviceScreen(deviceId: String) {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
MainFragmentDirections.actionOverviewFragmentToManageDeviceFragment(deviceId),
|
|
||||||
R.id.overviewFragment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onShowPurchaseScreen() {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
MainFragmentDirections.actionOverviewFragmentToPurchaseFragment(),
|
|
||||||
R.id.overviewFragment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onShowStayAwesomeScreen() {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
MainFragmentDirections.actionOverviewFragmentToStayAwesomeFragment(),
|
|
||||||
R.id.overviewFragment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openManageParentScreen(parentId: String) {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
MainFragmentDirections.actionOverviewFragmentToManageParentFragment(parentId),
|
|
||||||
R.id.overviewFragment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openSetupDeviceScreen() {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
MainFragmentDirections.actionOverviewFragmentToSetupDeviceFragment(),
|
|
||||||
R.id.overviewFragment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onShowDiagnoseScreen() {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
MainFragmentDirections.actionOverviewFragmentToDiagnoseMainFragment(),
|
|
||||||
R.id.overviewFragment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCustomTitle(): LiveData<String?> = liveDataFromNullableValue("${getString(R.string.main_tab_overview)} (${getString(R.string.app_name)})")
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
super.onCreateOptionsMenu(menu, inflater)
|
|
||||||
|
|
||||||
inflater.inflate(R.menu.fragment_main_menu, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
|
||||||
R.id.menu_main_about -> {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
MainFragmentDirections.actionOverviewFragmentToAboutFragmentWrapped(),
|
|
||||||
R.id.overviewFragment
|
|
||||||
)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
R.id.menu_main_uninstall -> {
|
|
||||||
navigation.safeNavigate(
|
|
||||||
MainFragmentDirections.actionOverviewFragmentToUninstallFragment(),
|
|
||||||
R.id.overviewFragment
|
|
||||||
)
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* 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.clickable
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
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)
|
||||||
|
@Composable
|
||||||
|
fun LazyItemScope.DeviceItem(
|
||||||
|
item: OverviewHandling.DeviceItem,
|
||||||
|
executeCommand: (UpdateStateCommand) -> Unit
|
||||||
|
) {
|
||||||
|
ListCardCommon.Card(
|
||||||
|
Modifier
|
||||||
|
.animateItemPlacement()
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.clickable(
|
||||||
|
onClick = {
|
||||||
|
executeCommand(UpdateStateCommand.Overview.ManageDevice(item.device.id))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ListCardCommon.TextWithIcon(
|
||||||
|
icon = Icons.Default.Smartphone,
|
||||||
|
label = stringResource(R.string.overview_device_item_name),
|
||||||
|
value = item.device.name,
|
||||||
|
style = MaterialTheme.typography.h6
|
||||||
|
)
|
||||||
|
|
||||||
|
if (item.userName != null) {
|
||||||
|
ListCardCommon.TextWithIcon(
|
||||||
|
icon = Icons.Default.AccountCircle,
|
||||||
|
label = stringResource(R.string.overview_device_item_user_name),
|
||||||
|
value = item.userName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.device.isUserKeptSignedIn) {
|
||||||
|
ListCardCommon.TextWithIcon(
|
||||||
|
icon = Icons.Default.LockOpen,
|
||||||
|
label = stringResource(R.string.overview_device_item_password_disabled),
|
||||||
|
value = stringResource(R.string.overview_device_item_password_disabled),
|
||||||
|
multiline = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.isConnected) {
|
||||||
|
ListCardCommon.TextWithIcon(
|
||||||
|
icon = Icons.Default.Wifi,
|
||||||
|
label = stringResource(R.string.overview_device_item_connected),
|
||||||
|
value = stringResource(R.string.overview_device_item_connected),
|
||||||
|
multiline = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.device.currentAppVersion < BuildConfig.VERSION_CODE) {
|
||||||
|
ListCardCommon.TextWithIcon(
|
||||||
|
icon = Icons.Default.Update,
|
||||||
|
label = stringResource(R.string.overview_device_item_older_version),
|
||||||
|
value = stringResource(R.string.overview_device_item_older_version),
|
||||||
|
tint = MaterialTheme.colors.primary,
|
||||||
|
multiline = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.device.hasAnyManipulation) {
|
||||||
|
ListCardCommon.TextWithIcon(
|
||||||
|
icon = Icons.Default.Warning,
|
||||||
|
label = stringResource(R.string.overview_device_item_manipulation),
|
||||||
|
value = stringResource(R.string.overview_device_item_manipulation),
|
||||||
|
tint = MaterialTheme.colors.error,
|
||||||
|
multiline = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.isMissingRequiredPermission) {
|
||||||
|
ListCardCommon.TextWithIcon(
|
||||||
|
icon = Icons.Default.Warning,
|
||||||
|
label = stringResource(R.string.overview_device_item_missing_permission),
|
||||||
|
value = stringResource(R.string.overview_device_item_missing_permission),
|
||||||
|
tint = MaterialTheme.colors.error,
|
||||||
|
multiline = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.device.didReportUninstall) {
|
||||||
|
ListCardCommon.TextWithIcon(
|
||||||
|
icon = Icons.Default.Warning,
|
||||||
|
label = stringResource(R.string.overview_device_item_uninstall),
|
||||||
|
value = stringResource(R.string.overview_device_item_uninstall),
|
||||||
|
tint = MaterialTheme.colors.error,
|
||||||
|
multiline = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.isCurrentDevice) {
|
||||||
|
ListCardCommon.TextWithIcon(
|
||||||
|
icon = null,
|
||||||
|
label = stringResource(R.string.manage_device_is_this_device),
|
||||||
|
value = stringResource(R.string.manage_device_is_this_device),
|
||||||
|
style = MaterialTheme.typography.subtitle1,
|
||||||
|
multiline = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* 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.layout.*
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.LocalTextStyle
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
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.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
object ListCardCommon {
|
||||||
|
@Composable
|
||||||
|
fun Card(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
androidx.compose.material.Card(
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(8.dp)) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TextWithIcon(
|
||||||
|
icon: ImageVector?,
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
style: TextStyle = LocalTextStyle.current,
|
||||||
|
tint: Color = Color.Unspecified,
|
||||||
|
multiline: Boolean = false
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
if (icon != null) Icon(icon, label, tint = tint)
|
||||||
|
else Spacer(Modifier.size(24.dp))
|
||||||
|
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
modifier = Modifier.weight(1.0f),
|
||||||
|
maxLines = if (multiline) Int.MAX_VALUE else 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = style,
|
||||||
|
color = tint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ActionButton(
|
||||||
|
label: String,
|
||||||
|
action: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.End
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = action
|
||||||
|
) {
|
||||||
|
Text(label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
/*
|
||||||
|
* 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.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
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 io.timelimit.android.R
|
||||||
|
|
||||||
|
object ListCommon {
|
||||||
|
@Composable
|
||||||
|
fun SectionHeader(text: String, modifier: Modifier = Modifier) {
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
style = MaterialTheme.typography.h5
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ActionListItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
label: String,
|
||||||
|
action: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(
|
||||||
|
onClickLabel = label,
|
||||||
|
onClick = action
|
||||||
|
)
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(icon, label)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShowMoreItem(modifier: Modifier = Modifier, action: () -> Unit) {
|
||||||
|
ActionListItem(
|
||||||
|
icon = Icons.Default.ExpandMore,
|
||||||
|
label = stringResource(R.string.generic_show_more),
|
||||||
|
action = action,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,173 +0,0 @@
|
||||||
/*
|
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 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 android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import io.timelimit.android.async.Threads
|
|
||||||
import io.timelimit.android.coroutines.CoroutineFragment
|
|
||||||
import io.timelimit.android.data.model.*
|
|
||||||
import io.timelimit.android.databinding.FragmentOverviewBinding
|
|
||||||
import io.timelimit.android.date.DateInTimezone
|
|
||||||
import io.timelimit.android.livedata.waitForNonNullValue
|
|
||||||
import io.timelimit.android.logic.AppLogic
|
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
|
||||||
import io.timelimit.android.logic.ServerApiLevelInfo
|
|
||||||
import io.timelimit.android.sync.actions.ReviewChildTaskAction
|
|
||||||
import io.timelimit.android.ui.main.ActivityViewModel
|
|
||||||
import io.timelimit.android.ui.main.getActivityViewModel
|
|
||||||
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class OverviewFragment : CoroutineFragment() {
|
|
||||||
private val handlers: OverviewFragmentParentHandlers by lazy { parentFragment as OverviewFragmentParentHandlers }
|
|
||||||
private val logic: AppLogic by lazy { DefaultAppLogic.with(requireContext()) }
|
|
||||||
private val auth: ActivityViewModel by lazy { getActivityViewModel(requireActivity()) }
|
|
||||||
private val model: OverviewFragmentModel by viewModels()
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
|
||||||
val binding = FragmentOverviewBinding.inflate(inflater, container, false)
|
|
||||||
val adapter = OverviewFragmentAdapter()
|
|
||||||
|
|
||||||
binding.recycler.adapter = adapter
|
|
||||||
binding.recycler.layoutManager = LinearLayoutManager(requireContext())
|
|
||||||
|
|
||||||
adapter.handlers = object: OverviewFragmentHandlers {
|
|
||||||
override fun onAddUserClicked() {
|
|
||||||
handlers.openAddUserScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDeviceClicked(device: Device) {
|
|
||||||
handlers.openManageDeviceScreen(deviceId = device.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUserClicked(user: User) {
|
|
||||||
if (
|
|
||||||
user.restrictViewingToParents &&
|
|
||||||
logic.deviceUserId.value != user.id &&
|
|
||||||
!auth.requestAuthenticationOrReturnTrue()
|
|
||||||
) {
|
|
||||||
// do "nothing"/ request authentication
|
|
||||||
} else {
|
|
||||||
when (user.type) {
|
|
||||||
UserType.Child -> handlers.openManageChildScreen(childId = user.id)
|
|
||||||
UserType.Parent -> handlers.openManageParentScreen(parentId = user.id)
|
|
||||||
}.let { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAddDeviceClicked() {
|
|
||||||
launch {
|
|
||||||
if (logic.database.config().getDeviceAuthTokenAsync().waitForNonNullValue().isEmpty()) {
|
|
||||||
CanNotAddDevicesInLocalModeDialogFragment()
|
|
||||||
.apply { setTargetFragment(this@OverviewFragment, 0) }
|
|
||||||
.show(fragmentManager!!)
|
|
||||||
} else if (auth.requestAuthenticationOrReturnTrue()) {
|
|
||||||
handlers.openAddDeviceScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFinishSetupClicked() {
|
|
||||||
handlers.openSetupDeviceScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onShowAllUsersClicked() {
|
|
||||||
model.showAllUsers()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSetDeviceListVisibility(level: DeviceListItemVisibility) {
|
|
||||||
model.showMoreDevices(level)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTaskConfirmed(task: ChildTask, hasPremium: Boolean, timezone: TimeZone, serverApiLevel: ServerApiLevelInfo) {
|
|
||||||
if (hasPremium) {
|
|
||||||
val time = logic.timeApi.getCurrentTimeInMillis()
|
|
||||||
val day = DateInTimezone.newInstance(time, timezone).dayOfEpoch
|
|
||||||
|
|
||||||
auth.tryDispatchParentAction(
|
|
||||||
ReviewChildTaskAction(
|
|
||||||
taskId = task.taskId,
|
|
||||||
ok = true,
|
|
||||||
time = time,
|
|
||||||
day = if (serverApiLevel.hasLevelOrIsOffline(2)) day else null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else RequiresPurchaseDialogFragment().show(parentFragmentManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTaskRejected(task: ChildTask) {
|
|
||||||
auth.tryDispatchParentAction(
|
|
||||||
ReviewChildTaskAction(
|
|
||||||
taskId = task.taskId,
|
|
||||||
ok = false,
|
|
||||||
time = logic.timeApi.getCurrentTimeInMillis(),
|
|
||||||
day = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSkipTaskReviewClicked(task: ChildTask) {
|
|
||||||
if (auth.requestAuthenticationOrReturnTrue()) model.hideTask(task.taskId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
model.listEntries.observe(viewLifecycleOwner) { adapter.data = it }
|
|
||||||
|
|
||||||
ItemTouchHelper(
|
|
||||||
object: ItemTouchHelper.Callback() {
|
|
||||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
|
||||||
val index = viewHolder.adapterPosition
|
|
||||||
val item = if (index == RecyclerView.NO_POSITION) null else adapter.data!![index]
|
|
||||||
|
|
||||||
if (item == OverviewFragmentHeaderIntro) {
|
|
||||||
return makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START or ItemTouchHelper.END) or
|
|
||||||
makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END or ItemTouchHelper.END)
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) = throw IllegalStateException()
|
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
|
||||||
// remove the introduction header
|
|
||||||
Threads.database.execute {
|
|
||||||
logic.database.config().setHintsShownSync(HintsToShow.OVERVIEW_INTRODUCTION)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).attachToRecyclerView(binding.recycler)
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OverviewFragmentParentHandlers {
|
|
||||||
fun openAddUserScreen()
|
|
||||||
fun openAddDeviceScreen()
|
|
||||||
fun openManageDeviceScreen(deviceId: String)
|
|
||||||
fun openManageChildScreen(childId: String)
|
|
||||||
fun openManageParentScreen(parentId: String)
|
|
||||||
fun openSetupDeviceScreen()
|
|
||||||
}
|
|
|
@ -1,330 +0,0 @@
|
||||||
/*
|
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 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 android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import io.timelimit.android.BuildConfig
|
|
||||||
import io.timelimit.android.R
|
|
||||||
import io.timelimit.android.data.model.ChildTask
|
|
||||||
import io.timelimit.android.data.model.Device
|
|
||||||
import io.timelimit.android.data.model.User
|
|
||||||
import io.timelimit.android.data.model.UserType
|
|
||||||
import io.timelimit.android.databinding.*
|
|
||||||
import io.timelimit.android.logic.ServerApiLevelInfo
|
|
||||||
import io.timelimit.android.ui.util.DateUtil
|
|
||||||
import io.timelimit.android.util.TimeTextUtil
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.properties.Delegates
|
|
||||||
|
|
||||||
class OverviewFragmentAdapter : RecyclerView.Adapter<OverviewFragmentViewHolder>() {
|
|
||||||
init {
|
|
||||||
setHasStableIds(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
var data: List<OverviewFragmentItem>? by Delegates.observable(null as List<OverviewFragmentItem>?) { _, _, _ -> notifyDataSetChanged() }
|
|
||||||
var handlers: OverviewFragmentHandlers? = null
|
|
||||||
|
|
||||||
fun getItem(index: Int): OverviewFragmentItem {
|
|
||||||
return data!![index]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
val item = getItem(position)
|
|
||||||
|
|
||||||
return when (item) {
|
|
||||||
is OverviewFragmentItemDevice -> "device ${item.device.id}".hashCode().toLong()
|
|
||||||
is OverviewFragmentItemUser -> "user ${item.user.id}".hashCode().toLong()
|
|
||||||
is TaskReviewOverviewItem -> "task ${item.task.taskId}".hashCode().toLong()
|
|
||||||
else -> item.hashCode().toLong()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
val data = this.data
|
|
||||||
|
|
||||||
if (data == null) {
|
|
||||||
return 0
|
|
||||||
} else {
|
|
||||||
return data.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getItemType(item: OverviewFragmentItem): OverviewFragmentViewType = when(item) {
|
|
||||||
is OverviewFragmentHeaderUsers -> OverviewFragmentViewType.Header
|
|
||||||
is OverviewFragmentHeaderDevices -> OverviewFragmentViewType.Header
|
|
||||||
is OverviewFragmentItemUser -> OverviewFragmentViewType.UserItem
|
|
||||||
is OverviewFragmentItemDevice -> OverviewFragmentViewType.DeviceItem
|
|
||||||
is OverviewFragmentActionAddUser -> OverviewFragmentViewType.AddUserItem
|
|
||||||
is OverviewFragmentActionAddDevice -> OverviewFragmentViewType.AddDeviceItem
|
|
||||||
is OverviewFragmentHeaderIntro -> OverviewFragmentViewType.Introduction
|
|
||||||
is OverviewFragmentHeaderFinishSetup -> OverviewFragmentViewType.FinishSetup
|
|
||||||
is OverviewFragmentItemMessage -> OverviewFragmentViewType.ServerMessage
|
|
||||||
is ShowMoreOverviewFragmentItem -> OverviewFragmentViewType.ShowMoreButton
|
|
||||||
is TaskReviewOverviewItem -> OverviewFragmentViewType.TaskReview
|
|
||||||
is OverviewFragmentItemOutdatedServer -> OverviewFragmentViewType.ServerMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) = getItemType(getItem(position)).ordinal
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when(viewType) {
|
|
||||||
OverviewFragmentViewType.Header.ordinal ->
|
|
||||||
HeaderViewHolder(
|
|
||||||
GenericListHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
)
|
|
||||||
|
|
||||||
OverviewFragmentViewType.UserItem.ordinal ->
|
|
||||||
UserViewHolder(
|
|
||||||
FragmentOverviewUserItemBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
OverviewFragmentViewType.DeviceItem.ordinal ->
|
|
||||||
DeviceViewHolder(
|
|
||||||
FragmentOverviewDeviceItemBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
OverviewFragmentViewType.AddUserItem.ordinal ->
|
|
||||||
AddUserViewHolder(
|
|
||||||
AddItemViewBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
).apply {
|
|
||||||
label = parent.context.getString(R.string.add_user_title)
|
|
||||||
|
|
||||||
root.setOnClickListener {
|
|
||||||
handlers?.onAddUserClicked()
|
|
||||||
}
|
|
||||||
}.root
|
|
||||||
)
|
|
||||||
|
|
||||||
OverviewFragmentViewType.AddDeviceItem.ordinal -> AddDeviceViewHolder(
|
|
||||||
AddItemViewBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
).apply {
|
|
||||||
label = parent.context.getString(R.string.overview_add_device)
|
|
||||||
|
|
||||||
root.setOnClickListener {
|
|
||||||
handlers?.onAddDeviceClicked()
|
|
||||||
}
|
|
||||||
}.root
|
|
||||||
)
|
|
||||||
|
|
||||||
OverviewFragmentViewType.Introduction.ordinal -> IntroViewHolder(
|
|
||||||
LayoutInflater.from(parent.context)
|
|
||||||
.inflate(R.layout.fragment_overview_intro, parent, false)
|
|
||||||
)
|
|
||||||
|
|
||||||
OverviewFragmentViewType.FinishSetup.ordinal -> FinishSetupViewHolder(
|
|
||||||
FragmentOverviewFinishSetupBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
).apply {
|
|
||||||
btnGo.setOnClickListener { handlers?.onFinishSetupClicked() }
|
|
||||||
}.root
|
|
||||||
)
|
|
||||||
|
|
||||||
OverviewFragmentViewType.ServerMessage.ordinal -> ServerMessageViewHolder(
|
|
||||||
FragmentOverviewServerMessageBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
OverviewFragmentViewType.ShowMoreButton.ordinal -> ShowMoreViewHolder(
|
|
||||||
LayoutInflater.from(parent.context)
|
|
||||||
.inflate(R.layout.show_more_list_item, parent, false)
|
|
||||||
)
|
|
||||||
|
|
||||||
OverviewFragmentViewType.TaskReview.ordinal -> TaskReviewHolder(
|
|
||||||
FragmentOverviewTaskReviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: OverviewFragmentViewHolder, position: Int) {
|
|
||||||
val context = holder.itemView.context
|
|
||||||
val item = getItem(position)
|
|
||||||
|
|
||||||
when (item) {
|
|
||||||
is OverviewFragmentHeaderUsers -> {
|
|
||||||
if (holder !is HeaderViewHolder) {
|
|
||||||
throw IllegalStateException()
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.header.text = holder.itemView.context.getString(R.string.overview_header_users)
|
|
||||||
holder.header.executePendingBindings()
|
|
||||||
}
|
|
||||||
is OverviewFragmentHeaderDevices -> {
|
|
||||||
if (holder !is HeaderViewHolder) {
|
|
||||||
throw IllegalStateException()
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.header.text = holder.itemView.context.getString(R.string.overview_header_devices)
|
|
||||||
holder.header.executePendingBindings()
|
|
||||||
}
|
|
||||||
is OverviewFragmentItemUser -> {
|
|
||||||
if (holder !is UserViewHolder) {
|
|
||||||
throw IllegalStateException()
|
|
||||||
}
|
|
||||||
|
|
||||||
val binding = holder.binding
|
|
||||||
|
|
||||||
binding.username = item.user.name
|
|
||||||
binding.areTimeLimitsDisabled = item.limitsTemporarilyDisabled
|
|
||||||
binding.isTemporarilyBlocked = item.temporarilyBlocked
|
|
||||||
binding.isParent = item.user.type == UserType.Parent
|
|
||||||
binding.isChild = item.user.type == UserType.Child
|
|
||||||
|
|
||||||
binding.card.setOnClickListener {
|
|
||||||
this.handlers?.onUserClicked(item.user)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.executePendingBindings()
|
|
||||||
}
|
|
||||||
is OverviewFragmentItemDevice -> {
|
|
||||||
if (holder !is DeviceViewHolder) {
|
|
||||||
throw IllegalStateException()
|
|
||||||
}
|
|
||||||
|
|
||||||
val binding = holder.binding
|
|
||||||
|
|
||||||
binding.deviceTitle = item.device.name
|
|
||||||
binding.currentDeviceUserTitle = item.deviceUser?.name
|
|
||||||
binding.hasManipulation = item.device.hasAnyManipulation
|
|
||||||
binding.isCurrentDevice = item.isCurrentDevice
|
|
||||||
binding.isMissingRequiredPermission = item.isMissingRequiredPermission
|
|
||||||
binding.didUninstall = item.device.didReportUninstall
|
|
||||||
binding.isPasswordDisabled = item.device.isUserKeptSignedIn
|
|
||||||
binding.isConnected = item.isConnected
|
|
||||||
binding.isUsingOlderVersion = item.device.currentAppVersion < BuildConfig.VERSION_CODE
|
|
||||||
binding.executePendingBindings()
|
|
||||||
|
|
||||||
binding.card.setOnClickListener {
|
|
||||||
this.handlers?.onDeviceClicked(item.device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is OverviewFragmentActionAddUser -> { /* nothing to do */ }
|
|
||||||
is OverviewFragmentActionAddDevice-> { /* nothing to do */ }
|
|
||||||
is OverviewFragmentHeaderIntro -> { /* nothing to do */ }
|
|
||||||
is OverviewFragmentHeaderFinishSetup -> { /* nothing to do */ }
|
|
||||||
is OverviewFragmentItemMessage -> {
|
|
||||||
holder as ServerMessageViewHolder
|
|
||||||
|
|
||||||
holder.binding.title = context.getString(R.string.overview_server_message)
|
|
||||||
holder.binding.text = item.message
|
|
||||||
holder.binding.executePendingBindings()
|
|
||||||
}
|
|
||||||
is ShowMoreOverviewFragmentItem -> {
|
|
||||||
holder as ShowMoreViewHolder
|
|
||||||
|
|
||||||
when (item) {
|
|
||||||
is ShowMoreOverviewFragmentItem.ShowAllUsers -> {
|
|
||||||
holder.itemView.setOnClickListener { handlers?.onShowAllUsersClicked() }
|
|
||||||
}
|
|
||||||
is ShowMoreOverviewFragmentItem.ShowMoreDevices -> {
|
|
||||||
holder.itemView.setOnClickListener { handlers?.onSetDeviceListVisibility(item.level) }
|
|
||||||
}
|
|
||||||
}.let { }
|
|
||||||
}
|
|
||||||
is TaskReviewOverviewItem -> {
|
|
||||||
holder as TaskReviewHolder
|
|
||||||
|
|
||||||
holder.binding.let {
|
|
||||||
it.categoryTitle = item.categoryTitle
|
|
||||||
it.childName = item.childTitle
|
|
||||||
it.duration = TimeTextUtil.time(item.task.extraTimeDuration, it.root.context)
|
|
||||||
it.lastGrant = if (item.task.lastGrantTimestamp == 0L) null else DateUtil.formatAbsoluteDate(it.root.context, item.task.lastGrantTimestamp)
|
|
||||||
it.taskTitle = item.task.taskTitle
|
|
||||||
|
|
||||||
it.yesButton.setOnClickListener {
|
|
||||||
handlers?.onTaskConfirmed(
|
|
||||||
task = item.task,
|
|
||||||
hasPremium = item.hasPremium,
|
|
||||||
timezone = item.childTimezone,
|
|
||||||
serverApiLevel = item.serverApiLevel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
it.noButton.setOnClickListener { handlers?.onTaskRejected(item.task) }
|
|
||||||
it.skipButton.setOnClickListener { handlers?.onSkipTaskReviewClicked(item.task) }
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.binding.executePendingBindings()
|
|
||||||
}
|
|
||||||
is OverviewFragmentItemOutdatedServer -> {
|
|
||||||
holder as ServerMessageViewHolder
|
|
||||||
|
|
||||||
holder.binding.title = context.getString(R.string.overview_server_outdated_title)
|
|
||||||
holder.binding.text = context.getString(R.string.overview_server_outdated_text)
|
|
||||||
holder.binding.executePendingBindings()
|
|
||||||
}
|
|
||||||
}.let { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class OverviewFragmentViewType {
|
|
||||||
Header,
|
|
||||||
UserItem,
|
|
||||||
DeviceItem,
|
|
||||||
AddUserItem,
|
|
||||||
AddDeviceItem,
|
|
||||||
Introduction,
|
|
||||||
FinishSetup,
|
|
||||||
ServerMessage,
|
|
||||||
ShowMoreButton,
|
|
||||||
TaskReview
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class OverviewFragmentViewHolder(view: View): RecyclerView.ViewHolder(view)
|
|
||||||
class HeaderViewHolder(val header: GenericListHeaderBinding): OverviewFragmentViewHolder(header.root)
|
|
||||||
class AddUserViewHolder(view: View): OverviewFragmentViewHolder(view)
|
|
||||||
class UserViewHolder(val binding: FragmentOverviewUserItemBinding): OverviewFragmentViewHolder(binding.root)
|
|
||||||
class DeviceViewHolder(val binding: FragmentOverviewDeviceItemBinding): OverviewFragmentViewHolder(binding.root)
|
|
||||||
class AddDeviceViewHolder(view: View): OverviewFragmentViewHolder(view)
|
|
||||||
class IntroViewHolder(view: View): OverviewFragmentViewHolder(view)
|
|
||||||
class FinishSetupViewHolder(view: View): OverviewFragmentViewHolder(view)
|
|
||||||
class ServerMessageViewHolder(val binding: FragmentOverviewServerMessageBinding): OverviewFragmentViewHolder(binding.root)
|
|
||||||
class ShowMoreViewHolder(view: View): OverviewFragmentViewHolder(view)
|
|
||||||
class TaskReviewHolder(val binding: FragmentOverviewTaskReviewBinding): OverviewFragmentViewHolder(binding.root)
|
|
||||||
|
|
||||||
interface OverviewFragmentHandlers {
|
|
||||||
fun onAddUserClicked()
|
|
||||||
fun onAddDeviceClicked()
|
|
||||||
fun onUserClicked(user: User)
|
|
||||||
fun onDeviceClicked(device: Device)
|
|
||||||
fun onFinishSetupClicked()
|
|
||||||
fun onShowAllUsersClicked()
|
|
||||||
fun onSetDeviceListVisibility(level: DeviceListItemVisibility)
|
|
||||||
fun onSkipTaskReviewClicked(task: ChildTask)
|
|
||||||
fun onTaskConfirmed(task: ChildTask, hasPremium: Boolean, timezone: TimeZone, serverApiLevel: ServerApiLevelInfo)
|
|
||||||
fun onTaskRejected(task: ChildTask)
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
/*
|
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 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 io.timelimit.android.data.model.ChildTask
|
|
||||||
import io.timelimit.android.data.model.Device
|
|
||||||
import io.timelimit.android.data.model.User
|
|
||||||
import io.timelimit.android.data.model.UserType
|
|
||||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
|
||||||
import io.timelimit.android.logic.ServerApiLevelInfo
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
sealed class OverviewFragmentItem
|
|
||||||
object OverviewFragmentHeaderUsers: OverviewFragmentItem()
|
|
||||||
object OverviewFragmentHeaderDevices: OverviewFragmentItem()
|
|
||||||
data class OverviewFragmentItemDevice(val device: Device, val deviceUser: User?, val isCurrentDevice: Boolean, val isConnected: Boolean): OverviewFragmentItem() {
|
|
||||||
val isMissingRequiredPermission = deviceUser?.type == UserType.Child && (
|
|
||||||
device.currentUsageStatsPermission == RuntimePermissionStatus.NotGranted || device.missingPermissionAtQOrLater
|
|
||||||
)
|
|
||||||
|
|
||||||
val isImportant get() = device.isImportant
|
|
||||||
}
|
|
||||||
data class OverviewFragmentItemUser(val user: User, val temporarilyBlocked: Boolean, val limitsTemporarilyDisabled: Boolean): OverviewFragmentItem()
|
|
||||||
object OverviewFragmentActionAddUser: OverviewFragmentItem()
|
|
||||||
object OverviewFragmentActionAddDevice: OverviewFragmentItem()
|
|
||||||
object OverviewFragmentHeaderIntro: OverviewFragmentItem()
|
|
||||||
object OverviewFragmentHeaderFinishSetup: OverviewFragmentItem()
|
|
||||||
data class OverviewFragmentItemMessage(val message: String): OverviewFragmentItem()
|
|
||||||
object OverviewFragmentItemOutdatedServer: OverviewFragmentItem()
|
|
||||||
sealed class ShowMoreOverviewFragmentItem: OverviewFragmentItem() {
|
|
||||||
object ShowAllUsers: ShowMoreOverviewFragmentItem()
|
|
||||||
data class ShowMoreDevices(val level: DeviceListItemVisibility): ShowMoreOverviewFragmentItem()
|
|
||||||
}
|
|
||||||
data class TaskReviewOverviewItem(
|
|
||||||
val task: ChildTask,
|
|
||||||
val childTitle: String,
|
|
||||||
val categoryTitle: String,
|
|
||||||
val hasPremium: Boolean,
|
|
||||||
val childTimezone: TimeZone,
|
|
||||||
val serverApiLevel: ServerApiLevelInfo
|
|
||||||
): OverviewFragmentItem()
|
|
|
@ -1,205 +0,0 @@
|
||||||
/*
|
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 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 android.app.Application
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import io.timelimit.android.BuildConfig
|
|
||||||
import io.timelimit.android.data.model.HintsToShow
|
|
||||||
import io.timelimit.android.data.model.UserType
|
|
||||||
import io.timelimit.android.livedata.*
|
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class OverviewFragmentModel(application: Application): AndroidViewModel(application) {
|
|
||||||
private val logic = DefaultAppLogic.with(application)
|
|
||||||
|
|
||||||
private val itemVisibility = MutableLiveData<OverviewItemVisibility>().apply { value = OverviewItemVisibility.default }
|
|
||||||
|
|
||||||
private val categoryEntries = logic.database.category().getAllCategoriesShortInfo()
|
|
||||||
private val usersWithTemporarilyDisabledLimits = logic.database.user().getAllUsersLive().switchMap {
|
|
||||||
users ->
|
|
||||||
|
|
||||||
liveDataFromFunction { logic.realTimeLogic.getCurrentTimeInMillis() }.map {
|
|
||||||
currentTime ->
|
|
||||||
|
|
||||||
users.map {
|
|
||||||
user ->
|
|
||||||
|
|
||||||
user to (user.disableLimitsUntil >= currentTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.ignoreUnchanged()
|
|
||||||
private val userEntries = usersWithTemporarilyDisabledLimits.switchMap { users ->
|
|
||||||
categoryEntries.switchMap { categories ->
|
|
||||||
liveDataFromFunction (5000) { logic.realTimeLogic.getCurrentTimeInMillis() }.map { now ->
|
|
||||||
users.map { user ->
|
|
||||||
OverviewFragmentItemUser(
|
|
||||||
user = user.first,
|
|
||||||
limitsTemporarilyDisabled = user.second,
|
|
||||||
temporarilyBlocked = categories.find { category ->
|
|
||||||
category.childId == user.first.id &&
|
|
||||||
category.temporarilyBlocked && (
|
|
||||||
category.temporarilyBlockedEndTime == 0L ||
|
|
||||||
category.temporarilyBlockedEndTime > now
|
|
||||||
)
|
|
||||||
} != null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.ignoreUnchanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val ownDeviceId = logic.deviceId
|
|
||||||
private val devices = logic.database.device().getAllDevicesLive()
|
|
||||||
private val devicesWithUsers = devices.switchMap { devices ->
|
|
||||||
usersWithTemporarilyDisabledLimits.map { users ->
|
|
||||||
devices.map { device ->
|
|
||||||
device to users.find { it.first.id == device.currentUserId }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val deviceEntries = ownDeviceId.switchMap { thisDeviceId ->
|
|
||||||
devicesWithUsers.switchMap { devices ->
|
|
||||||
logic.websocket.connectedDevices.map { connectedDevices ->
|
|
||||||
devices.map { (device, user) ->
|
|
||||||
OverviewFragmentItemDevice(
|
|
||||||
device = device,
|
|
||||||
deviceUser = user?.first,
|
|
||||||
isCurrentDevice = device.id == thisDeviceId,
|
|
||||||
isConnected = connectedDevices.contains(device.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val isNoUserAssignedLive = logic.deviceUserEntry.map { it == null }.ignoreUnchanged()
|
|
||||||
private val hasShownIntroduction = logic.database.config().wereHintsShown(HintsToShow.OVERVIEW_INTRODUCTION)
|
|
||||||
private val messageLive = logic.database.config().getServerMessage()
|
|
||||||
private val serverVersion = logic.serverApiLevelLogic.infoLive
|
|
||||||
private val introEntries = mergeLiveDataWaitForValues(isNoUserAssignedLive, hasShownIntroduction, messageLive, serverVersion)
|
|
||||||
.map { (noUserAssigned, hasShownIntro, message, serverVersion) ->
|
|
||||||
val result = mutableListOf<OverviewFragmentItem>()
|
|
||||||
|
|
||||||
if (noUserAssigned) {
|
|
||||||
result.add(OverviewFragmentHeaderFinishSetup)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!serverVersion.hasLevelOrIsOffline(BuildConfig.minimumRecommendServerVersion)) {
|
|
||||||
result.add(OverviewFragmentItemOutdatedServer)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message != null) {
|
|
||||||
result.add(OverviewFragmentItemMessage(message))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasShownIntro) {
|
|
||||||
result.add(OverviewFragmentHeaderIntro)
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
private val hiddenTaskIdsLive = MutableLiveData<Set<String>>().apply { value = emptySet() }
|
|
||||||
private val tasksWithPendingReviewLive = logic.database.childTasks().getPendingTasks()
|
|
||||||
private val pendingTasksToShowLive = hiddenTaskIdsLive.switchMap { hiddenTaskIds ->
|
|
||||||
tasksWithPendingReviewLive.map { tasksWithPendingReview ->
|
|
||||||
tasksWithPendingReview.filterNot { hiddenTaskIds.contains(it.childTask.taskId) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val hasPremiumLive = logic.fullVersion.shouldProvideFullVersionFunctions
|
|
||||||
private val pendingTaskItemLive = hasPremiumLive.switchMap { hasPremium ->
|
|
||||||
logic.serverApiLevelLogic.infoLive.switchMap { serverApiLevel ->
|
|
||||||
pendingTasksToShowLive.map { tasks ->
|
|
||||||
tasks.firstOrNull()?.let {
|
|
||||||
TaskReviewOverviewItem(
|
|
||||||
task = it.childTask,
|
|
||||||
childTitle = it.childName,
|
|
||||||
categoryTitle = it.categoryTitle,
|
|
||||||
hasPremium = hasPremium,
|
|
||||||
childTimezone = TimeZone.getTimeZone(it.childTimezone),
|
|
||||||
serverApiLevel = serverApiLevel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideTask(taskId: String) {
|
|
||||||
hiddenTaskIdsLive.value = (hiddenTaskIdsLive.value ?: emptySet()) + setOf(taskId)
|
|
||||||
}
|
|
||||||
|
|
||||||
val listEntries = introEntries.switchMap { introEntries ->
|
|
||||||
deviceEntries.switchMap { deviceEntries ->
|
|
||||||
userEntries.switchMap { userEntries ->
|
|
||||||
pendingTaskItemLive.switchMap { pendingTaskItem ->
|
|
||||||
itemVisibility.map { itemVisibility ->
|
|
||||||
mutableListOf<OverviewFragmentItem>().apply {
|
|
||||||
addAll(introEntries)
|
|
||||||
|
|
||||||
if (pendingTaskItem != null) add(pendingTaskItem)
|
|
||||||
|
|
||||||
add(OverviewFragmentHeaderDevices)
|
|
||||||
val shownDevices = when (itemVisibility.devices) {
|
|
||||||
DeviceListItemVisibility.BareMinimum -> deviceEntries.filter { it.isCurrentDevice || it.isImportant }
|
|
||||||
DeviceListItemVisibility.AllChildDevices -> deviceEntries.filter { it.isCurrentDevice || it.isImportant || it.deviceUser?.type == UserType.Child }
|
|
||||||
DeviceListItemVisibility.AllDevices -> deviceEntries
|
|
||||||
}
|
|
||||||
addAll(shownDevices)
|
|
||||||
if (shownDevices.size == deviceEntries.size) {
|
|
||||||
add(OverviewFragmentActionAddDevice)
|
|
||||||
} else {
|
|
||||||
add(ShowMoreOverviewFragmentItem.ShowMoreDevices(when (itemVisibility.devices) {
|
|
||||||
DeviceListItemVisibility.BareMinimum -> run {
|
|
||||||
if (
|
|
||||||
deviceEntries.any {
|
|
||||||
!it.isCurrentDevice &&
|
|
||||||
!it.isImportant &&
|
|
||||||
it.deviceUser?.type == UserType.Child
|
|
||||||
}
|
|
||||||
) DeviceListItemVisibility.AllChildDevices else DeviceListItemVisibility.AllDevices
|
|
||||||
}
|
|
||||||
DeviceListItemVisibility.AllChildDevices -> DeviceListItemVisibility.AllDevices
|
|
||||||
DeviceListItemVisibility.AllDevices -> DeviceListItemVisibility.AllDevices
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
add(OverviewFragmentHeaderUsers)
|
|
||||||
userEntries.forEach { if (it.user.type != UserType.Parent) add(it) }
|
|
||||||
if (itemVisibility.showParentUsers || userEntries.all { it.user.type == UserType.Parent }) {
|
|
||||||
userEntries.forEach { if (it.user.type == UserType.Parent) add(it) }
|
|
||||||
add(OverviewFragmentActionAddUser)
|
|
||||||
} else {
|
|
||||||
add(ShowMoreOverviewFragmentItem.ShowAllUsers)
|
|
||||||
}
|
|
||||||
}.toList()
|
|
||||||
}
|
|
||||||
} as LiveData<List<OverviewFragmentItem>>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showAllUsers() {
|
|
||||||
itemVisibility.value = itemVisibility.value!!.copy(showParentUsers = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showMoreDevices(level: DeviceListItemVisibility) {
|
|
||||||
itemVisibility.value = itemVisibility.value!!.copy(devices = level)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
/*
|
|
||||||
* TimeLimit Copyright <C> 2019 - 2021 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 android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
|
|
||||||
@Parcelize
|
|
||||||
data class OverviewItemVisibility(
|
|
||||||
val showParentUsers: Boolean,
|
|
||||||
val devices: DeviceListItemVisibility
|
|
||||||
) : Parcelable {
|
|
||||||
companion object {
|
|
||||||
val default = OverviewItemVisibility(
|
|
||||||
showParentUsers = false,
|
|
||||||
devices = DeviceListItemVisibility.BareMinimum
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class DeviceListItemVisibility {
|
|
||||||
BareMinimum, // current device + devices with warnings
|
|
||||||
AllChildDevices,
|
|
||||||
AllDevices
|
|
||||||
}
|
|
|
@ -0,0 +1,287 @@
|
||||||
|
/*
|
||||||
|
* 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.*
|
||||||
|
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 androidx.lifecycle.lifecycleScope
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.date.DateInTimezone
|
||||||
|
import io.timelimit.android.livedata.waitForNonNullValue
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.sync.actions.ReviewChildTaskAction
|
||||||
|
import io.timelimit.android.ui.MainActivity
|
||||||
|
import io.timelimit.android.ui.manage.device.add.AddDeviceFragment
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.main.OverviewHandling
|
||||||
|
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
||||||
|
import io.timelimit.android.util.TimeTextUtil
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun OverviewScreen(
|
||||||
|
screen: OverviewHandling.OverviewScreen,
|
||||||
|
executeCommand: (UpdateStateCommand) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val activity = LocalContext.current as MainActivity
|
||||||
|
|
||||||
|
LazyColumn (
|
||||||
|
contentPadding = PaddingValues(0.dp, 8.dp),
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
Row {
|
||||||
|
val auth = activity.getActivityViewModel()
|
||||||
|
val logic = auth.logic
|
||||||
|
|
||||||
|
TextButton(onClick = {
|
||||||
|
if (activity.getActivityViewModel().isParentAuthenticated()) {
|
||||||
|
screen.actions.skipTaskReview(screen.taskToReview)
|
||||||
|
} else activity.showAuthenticationScreen()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.generic_skip))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1.0f))
|
||||||
|
|
||||||
|
OutlinedButton(onClick = {
|
||||||
|
if (activity.getActivityViewModel().isParentAuthenticated()) {
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
ReviewChildTaskAction(
|
||||||
|
taskId = screen.taskToReview.task.childTask.taskId,
|
||||||
|
ok = false,
|
||||||
|
time = logic.timeApi.getCurrentTimeInMillis(),
|
||||||
|
day = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else activity.showAuthenticationScreen()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.generic_no))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
|
||||||
|
OutlinedButton(onClick = {
|
||||||
|
if (activity.getActivityViewModel().isParentAuthenticated()) {
|
||||||
|
if (screen.taskToReview.hasPremium) {
|
||||||
|
val time = logic.timeApi.getCurrentTimeInMillis()
|
||||||
|
val day = DateInTimezone.newInstance(time, screen.taskToReview.childTimezone).dayOfEpoch
|
||||||
|
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
ReviewChildTaskAction(
|
||||||
|
taskId = screen.taskToReview.task.childTask.taskId,
|
||||||
|
ok = true,
|
||||||
|
time = time,
|
||||||
|
day = if (screen.taskToReview.serverApiLevel.hasLevelOrIsOffline(2)) day else null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else RequiresPurchaseDialogFragment().show(activity.supportFragmentManager)
|
||||||
|
} else activity.showAuthenticationScreen()
|
||||||
|
}) {
|
||||||
|
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) {
|
||||||
|
// TODO: implement this without dependency on MainActivity
|
||||||
|
item (key = Pair("devices", "add")) {
|
||||||
|
ListCommon.ActionListItem(
|
||||||
|
icon = Icons.Default.Add,
|
||||||
|
label = stringResource(R.string.add_device),
|
||||||
|
action = {
|
||||||
|
activity.lifecycleScope.launch {
|
||||||
|
val logic = DefaultAppLogic.with(activity)
|
||||||
|
|
||||||
|
if (logic.database.config().getDeviceAuthTokenAsync()
|
||||||
|
.waitForNonNullValue().isEmpty()
|
||||||
|
) {
|
||||||
|
CanNotAddDevicesInLocalModeDialogFragment()
|
||||||
|
.show(activity.supportFragmentManager)
|
||||||
|
} else if (activity.getActivityViewModel().requestAuthenticationOrReturnTrue()) {
|
||||||
|
AddDeviceFragment().show(activity.supportFragmentManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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, executeCommand) }
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* 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.clickable
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
|
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.runtime.Composable
|
||||||
|
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.data.model.UserType
|
||||||
|
import io.timelimit.android.ui.MainActivity
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.main.OverviewHandling
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun LazyItemScope.UserItem(
|
||||||
|
user: OverviewHandling.UserItem,
|
||||||
|
executeCommand: (UpdateStateCommand) -> Unit
|
||||||
|
) {
|
||||||
|
// TODO: implement this without dependency on MainActivity
|
||||||
|
val activity = LocalContext.current as MainActivity
|
||||||
|
|
||||||
|
ListCardCommon.Card(
|
||||||
|
Modifier
|
||||||
|
.animateItemPlacement()
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.clickable(
|
||||||
|
onClick = {
|
||||||
|
when (user.type) {
|
||||||
|
UserType.Child -> {
|
||||||
|
if (!user.viewingNeedsAuthentication || activity.getActivityViewModel().isParentOrChildAuthenticated(user.id)) {
|
||||||
|
executeCommand(UpdateStateCommand.Overview.ManageChild(user.id))
|
||||||
|
} else {
|
||||||
|
activity.showAuthenticationScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UserType.Parent -> executeCommand(UpdateStateCommand.Overview.ManageParent(user.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ListCardCommon.TextWithIcon(
|
||||||
|
icon = Icons.Default.AccountCircle,
|
||||||
|
label = stringResource(R.string.overview_user_item_name),
|
||||||
|
value = user.name,
|
||||||
|
style = MaterialTheme.typography.h6
|
||||||
|
)
|
||||||
|
|
||||||
|
ListCardCommon.TextWithIcon(
|
||||||
|
icon = when (user.type) {
|
||||||
|
UserType.Child -> Icons.Default.Security
|
||||||
|
UserType.Parent -> Icons.Default.Settings
|
||||||
|
},
|
||||||
|
label = stringResource(R.string.overview_user_item_role),
|
||||||
|
value = when (user.type) {
|
||||||
|
UserType.Child -> stringResource(R.string.overview_user_item_role_child)
|
||||||
|
UserType.Parent -> stringResource(R.string.overview_user_item_role_parent)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (user.areLimitsTemporarilyDisabled) {
|
||||||
|
ListCardCommon.TextWithIcon(
|
||||||
|
icon = Icons.Default.AlarmOff,
|
||||||
|
label = stringResource(R.string.overview_user_item_temporarily_disabled),
|
||||||
|
value = stringResource(R.string.overview_user_item_temporarily_disabled)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -20,15 +20,14 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
|
||||||
import io.timelimit.android.async.Threads
|
import io.timelimit.android.async.Threads
|
||||||
import io.timelimit.android.databinding.FragmentSetupDevicePermissionsBinding
|
import io.timelimit.android.databinding.FragmentSetupDevicePermissionsBinding
|
||||||
import io.timelimit.android.extensions.safeNavigate
|
|
||||||
import io.timelimit.android.integration.platform.SystemPermission
|
import io.timelimit.android.integration.platform.SystemPermission
|
||||||
import io.timelimit.android.logic.AppLogic
|
import io.timelimit.android.logic.AppLogic
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.ui.manage.device.manage.permission.PermissionInfoHelpDialog
|
import io.timelimit.android.ui.manage.device.manage.permission.PermissionInfoHelpDialog
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
class SetupDevicePermissionsFragment : Fragment() {
|
class SetupDevicePermissionsFragment : Fragment() {
|
||||||
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||||
|
@ -45,8 +44,6 @@ class SetupDevicePermissionsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val navigation = Navigation.findNavController(container!!)
|
|
||||||
|
|
||||||
binding = FragmentSetupDevicePermissionsBinding.inflate(inflater, container, false)
|
binding = FragmentSetupDevicePermissionsBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
binding.handlers = object: SetupDevicePermissionsHandlers {
|
binding.handlers = object: SetupDevicePermissionsHandlers {
|
||||||
|
@ -71,11 +68,7 @@ class SetupDevicePermissionsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun gotoNextStep() {
|
override fun gotoNextStep() {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.Setup.LocalMode)
|
||||||
SetupDevicePermissionsFragmentDirections
|
|
||||||
.actionSetupDevicePermissionsFragmentToSetupLocalModeFragment(),
|
|
||||||
R.id.setupDevicePermissionsFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun helpUsageStatsAccess() {
|
override fun helpUsageStatsAccess() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -21,11 +21,11 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.databinding.SetupHelpInfoFragmentBinding
|
import io.timelimit.android.databinding.SetupHelpInfoFragmentBinding
|
||||||
import io.timelimit.android.extensions.safeNavigate
|
|
||||||
import io.timelimit.android.ui.help.HelpDialogFragment
|
import io.timelimit.android.ui.help.HelpDialogFragment
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
class SetupHelpInfoFragment: Fragment() {
|
class SetupHelpInfoFragment: Fragment() {
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
@ -41,10 +41,7 @@ class SetupHelpInfoFragment: Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.nextButton.setOnClickListener {
|
binding.nextButton.setOnClickListener {
|
||||||
Navigation.findNavController(view!!).safeNavigate(
|
requireActivity().execute(UpdateStateCommand.Setup.SelectMode)
|
||||||
SetupHelpInfoFragmentDirections.actionSetupHelpInfoFragmentToSetupSelectModeFragment(),
|
|
||||||
R.id.setupHelpInfoFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -28,7 +28,6 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.coroutines.runAsync
|
import io.timelimit.android.coroutines.runAsync
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -28,16 +28,15 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.async.Threads
|
import io.timelimit.android.async.Threads
|
||||||
import io.timelimit.android.coroutines.executeAndWait
|
import io.timelimit.android.coroutines.executeAndWait
|
||||||
import io.timelimit.android.coroutines.runAsync
|
import io.timelimit.android.coroutines.runAsync
|
||||||
import io.timelimit.android.databinding.FragmentSetupSelectModeBinding
|
import io.timelimit.android.databinding.FragmentSetupSelectModeBinding
|
||||||
import io.timelimit.android.extensions.safeNavigate
|
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
import io.timelimit.android.ui.setup.parentmode.SetupParentmodeDialogFragment
|
import io.timelimit.android.ui.setup.parentmode.SetupParentmodeDialogFragment
|
||||||
import io.timelimit.android.ui.setup.privacy.PrivacyInfoDialogFragment
|
import io.timelimit.android.ui.setup.privacy.PrivacyInfoDialogFragment
|
||||||
|
|
||||||
|
@ -49,7 +48,6 @@ class SetupSelectModeFragment : Fragment() {
|
||||||
private const val REQUEST_SETUP_PARENT_MODE = 3
|
private const val REQUEST_SETUP_PARENT_MODE = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var navigation: NavController
|
|
||||||
private lateinit var binding: FragmentSetupSelectModeBinding
|
private lateinit var binding: FragmentSetupSelectModeBinding
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
@ -61,13 +59,8 @@ class SetupSelectModeFragment : Fragment() {
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
navigation = Navigation.findNavController(view)
|
|
||||||
|
|
||||||
binding.btnLocalMode.setOnClickListener {
|
binding.btnLocalMode.setOnClickListener {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.Setup.DevicePermissions)
|
||||||
SetupSelectModeFragmentDirections.actionSetupSelectModeFragmentToSetupDevicePermissionsFragment(),
|
|
||||||
R.id.setupSelectModeFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.btnParentMode.setOnClickListener {
|
binding.btnParentMode.setOnClickListener {
|
||||||
|
@ -131,15 +124,9 @@ class SetupSelectModeFragment : Fragment() {
|
||||||
|
|
||||||
if (resultCode == Activity.RESULT_OK) {
|
if (resultCode == Activity.RESULT_OK) {
|
||||||
if (requestCode == REQ_SETUP_CONNECTED_CHILD) {
|
if (requestCode == REQ_SETUP_CONNECTED_CHILD) {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.Setup.RemoteChild)
|
||||||
SetupSelectModeFragmentDirections.actionSetupSelectModeFragmentToSetupRemoteChildFragment(),
|
|
||||||
R.id.setupSelectModeFragment
|
|
||||||
)
|
|
||||||
} else if (requestCode == REQ_SETUP_CONNECTED_PARENT) {
|
} else if (requestCode == REQ_SETUP_CONNECTED_PARENT) {
|
||||||
navigation.safeNavigate(
|
requireActivity().execute(UpdateStateCommand.Setup.ParentMode)
|
||||||
SetupSelectModeFragmentDirections.actionSetupSelectModeFragmentToSetupParentModeFragment(),
|
|
||||||
R.id.setupSelectModeFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -23,11 +23,11 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.databinding.FragmentSetupTermsBinding
|
import io.timelimit.android.databinding.FragmentSetupTermsBinding
|
||||||
import io.timelimit.android.extensions.safeNavigate
|
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
import io.timelimit.android.ui.obsolete.ObsoleteDialogFragment
|
import io.timelimit.android.ui.obsolete.ObsoleteDialogFragment
|
||||||
import io.timelimit.android.ui.setup.customserver.SelectCustomServerDialogFragment
|
import io.timelimit.android.ui.setup.customserver.SelectCustomServerDialogFragment
|
||||||
|
|
||||||
|
@ -66,9 +66,6 @@ class SetupTermsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun acceptTerms() {
|
private fun acceptTerms() {
|
||||||
Navigation.findNavController(view!!).safeNavigate(
|
requireActivity().execute(UpdateStateCommand.Setup.Help)
|
||||||
SetupTermsFragmentDirections.actionSetupTermsFragmentToSetupHelpInfoFragment(),
|
|
||||||
R.id.setupTermsFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -33,21 +33,20 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.observe
|
import androidx.lifecycle.observe
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.coroutines.runAsync
|
import io.timelimit.android.coroutines.runAsync
|
||||||
import io.timelimit.android.data.IdGenerator
|
import io.timelimit.android.data.IdGenerator
|
||||||
import io.timelimit.android.data.model.AppRecommendation
|
import io.timelimit.android.data.model.AppRecommendation
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
import io.timelimit.android.databinding.FragmentSetupDeviceBinding
|
import io.timelimit.android.databinding.FragmentSetupDeviceBinding
|
||||||
import io.timelimit.android.extensions.safeNavigate
|
|
||||||
import io.timelimit.android.livedata.*
|
import io.timelimit.android.livedata.*
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
import io.timelimit.android.ui.manage.device.manage.advanced.ManageDeviceBackgroundSync
|
import io.timelimit.android.ui.manage.device.manage.advanced.ManageDeviceBackgroundSync
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
import io.timelimit.android.ui.mustread.MustReadFragment
|
import io.timelimit.android.ui.mustread.MustReadFragment
|
||||||
import io.timelimit.android.ui.overview.main.MainFragmentDirections
|
|
||||||
import io.timelimit.android.ui.setup.SetupNetworkTimeVerification
|
import io.timelimit.android.ui.setup.SetupNetworkTimeVerification
|
||||||
import io.timelimit.android.ui.update.UpdateConsentCard
|
import io.timelimit.android.ui.update.UpdateConsentCard
|
||||||
import io.timelimit.android.ui.view.NotifyPermissionCard
|
import io.timelimit.android.ui.view.NotifyPermissionCard
|
||||||
|
@ -121,7 +120,6 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
val binding = FragmentSetupDeviceBinding.inflate(inflater, container, false)
|
val binding = FragmentSetupDeviceBinding.inflate(inflater, container, false)
|
||||||
val logic = DefaultAppLogic.with(requireContext())
|
val logic = DefaultAppLogic.with(requireContext())
|
||||||
val activity = activity as ActivityViewModelHolder
|
val activity = activity as ActivityViewModelHolder
|
||||||
val navigation = Navigation.findNavController(container!!)
|
|
||||||
|
|
||||||
binding.needsParent.authBtn.setOnClickListener {
|
binding.needsParent.authBtn.setOnClickListener {
|
||||||
activity.showAuthenticationScreen()
|
activity.showAuthenticationScreen()
|
||||||
|
@ -147,13 +145,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
|
||||||
val ownDeviceId = logic.deviceId.waitForNullableValue()!!
|
val ownDeviceId = logic.deviceId.waitForNullableValue()!!
|
||||||
|
|
||||||
navigation.popBackStack()
|
requireActivity().execute(UpdateStateCommand.ManageDevice.EnterFromDeviceSetup(ownDeviceId))
|
||||||
navigation.safeNavigate(
|
|
||||||
MainFragmentDirections.actionOverviewFragmentToManageDeviceFragment(
|
|
||||||
ownDeviceId
|
|
||||||
),
|
|
||||||
R.id.overviewFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SetupDeviceModelStatus.Working -> { /* nothing to do */ }
|
SetupDeviceModelStatus.Working -> { /* nothing to do */ }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -24,7 +24,6 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.navigation.Navigation
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
|
@ -33,6 +32,8 @@ import io.timelimit.android.livedata.*
|
||||||
import io.timelimit.android.ui.main.ActivityViewModel
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
|
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||||
|
import io.timelimit.android.ui.model.execute
|
||||||
|
|
||||||
class AddUserFragment : Fragment(), FragmentWithCustomTitle {
|
class AddUserFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -47,7 +48,6 @@ class AddUserFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
val binding = FragmentAddUserBinding.inflate(inflater, container, false)
|
val binding = FragmentAddUserBinding.inflate(inflater, container, false)
|
||||||
val navigation = Navigation.findNavController(container!!)
|
|
||||||
|
|
||||||
// user type
|
// user type
|
||||||
|
|
||||||
|
@ -121,7 +121,8 @@ class AddUserFragment : Fragment(), FragmentWithCustomTitle {
|
||||||
}
|
}
|
||||||
AddUserModelStatus.Done -> {
|
AddUserModelStatus.Done -> {
|
||||||
Snackbar.make(binding.root, R.string.add_user_confirmation_done, Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(binding.root, R.string.add_user_confirmation_done, Snackbar.LENGTH_SHORT).show()
|
||||||
navigation.popBackStack()
|
|
||||||
|
requireActivity().execute(UpdateStateCommand.AddUser.Leave)
|
||||||
|
|
||||||
binding.flipper.displayedChild = PAGE_WAIT
|
binding.flipper.displayedChild = PAGE_WAIT
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
TimeLimit Copyright <C> 2019 - 2020 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/>.
|
|
||||||
-->
|
|
||||||
<FrameLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:id="@+id/nav_host" />
|
|
|
@ -1,29 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
TimeLimit Copyright <C> 2019 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/>.
|
|
||||||
-->
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
tools:context=".ui.overview.overview.OverviewFragment">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:id="@+id/recycler"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
|
@ -1,182 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
TimeLimit Copyright <C> 2019 - 2020 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/>.
|
|
||||||
-->
|
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<data>
|
|
||||||
<variable
|
|
||||||
name="deviceTitle"
|
|
||||||
type="String" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="currentDeviceUserTitle"
|
|
||||||
type="String" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="hasManipulation"
|
|
||||||
type="Boolean" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="isCurrentDevice"
|
|
||||||
type="Boolean" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="isMissingRequiredPermission"
|
|
||||||
type="Boolean" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="didUninstall"
|
|
||||||
type="Boolean" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="isPasswordDisabled"
|
|
||||||
type="boolean" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="isConnected"
|
|
||||||
type="boolean" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="isUsingOlderVersion"
|
|
||||||
type="boolean" />
|
|
||||||
|
|
||||||
<import type="android.view.View" />
|
|
||||||
</data>
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<androidx.cardview.widget.CardView
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:id="@+id/card"
|
|
||||||
android:foreground="?selectableItemBackground"
|
|
||||||
app:cardUseCompatPadding="true"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<LinearLayout
|
|
||||||
android:padding="8dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
tools:ignore="UnusedAttribute"
|
|
||||||
android:drawableTint="?colorOnSurface"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:drawableStart="@drawable/ic_smartphone_black_24dp"
|
|
||||||
tools:text="Galaxy S8"
|
|
||||||
android:textAppearance="?android:textAppearanceLarge"
|
|
||||||
android:text="@{deviceTitle}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
tools:ignore="UnusedAttribute"
|
|
||||||
android:drawableTint="?colorOnSurface"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:drawableStart="@drawable/ic_account_circle_black_24dp"
|
|
||||||
android:visibility="@{currentDeviceUserTitle == null ? View.GONE : View.VISIBLE}"
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
tools:text="Max Mustermann"
|
|
||||||
android:text="@{currentDeviceUserTitle}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
tools:ignore="UnusedAttribute"
|
|
||||||
android:drawableTint="?colorOnSurface"
|
|
||||||
android:visibility="@{isPasswordDisabled ? View.VISIBLE : View.GONE}"
|
|
||||||
android:drawableStart="@drawable/ic_lock_open_black_24dp"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:text="@string/overview_device_item_password_disabled"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
tools:ignore="UnusedAttribute"
|
|
||||||
android:drawableTint="?colorOnSurface"
|
|
||||||
android:drawableStart="@drawable/ic_wifi_black_24dp"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:visibility="@{isConnected ? View.VISIBLE : View.GONE}"
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:text="@string/overview_device_item_connected"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
tools:ignore="UnusedAttribute"
|
|
||||||
android:drawableTint="?colorPrimary"
|
|
||||||
android:textColor="?colorPrimary"
|
|
||||||
android:drawableStart="@drawable/ic_update_black_24dp"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:visibility="@{isUsingOlderVersion ? View.VISIBLE : View.GONE}"
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:text="@string/overview_device_item_older_version"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
tools:ignore="UnusedAttribute"
|
|
||||||
android:drawableTint="@color/orange_text"
|
|
||||||
android:textColor="@color/orange_text"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:visibility="@{safeUnbox(hasManipulation) ? View.VISIBLE : View.GONE}"
|
|
||||||
android:drawableStart="@drawable/ic_warning_black_24dp"
|
|
||||||
android:text="@string/overview_device_item_manipulation"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
tools:ignore="UnusedAttribute"
|
|
||||||
android:drawableTint="@color/orange_text"
|
|
||||||
android:textColor="@color/orange_text"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:visibility="@{safeUnbox(isMissingRequiredPermission) ? View.VISIBLE : View.GONE}"
|
|
||||||
android:drawableStart="@drawable/ic_warning_black_24dp"
|
|
||||||
android:text="@string/overview_device_item_missing_permission"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
tools:ignore="UnusedAttribute"
|
|
||||||
android:drawableTint="@color/orange_text"
|
|
||||||
android:textColor="@color/orange_text"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:visibility="@{safeUnbox(didUninstall) ? View.VISIBLE : View.GONE}"
|
|
||||||
android:drawableStart="@drawable/ic_warning_black_24dp"
|
|
||||||
android:text="@string/overview_device_item_uninstall"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:visibility="@{safeUnbox(isCurrentDevice) ? View.VISIBLE : View.GONE}"
|
|
||||||
android:paddingStart="32dp"
|
|
||||||
android:paddingEnd="0dp"
|
|
||||||
android:textAppearance="?android:textAppearanceSmall"
|
|
||||||
android:text="@string/manage_device_is_this_device"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.cardview.widget.CardView>
|
|
||||||
</FrameLayout>
|
|
||||||
</layout>
|
|
|
@ -1,55 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
TimeLimit Copyright <C> 2019 - 2020 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/>.
|
|
||||||
-->
|
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<androidx.cardview.widget.CardView
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
app:cardUseCompatPadding="true"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<LinearLayout
|
|
||||||
android:padding="8dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:textAppearance="?android:textAppearanceLarge"
|
|
||||||
android:text="@string/overview_finish_setup_title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:text="@string/overview_finish_setup_text"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:layout_marginEnd="4dp"
|
|
||||||
android:id="@+id/btn_go"
|
|
||||||
android:layout_gravity="end"
|
|
||||||
android:text="@string/generic_go"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.cardview.widget.CardView>
|
|
||||||
</FrameLayout>
|
|
||||||
</layout>
|
|
|
@ -1,51 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
TimeLimit Copyright <C> 2019 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/>.
|
|
||||||
-->
|
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
<androidx.cardview.widget.CardView
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
app:cardUseCompatPadding="true"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<LinearLayout
|
|
||||||
android:padding="8dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:textAppearance="?android:textAppearanceLarge"
|
|
||||||
android:text="@string/overview_intro_title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:text="@string/overview_intro_text"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:textAppearance="?android:textAppearanceSmall"
|
|
||||||
android:text="@string/generic_swipe_to_dismiss"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.cardview.widget.CardView>
|
|
||||||
</FrameLayout>
|
|
|
@ -1,58 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
TimeLimit Copyright <C> 2019 - 2022 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/>.
|
|
||||||
-->
|
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<data>
|
|
||||||
<variable
|
|
||||||
name="title"
|
|
||||||
type="String" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="text"
|
|
||||||
type="String" />
|
|
||||||
</data>
|
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
|
||||||
android:layout_margin="8dp"
|
|
||||||
app:cardUseCompatPadding="true"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
tools:text="@string/overview_server_message"
|
|
||||||
android:text="@{title}"
|
|
||||||
android:textAppearance="?android:textAppearanceLarge"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
tools:text="That's a message"
|
|
||||||
android:text="@{text}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.cardview.widget.CardView>
|
|
||||||
|
|
||||||
</layout>
|
|
|
@ -1,134 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
TimeLimit Copyright <C> 2019 - 2020 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/>.
|
|
||||||
-->
|
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<data>
|
|
||||||
<variable
|
|
||||||
name="childName"
|
|
||||||
type="String" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="taskTitle"
|
|
||||||
type="String" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="categoryTitle"
|
|
||||||
type="String" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="duration"
|
|
||||||
type="String" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="lastGrant"
|
|
||||||
type="String" />
|
|
||||||
|
|
||||||
<import type="android.text.TextUtils" />
|
|
||||||
<import type="android.view.View" />
|
|
||||||
</data>
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<androidx.cardview.widget.CardView
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:foreground="?selectableItemBackground"
|
|
||||||
app:cardUseCompatPadding="true"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<LinearLayout
|
|
||||||
android:padding="8dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="@string/task_review_title"
|
|
||||||
android:textAppearance="?android:textAppearanceLarge"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
tools:text="@string/task_review_text"
|
|
||||||
android:text="@{@string/task_review_text(childName, taskTitle)}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:textAppearance="?android:textAppearanceSmall"
|
|
||||||
tools:text="@string/task_review_category"
|
|
||||||
android:text="@{@string/task_review_category(duration, categoryTitle)}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:visibility="@{TextUtils.isEmpty(lastGrant) ? View.GONE : View.VISIBLE}"
|
|
||||||
android:textAppearance="?android:textAppearanceSmall"
|
|
||||||
tools:text="@string/task_review_last_grant"
|
|
||||||
android:text="@{@string/task_review_last_grant(lastGrant)}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_marginEnd="4dp"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/skip_button"
|
|
||||||
style="?borderlessButtonStyle"
|
|
||||||
android:text="@string/generic_skip"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/no_button"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
style="?materialButtonOutlinedStyle"
|
|
||||||
android:text="@string/generic_no"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/yes_button"
|
|
||||||
style="?materialButtonOutlinedStyle"
|
|
||||||
android:text="@string/generic_yes"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:text="@string/purchase_required_info_local_mode_free"
|
|
||||||
android:textAppearance="?android:textAppearanceSmall"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.cardview.widget.CardView>
|
|
||||||
</FrameLayout>
|
|
||||||
</layout>
|
|
|
@ -1,114 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
TimeLimit Copyright <C> 2019 - 2020 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/>.
|
|
||||||
-->
|
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<data>
|
|
||||||
<variable
|
|
||||||
name="username"
|
|
||||||
type="String" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="isTemporarilyBlocked"
|
|
||||||
type="Boolean" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="areTimeLimitsDisabled"
|
|
||||||
type="Boolean" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="isParent"
|
|
||||||
type="Boolean" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="isChild"
|
|
||||||
type="Boolean" />
|
|
||||||
|
|
||||||
<import type="android.view.View" />
|
|
||||||
</data>
|
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
android:layout_marginRight="8dp"
|
|
||||||
android:id="@+id/card"
|
|
||||||
android:foreground="?selectableItemBackground"
|
|
||||||
app:cardUseCompatPadding="true"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<LinearLayout
|
|
||||||
android:padding="8dp"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
tools:ignore="UnusedAttribute"
|
|
||||||
android:drawableTint="?colorOnSurface"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:drawableStart="@drawable/ic_account_circle_black_24dp"
|
|
||||||
android:textAppearance="?android:textAppearanceLarge"
|
|
||||||
tools:text="Anton"
|
|
||||||
android:text="@{username}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
tools:ignore="UnusedAttribute"
|
|
||||||
android:drawableTint="?colorOnSurface"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:drawableStart="@drawable/ic_settings_black_24dp"
|
|
||||||
android:visibility="@{safeUnbox(isParent) ? View.VISIBLE : View.GONE}"
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:text="@string/overview_user_item_role_parent"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
tools:ignore="UnusedAttribute"
|
|
||||||
android:drawableTint="?colorOnSurface"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:drawableStart="@drawable/ic_security_black_24dp"
|
|
||||||
android:visibility="@{safeUnbox(isChild) ? View.VISIBLE : View.GONE}"
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:text="@string/overview_user_item_role_child"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
tools:ignore="UnusedAttribute"
|
|
||||||
android:drawableTint="?colorOnSurface"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:drawableStart="@drawable/ic_lock_outline_black_24dp"
|
|
||||||
android:visibility="@{safeUnbox(isTemporarilyBlocked) ? View.VISIBLE : View.GONE}"
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:text="@string/overview_user_item_temporarily_blocked"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
tools:ignore="UnusedAttribute"
|
|
||||||
android:drawableTint="?colorOnSurface"
|
|
||||||
android:drawablePadding="8dp"
|
|
||||||
android:drawableStart="@drawable/ic_alarm_off_black_24dp"
|
|
||||||
android:visibility="@{safeUnbox(areTimeLimitsDisabled) ? View.VISIBLE : View.GONE}"
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:text="@string/overview_user_item_temporarily_disabled"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.cardview.widget.CardView>
|
|
||||||
</layout>
|
|
|
@ -1,31 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
TimeLimit Copyright <C> 2019 - 2020 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/>.
|
|
||||||
-->
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
<item
|
|
||||||
app:showAsAction="never"
|
|
||||||
app:iconTint="?colorOnPrimarySurface"
|
|
||||||
android:icon="@drawable/ic_delete_black_24dp"
|
|
||||||
android:title="@string/main_tab_uninstall"
|
|
||||||
android:id="@+id/menu_main_uninstall" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
app:showAsAction="always"
|
|
||||||
app:iconTint="?colorOnPrimarySurface"
|
|
||||||
android:icon="@drawable/ic_info_outline_black_24dp"
|
|
||||||
android:title="@string/main_tab_about"
|
|
||||||
android:id="@+id/menu_main_about" />
|
|
||||||
</menu>
|
|
|
@ -1,30 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
TimeLimit Copyright <C> 2019 - 2020 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/>.
|
|
||||||
-->
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
<item
|
|
||||||
android:icon="@drawable/ic_time_white_24dp"
|
|
||||||
android:id="@+id/menu_manage_category_blocked_time_areas"
|
|
||||||
android:title="@string/blocked_time_areas"
|
|
||||||
app:iconTint="?colorOnPrimarySurface"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
<item
|
|
||||||
android:icon="@drawable/ic_settings_white_24dp"
|
|
||||||
android:id="@+id/menu_manage_category_settings"
|
|
||||||
android:title="@string/category_settings"
|
|
||||||
app:iconTint="?colorOnPrimarySurface"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
</menu>
|
|
|
@ -1,54 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
TimeLimit Copyright <C> 2019 - 2021 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/>.
|
|
||||||
-->
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<item
|
|
||||||
app:showAsAction="ifRoom"
|
|
||||||
app:iconTint="?colorOnPrimarySurface"
|
|
||||||
android:icon="@drawable/ic_baseline_directions_bike_24"
|
|
||||||
android:title="@string/manage_child_tasks"
|
|
||||||
android:id="@+id/menu_manage_child_tasks" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
app:showAsAction="ifRoom"
|
|
||||||
app:iconTint="?colorOnPrimarySurface"
|
|
||||||
android:icon="@drawable/ic_phone_black_24dp"
|
|
||||||
android:title="@string/contacts_title_long"
|
|
||||||
android:id="@+id/menu_manage_child_phone" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
app:showAsAction="never"
|
|
||||||
app:iconTint="?colorOnPrimarySurface"
|
|
||||||
android:icon="@drawable/ic_apps_white_24dp"
|
|
||||||
android:title="@string/child_apps_title"
|
|
||||||
android:id="@+id/menu_manage_child_apps" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
app:showAsAction="never"
|
|
||||||
app:iconTint="?colorOnPrimarySurface"
|
|
||||||
android:icon="@drawable/ic_history_black_24dp"
|
|
||||||
android:title="@string/usage_history_title"
|
|
||||||
android:id="@+id/menu_manage_child_usage_history" />
|
|
||||||
|
|
||||||
<item
|
|
||||||
app:showAsAction="never"
|
|
||||||
app:iconTint="?colorOnPrimarySurface"
|
|
||||||
android:icon="@drawable/ic_settings_white_24dp"
|
|
||||||
android:title="@string/manage_child_tab_other"
|
|
||||||
android:id="@+id/menu_manage_child_advanced" />
|
|
||||||
|
|
||||||
</menu>
|
|
|
@ -32,6 +32,8 @@
|
||||||
<string name="generic_show_details">Details anzeigen</string>
|
<string name="generic_show_details">Details anzeigen</string>
|
||||||
<string name="generic_accept">Akzeptieren</string>
|
<string name="generic_accept">Akzeptieren</string>
|
||||||
<string name="generic_reject">Ablehnen</string>
|
<string name="generic_reject">Ablehnen</string>
|
||||||
|
<string name="generic_menu">Menü</string>
|
||||||
|
<string name="generic_back">Zurück</string>
|
||||||
|
|
||||||
<string name="generic_swipe_to_dismiss">Sie können diesen Hinweis entfernen, indem Sie ihn zur Seite wischen</string>
|
<string name="generic_swipe_to_dismiss">Sie können diesen Hinweis entfernen, indem Sie ihn zur Seite wischen</string>
|
||||||
<string name="generic_runtime_permission_rejected">Berechtigung abgelehnt; Sie können die Berechtigungen in den Systemeinstellungen verwalten</string>
|
<string name="generic_runtime_permission_rejected">Berechtigung abgelehnt; Sie können die Berechtigungen in den Systemeinstellungen verwalten</string>
|
||||||
|
@ -126,6 +128,7 @@
|
||||||
</string>
|
</string>
|
||||||
<string name="annoy_unlock_dialog_action">Entsperren</string>
|
<string name="annoy_unlock_dialog_action">Entsperren</string>
|
||||||
|
|
||||||
|
<string name="authentication_action">Anmelden</string>
|
||||||
<string name="authentication_required_overlay_title">Melden Sie sich an, um Einstellungen zu ändern</string>
|
<string name="authentication_required_overlay_title">Melden Sie sich an, um Einstellungen zu ändern</string>
|
||||||
<string name="authentication_required_overlay_text">Die Einstellungen werden wieder gesperrt, wenn Sie TimeLimit verlassen</string>
|
<string name="authentication_required_overlay_text">Die Einstellungen werden wieder gesperrt, wenn Sie TimeLimit verlassen</string>
|
||||||
|
|
||||||
|
@ -1195,6 +1198,8 @@
|
||||||
\n\nBei den Benutzern können Sie die erlaubten Apps und die Zeitbeschränkungen wählen.
|
\n\nBei den Benutzern können Sie die erlaubten Apps und die Zeitbeschränkungen wählen.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
|
<string name="overview_device_item_name">Name</string>
|
||||||
|
<string name="overview_device_item_user_name">aktueller Benutzer</string>
|
||||||
<string name="overview_device_item_manipulation">An diesem Gerät wurden Manipulationen durchgeführt</string>
|
<string name="overview_device_item_manipulation">An diesem Gerät wurden Manipulationen durchgeführt</string>
|
||||||
<string name="overview_device_item_missing_permission">An diesem Gerät wurde eine erforderliche Berechtigung nicht gewährt</string>
|
<string name="overview_device_item_missing_permission">An diesem Gerät wurde eine erforderliche Berechtigung nicht gewährt</string>
|
||||||
<string name="overview_device_item_uninstall">TimeLimit wurde auf diesem Gerät deinstalliert</string>
|
<string name="overview_device_item_uninstall">TimeLimit wurde auf diesem Gerät deinstalliert</string>
|
||||||
|
@ -1202,10 +1207,12 @@
|
||||||
<string name="overview_device_item_connected">Verbunden</string>
|
<string name="overview_device_item_connected">Verbunden</string>
|
||||||
<string name="overview_device_item_older_version">verwendet eine ältere TimeLimit-Version</string>
|
<string name="overview_device_item_older_version">verwendet eine ältere TimeLimit-Version</string>
|
||||||
|
|
||||||
|
<string name="overview_user_item_name">Name</string>
|
||||||
<string name="overview_user_item_temporarily_blocked">vorübergehend gesperrt</string>
|
<string name="overview_user_item_temporarily_blocked">vorübergehend gesperrt</string>
|
||||||
<string name="overview_user_item_temporarily_blocked_until">gesperrt bis %s</string>
|
<string name="overview_user_item_temporarily_blocked_until">gesperrt bis %s</string>
|
||||||
<string name="overview_user_item_temporarily_disabled">Zeitbegrenzungen vorübergehend deaktiviert</string>
|
<string name="overview_user_item_temporarily_disabled">Zeitbegrenzungen vorübergehend deaktiviert</string>
|
||||||
<string name="overview_user_item_temporarily_disabled_until">Zeitbegrenzungen deaktiviert bis %s</string>
|
<string name="overview_user_item_temporarily_disabled_until">Zeitbegrenzungen deaktiviert bis %s</string>
|
||||||
|
<string name="overview_user_item_role">Rolle</string>
|
||||||
<string name="overview_user_item_role_child">wird eingeschränkt</string>
|
<string name="overview_user_item_role_child">wird eingeschränkt</string>
|
||||||
<string name="overview_user_item_role_parent">kann Einstellungen ändern</string>
|
<string name="overview_user_item_role_parent">kann Einstellungen ändern</string>
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,8 @@
|
||||||
<string name="generic_show_details">Show details</string>
|
<string name="generic_show_details">Show details</string>
|
||||||
<string name="generic_accept">Accept</string>
|
<string name="generic_accept">Accept</string>
|
||||||
<string name="generic_reject">Reject</string>
|
<string name="generic_reject">Reject</string>
|
||||||
|
<string name="generic_menu">Menu</string>
|
||||||
|
<string name="generic_back">Back</string>
|
||||||
|
|
||||||
<string name="generic_swipe_to_dismiss">Swipe to the side to remove this message</string>
|
<string name="generic_swipe_to_dismiss">Swipe to the side to remove this message</string>
|
||||||
<string name="generic_runtime_permission_rejected">Permission rejected; You can manage permissions in the system settings</string>
|
<string name="generic_runtime_permission_rejected">Permission rejected; You can manage permissions in the system settings</string>
|
||||||
|
@ -170,6 +172,7 @@
|
||||||
</string>
|
</string>
|
||||||
<string name="annoy_unlock_dialog_action">Unlock</string>
|
<string name="annoy_unlock_dialog_action">Unlock</string>
|
||||||
|
|
||||||
|
<string name="authentication_action">Authenticate</string>
|
||||||
<string name="authentication_required_overlay_title">Sign in to change settings</string>
|
<string name="authentication_required_overlay_title">Sign in to change settings</string>
|
||||||
<string name="authentication_required_overlay_text">The settings will be locked again when you leave TimeLimit</string>
|
<string name="authentication_required_overlay_text">The settings will be locked again when you leave TimeLimit</string>
|
||||||
|
|
||||||
|
@ -1239,6 +1242,8 @@
|
||||||
\n\nUnder users, you can configure the allowed Apps and time limits.
|
\n\nUnder users, you can configure the allowed Apps and time limits.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
|
<string name="overview_device_item_name">Name</string>
|
||||||
|
<string name="overview_device_item_user_name">current User</string>
|
||||||
<string name="overview_device_item_manipulation">There was a manipulation on this device</string>
|
<string name="overview_device_item_manipulation">There was a manipulation on this device</string>
|
||||||
<string name="overview_device_item_missing_permission">A required permission was not granted on this device</string>
|
<string name="overview_device_item_missing_permission">A required permission was not granted on this device</string>
|
||||||
<string name="overview_device_item_uninstall">TimeLimit was uninstalled at this device</string>
|
<string name="overview_device_item_uninstall">TimeLimit was uninstalled at this device</string>
|
||||||
|
@ -1246,10 +1251,12 @@
|
||||||
<string name="overview_device_item_connected">Connected</string>
|
<string name="overview_device_item_connected">Connected</string>
|
||||||
<string name="overview_device_item_older_version">uses an older TimeLimit version</string>
|
<string name="overview_device_item_older_version">uses an older TimeLimit version</string>
|
||||||
|
|
||||||
|
<string name="overview_user_item_name">Name</string>
|
||||||
<string name="overview_user_item_temporarily_blocked">temporarily blocked</string>
|
<string name="overview_user_item_temporarily_blocked">temporarily blocked</string>
|
||||||
<string name="overview_user_item_temporarily_blocked_until">temporarily blocked until %s</string>
|
<string name="overview_user_item_temporarily_blocked_until">temporarily blocked until %s</string>
|
||||||
<string name="overview_user_item_temporarily_disabled">Time limits temporarily disabled</string>
|
<string name="overview_user_item_temporarily_disabled">Time limits temporarily disabled</string>
|
||||||
<string name="overview_user_item_temporarily_disabled_until">Time limits temporarily disabled until %s</string>
|
<string name="overview_user_item_temporarily_disabled_until">Time limits temporarily disabled until %s</string>
|
||||||
|
<string name="overview_user_item_role">Role</string>
|
||||||
<string name="overview_user_item_role_child">is restricted</string>
|
<string name="overview_user_item_role_child">is restricted</string>
|
||||||
<string name="overview_user_item_role_parent">can change settings</string>
|
<string name="overview_user_item_role_parent">can change settings</string>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue