mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 01:39:22 +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'
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
|
@ -151,6 +152,10 @@ android {
|
|||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.0-alpha02"
|
||||
}
|
||||
}
|
||||
|
||||
wire {
|
||||
|
@ -170,6 +175,9 @@ dependencies {
|
|||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.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.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
|
||||
* 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.derived.ChildTaskWithCategoryTitle
|
||||
import io.timelimit.android.data.model.derived.FullChildTask
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
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")
|
||||
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>>
|
||||
|
||||
@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")
|
||||
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
|
||||
* 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.sync.network.ServerDhKey
|
||||
import io.timelimit.android.update.UpdateStatus
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.io.StringWriter
|
||||
|
||||
@Dao
|
||||
|
@ -59,6 +61,13 @@ abstract class ConfigDao {
|
|||
@Query("SELECT * FROM config WHERE id = :key")
|
||||
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? {
|
||||
return getRowCoroutine(key)?.value
|
||||
}
|
||||
|
@ -81,6 +90,10 @@ abstract class ConfigDao {
|
|||
return getValueOfKeyAsync(ConfigurationItemType.OwnDeviceId)
|
||||
}
|
||||
|
||||
fun getOwnDeviceIdFlow(): Flow<String?> {
|
||||
return getValueOfKeyFlow(ConfigurationItemType.OwnDeviceId)
|
||||
}
|
||||
|
||||
fun getOwnDeviceIdSync(): String? {
|
||||
return getValueOfKeySync(ConfigurationItemType.OwnDeviceId)
|
||||
}
|
||||
|
@ -213,6 +226,7 @@ abstract class ConfigDao {
|
|||
|
||||
fun setServerMessage(message: String?) = updateValueSync(ConfigurationItemType.ServerMessage, message ?: "")
|
||||
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 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
|
||||
* 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.ProtectionLevelConverter
|
||||
import io.timelimit.android.integration.platform.RuntimePermissionStatusConverter
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
@TypeConverters(
|
||||
NetworkTimeAdapter::class,
|
||||
ProtectionLevelConverter::class,
|
||||
RuntimePermissionStatusConverter::class,
|
||||
NewPermissionStatusConverter::class
|
||||
NewPermissionStatusConverter::class,
|
||||
UserTypeConverter::class
|
||||
)
|
||||
abstract class DeviceDao {
|
||||
@Query("SELECT * FROM device WHERE id = :deviceId")
|
||||
|
@ -45,6 +47,9 @@ abstract class DeviceDao {
|
|||
@Query("SELECT * FROM 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
|
||||
abstract fun addDeviceSync(device: Device)
|
||||
|
||||
|
@ -117,3 +122,12 @@ data class DeviceDetailDataBase(
|
|||
@ColumnInfo(name = "app_diff_version")
|
||||
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.Update
|
||||
import io.timelimit.android.data.model.User
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
abstract class UserDao {
|
||||
|
@ -45,6 +46,9 @@ abstract class UserDao {
|
|||
@Query("SELECT * FROM user ORDER by type DESC, name ASC")
|
||||
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")
|
||||
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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -25,6 +25,8 @@ data class FullChildTask(
|
|||
val childTask: ChildTask,
|
||||
@ColumnInfo(name = "category_title")
|
||||
val categoryTitle: String,
|
||||
@ColumnInfo(name = "child_id")
|
||||
val childId: String,
|
||||
@ColumnInfo(name = "child_name")
|
||||
val childName: String,
|
||||
@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
|
||||
* 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.os.Bundle
|
||||
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.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.FragmentManager
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
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 androidx.lifecycle.*
|
||||
import io.timelimit.android.Application
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.extensions.showSafe
|
||||
import io.timelimit.android.integration.platform.android.NotificationChannels
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.livedata.liveDataFromNullableValue
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.switchMap
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.u2f.U2fManager
|
||||
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.NewLoginFragment
|
||||
import io.timelimit.android.ui.main.ActivityViewModel
|
||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||
import io.timelimit.android.ui.main.AuthenticatedUser
|
||||
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.util.SyncStatusModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.security.SecureRandom
|
||||
|
||||
class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.DeviceFoundListener {
|
||||
class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.DeviceFoundListener, MainModelActivity {
|
||||
companion object {
|
||||
private const val LOG_TAG = "MainActivity"
|
||||
private const val AUTH_DIALOG_TAG = "adt"
|
||||
const val ACTION_USER_OPTIONS = "OPEN_USER_OPTIONS"
|
||||
const val EXTRA_USER_ID = "userId"
|
||||
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
|
||||
|
||||
|
@ -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 {
|
||||
ViewModelProviders.of(this).get(SyncStatusModel::class.java)
|
||||
}
|
||||
|
@ -103,114 +117,162 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
|
|||
override var ignoreStop: Boolean = false
|
||||
override val showPasswordRecovery: Boolean = true
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
supportActionBar!!.hide()
|
||||
|
||||
U2fManager.setupActivity(this)
|
||||
|
||||
NotificationChannels.createNotificationChannels(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager, this)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
NavHostFragment.create(R.navigation.nav_graph).let { navhost ->
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.nav_host, navhost)
|
||||
.setPrimaryNavigationFragment(navhost)
|
||||
.commitNow()
|
||||
}
|
||||
if (savedInstanceState != null) {
|
||||
mainModel.state.value = savedInstanceState.getSerializable(MAIN_MODEL_STATE) as State
|
||||
fragmentIds.addAll(savedInstanceState.getIntegerArrayList(FRAGMENT_IDS_STATE) ?: emptyList())
|
||||
}
|
||||
|
||||
// init the purchaseModel
|
||||
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
|
||||
DefaultAppLogic.with(this)
|
||||
|
||||
val fragmentContainer = supportFragmentManager.findFragmentById(R.id.nav_host)!!
|
||||
val fragmentContainerManager = fragmentContainer.childFragmentManager
|
||||
val fragments = MutableStateFlow(emptyMap<Int, Fragment>())
|
||||
|
||||
fragmentContainerManager.registerFragmentLifecycleCallbacks(object: FragmentManager.FragmentLifecycleCallbacks() {
|
||||
supportFragmentManager.registerFragmentLifecycleCallbacks(object: FragmentManager.FragmentLifecycleCallbacks() {
|
||||
override fun onFragmentStarted(fm: FragmentManager, f: Fragment) {
|
||||
super.onFragmentStarted(fm, f)
|
||||
|
||||
if (!(f is DialogFragment)) {
|
||||
currentNavigatorFragment.value = f
|
||||
fragments.update {
|
||||
it + Pair(f.id, f)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFragmentStopped(fm: FragmentManager, f: Fragment) {
|
||||
super.onFragmentStopped(fm, f)
|
||||
|
||||
if (currentNavigatorFragment.value === f) {
|
||||
currentNavigatorFragment.value = null
|
||||
fragments.update {
|
||||
it - f.id
|
||||
}
|
||||
|
||||
cleanupFragments()
|
||||
}
|
||||
}, false)
|
||||
|
||||
title.observe(this, Observer { setTitle(it) })
|
||||
syncModel.statusText.observe(this, Observer { supportActionBar!!.subtitle = it })
|
||||
|
||||
handleParameters(intent)
|
||||
|
||||
val hasDeviceId = getActivityViewModel().logic.deviceId.map { it != null }.ignoreUnchanged()
|
||||
val hasParentKey = getActivityViewModel().logic.database.config().getParentModeKeyLive().map { it != null }.ignoreUnchanged()
|
||||
|
||||
hasDeviceId.observe(this) {
|
||||
val rootDestination = getNavController().backQueue.getOrNull(1)?.destination?.id
|
||||
val rootDestination = mainModel.state.value.first()
|
||||
|
||||
if (!it) getActivityViewModel().logOut()
|
||||
|
||||
if (
|
||||
it && rootDestination != R.id.overviewFragment ||
|
||||
!it && rootDestination == R.id.overviewFragment
|
||||
it && rootDestination !is State.Overview ||
|
||||
!it && rootDestination is State.Overview
|
||||
) {
|
||||
restartContent()
|
||||
}
|
||||
}
|
||||
|
||||
hasParentKey.observe(this) {
|
||||
val rootDestination = getNavController().backQueue.getOrNull(1)?.destination?.id
|
||||
val rootDestination = mainModel.state.value.first()
|
||||
|
||||
if (
|
||||
it && rootDestination != R.id.parentModeFragment ||
|
||||
!it && rootDestination == R.id.parentModeFragment
|
||||
it && rootDestination !is State.ParentMode ||
|
||||
!it && rootDestination is State.ParentMode
|
||||
) {
|
||||
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)
|
||||
}
|
||||
|
||||
updateTransition(
|
||||
targetState = screenLive,
|
||||
label = "AnimatedContent"
|
||||
).AnimatedContent(
|
||||
modifier = Modifier.background(Color.Black),
|
||||
contentKey = { screen ->
|
||||
when (screen) {
|
||||
is Screen.FragmentScreen -> screen.fragment.containerId
|
||||
is Screen.OverviewScreen -> "overview"
|
||||
null -> null
|
||||
}
|
||||
},
|
||||
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 onOptionsItemSelected(item: MenuItem) = when {
|
||||
item.itemId == android.R.id.home -> {
|
||||
onBackPressed()
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
outState.putSerializable(MAIN_MODEL_STATE, mainModel.state.value)
|
||||
outState.putIntegerArrayList(FRAGMENT_IDS_STATE, ArrayList(fragmentIds))
|
||||
}
|
||||
|
||||
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}
|
||||
|
||||
if (userId != null && valid) {
|
||||
getNavController().popBackStack(R.id.overviewFragment, true)
|
||||
getNavController().handleDeepLink(
|
||||
getNavController().createDeepLink()
|
||||
.setDestination(R.id.manageParentFragment)
|
||||
.setArguments(ManageParentFragmentArgs(userId).toBundle())
|
||||
.createTaskStackBuilder()
|
||||
.intents
|
||||
.first()
|
||||
)
|
||||
|
||||
return true
|
||||
execute(UpdateStateCommand.RecoverPassword(userId))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,47 +329,53 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
|
|||
}
|
||||
|
||||
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() {
|
||||
while (getNavController().popBackStack()) {/* do nothing */}
|
||||
|
||||
getNavController().clearBackStack(R.id.launchFragment)
|
||||
getNavController().navigate(R.id.launchFragment)
|
||||
mainModel.execute(UpdateStateCommand.Reset)
|
||||
}
|
||||
|
||||
override fun getActivityViewModel(): ActivityViewModel {
|
||||
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() {
|
||||
if (supportFragmentManager.findFragmentByTag(AUTH_DIALOG_TAG) == null) {
|
||||
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() {
|
||||
super.onResume()
|
||||
|
||||
|
@ -331,4 +389,6 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
|
|||
}
|
||||
|
||||
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
|
||||
* 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.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.databinding.FragmentDiagnoseMainBinding
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
||||
import io.timelimit.android.livedata.liveDataFromNullableValue
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||
import io.timelimit.android.ui.main.AuthenticationFab
|
||||
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 {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val binding = FragmentDiagnoseMainBinding.inflate(inflater, container, false)
|
||||
val navigation = Navigation.findNavController(container!!)
|
||||
val logic = DefaultAppLogic.with(requireContext())
|
||||
val activity: ActivityViewModelHolder = activity as ActivityViewModelHolder
|
||||
val auth = activity.getActivityViewModel()
|
||||
|
||||
binding.diagnoseClockButton.setOnClickListener {
|
||||
navigation.safeNavigate(
|
||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseClockFragment(),
|
||||
R.id.diagnoseMainFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.Diagnose.Clock)
|
||||
}
|
||||
|
||||
binding.diagnoseConnectionButton.setOnClickListener {
|
||||
navigation.safeNavigate(
|
||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseConnectionFragment(),
|
||||
R.id.diagnoseMainFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.Diagnose.Connection)
|
||||
}
|
||||
|
||||
binding.diagnoseSyncButton.setOnClickListener {
|
||||
navigation.safeNavigate(
|
||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseSyncFragment(),
|
||||
R.id.diagnoseMainFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.Diagnose.Sync)
|
||||
}
|
||||
|
||||
binding.diagnoseCryButton.setOnClickListener {
|
||||
navigation.safeNavigate(
|
||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseCryptoFragment(),
|
||||
R.id.diagnoseMainFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.Diagnose.Crypto)
|
||||
}
|
||||
|
||||
binding.diagnoseBatteryButton.setOnClickListener {
|
||||
navigation.safeNavigate(
|
||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseBatteryFragment(),
|
||||
R.id.diagnoseMainFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.Diagnose.Battery)
|
||||
}
|
||||
|
||||
binding.diagnoseFgaButton.setOnClickListener {
|
||||
navigation.safeNavigate(
|
||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseForegroundAppFragment(),
|
||||
R.id.diagnoseMainFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.Diagnose.ForegroundApp)
|
||||
}
|
||||
|
||||
binding.diagnoseExfButton.setOnClickListener {
|
||||
navigation.safeNavigate(
|
||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseExperimentalFlagFragment(),
|
||||
R.id.diagnoseMainFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.Diagnose.ExperimentalFlags)
|
||||
}
|
||||
|
||||
logic.backgroundTaskLogic.lastLoopException.observe(this, Observer { ex ->
|
||||
|
@ -108,10 +86,7 @@ class DiagnoseMainFragment : Fragment(), FragmentWithCustomTitle {
|
|||
}
|
||||
|
||||
binding.diagnoseExitReasonsButton.setOnClickListener {
|
||||
navigation.safeNavigate(
|
||||
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseExitReasonFragment(),
|
||||
R.id.diagnoseMainFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.Diagnose.ExitReasons)
|
||||
}
|
||||
|
||||
AuthenticationFab.manageAuthenticationFab(
|
||||
|
|
|
@ -27,6 +27,8 @@ import io.timelimit.android.livedata.switchMap
|
|||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||
import io.timelimit.android.ui.manage.category.blocked_times.BlockedTimeAreasFragment
|
||||
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 val childId: String
|
||||
|
@ -44,7 +46,7 @@ abstract class CategoryFragmentWrapper: SingleFragmentWrapper(), FragmentWithCus
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
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.apps.ChildAppsFragment
|
||||
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 val childId: String
|
||||
|
@ -37,7 +39,7 @@ abstract class ChildFragmentWrapper: SingleFragmentWrapper() {
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
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
|
||||
* 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.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.databinding.SingleFragmentWrapperBinding
|
||||
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
||||
|
@ -30,14 +28,9 @@ import io.timelimit.android.ui.main.AuthenticationFab
|
|||
|
||||
abstract class SingleFragmentWrapper: Fragment() {
|
||||
val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||
private lateinit var navController: NavController
|
||||
protected lateinit var binding: SingleFragmentWrapperBinding
|
||||
|
||||
protected val navigation get() = navController
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
navController = Navigation.findNavController(container!!)
|
||||
|
||||
binding = SingleFragmentWrapperBinding.inflate(inflater, container, false)
|
||||
|
||||
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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -17,8 +17,8 @@
|
|||
package io.timelimit.android.ui.fragment
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||
import io.timelimit.android.ui.model.execute
|
||||
import io.timelimit.android.ui.overview.about.AboutFragment
|
||||
import io.timelimit.android.ui.overview.about.AboutFragmentParentHandlers
|
||||
|
||||
|
@ -27,23 +27,14 @@ class AboutFragmentWrapped: SingleFragmentWrapper(), AboutFragmentParentHandlers
|
|||
override fun createChildFragment(): Fragment = AboutFragment()
|
||||
|
||||
override fun onShowDiagnoseScreen() {
|
||||
navigation.safeNavigate(
|
||||
AboutFragmentWrappedDirections.actionAboutFragmentWrappedToDiagnoseMainFragment(),
|
||||
R.id.aboutFragmentWrapped
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.About.Diagnose)
|
||||
}
|
||||
|
||||
override fun onShowPurchaseScreen() {
|
||||
navigation.safeNavigate(
|
||||
AboutFragmentWrappedDirections.actionAboutFragmentWrappedToPurchaseFragment(),
|
||||
R.id.aboutFragmentWrapped
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.About.Purchase)
|
||||
}
|
||||
|
||||
override fun onShowStayAwesomeScreen() {
|
||||
navigation.safeNavigate(
|
||||
AboutFragmentWrappedDirections.actionAboutFragmentWrappedToStayAwesomeFragment(),
|
||||
R.id.aboutFragmentWrapped
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.About.StayAwesome)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -15,13 +15,7 @@
|
|||
*/
|
||||
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 io.timelimit.android.R
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
import io.timelimit.android.ui.fragment.CategoryFragmentWrapper
|
||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||
import io.timelimit.android.ui.manage.category.appsandrules.CombinedAppsAndRulesFragment
|
||||
|
@ -35,42 +29,4 @@ class ManageCategoryFragment : CategoryFragmentWrapper(), FragmentWithCustomTitl
|
|||
childId = childId,
|
||||
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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -16,14 +16,10 @@
|
|||
package io.timelimit.android.ui.manage.child
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.ui.fragment.ChildFragmentWrapper
|
||||
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? }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
* 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.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.data.model.Category
|
||||
import io.timelimit.android.data.model.HintsToShow
|
||||
import io.timelimit.android.databinding.RecyclerFragmentBinding
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
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.getActivityViewModel
|
||||
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.specialmode.SetCategorySpecialModeFragment
|
||||
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() {
|
||||
companion object {
|
||||
|
@ -69,17 +67,13 @@ class ManageChildCategoriesFragment : Fragment() {
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val adapter = Adapter()
|
||||
val navigation = Navigation.findNavController(view)
|
||||
|
||||
adapter.handlers = object: Handlers {
|
||||
override fun onCategoryClicked(category: Category) {
|
||||
navigation.safeNavigate(
|
||||
ManageChildFragmentDirections.actionManageChildFragmentToManageCategoryFragment(
|
||||
params.childId,
|
||||
category.id
|
||||
),
|
||||
R.id.manageChildFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.ManageChild.Category(
|
||||
childId = params.childId,
|
||||
categoryId = category.id
|
||||
))
|
||||
}
|
||||
|
||||
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
|
||||
* 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.ViewModelProviders
|
||||
import androidx.lifecycle.switchMap
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.crypto.Curve25519
|
||||
import io.timelimit.android.crypto.HexString
|
||||
import io.timelimit.android.data.model.Device
|
||||
import io.timelimit.android.databinding.FragmentManageDeviceBinding
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
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.manage.device.manage.feature.ManageDeviceFeaturesFragment
|
||||
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 {
|
||||
private val activity: ActivityViewModelHolder by lazy { getActivity() as ActivityViewModelHolder }
|
||||
|
@ -52,8 +52,7 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
logic.database.device().getDeviceById(args.deviceId)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val navigation = Navigation.findNavController(container!!)
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val binding = FragmentManageDeviceBinding.inflate(inflater, container, false)
|
||||
val userEntries = logic.database.user().getAllUsersLive()
|
||||
|
||||
|
@ -82,39 +81,19 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
|
||||
binding.handlers = object: ManageDeviceFragmentHandlers {
|
||||
override fun showUserScreen() {
|
||||
navigation.safeNavigate(
|
||||
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDeviceUserFragment(
|
||||
deviceId = args.deviceId
|
||||
),
|
||||
R.id.manageDeviceFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.ManageDevice.User(args.deviceId))
|
||||
}
|
||||
|
||||
override fun showPermissionsScreen() {
|
||||
navigation.safeNavigate(
|
||||
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDevicePermissionsFragment(
|
||||
deviceId = args.deviceId
|
||||
),
|
||||
R.id.manageDeviceFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.ManageDevice.Permissions(args.deviceId))
|
||||
}
|
||||
|
||||
override fun showFeaturesScreen() {
|
||||
navigation.safeNavigate(
|
||||
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDeviceFeaturesFragment(
|
||||
deviceId = args.deviceId
|
||||
),
|
||||
R.id.manageDeviceFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.ManageDevice.Features(args.deviceId))
|
||||
}
|
||||
|
||||
override fun showManageScreen() {
|
||||
navigation.safeNavigate(
|
||||
ManageDeviceFragmentDirections.actionManageDeviceFragmentToManageDeviceAdvancedFragment(
|
||||
deviceId = args.deviceId
|
||||
),
|
||||
R.id.manageDeviceFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.ManageDevice.Advanced(args.deviceId))
|
||||
}
|
||||
|
||||
override fun showAuthenticationScreen() {
|
||||
|
@ -126,7 +105,7 @@ class ManageDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
device ->
|
||||
|
||||
if (device == null) {
|
||||
navigation.popBackStack()
|
||||
requireActivity().execute(UpdateStateCommand.ManageDevice.Leave)
|
||||
} else {
|
||||
val now = RealTime.newInstance()
|
||||
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
|
||||
* 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.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.Device
|
||||
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.AuthenticationFab
|
||||
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 {
|
||||
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? {
|
||||
val binding = ManageDeviceAdvancedFragmentBinding.inflate(inflater, container, false)
|
||||
val navigation = Navigation.findNavController(container!!)
|
||||
val isThisDevice = logic.deviceId.map { ownDeviceId -> ownDeviceId == args.deviceId }.ignoreUnchanged()
|
||||
|
||||
val userEntry = deviceEntry.switchMap { device ->
|
||||
|
@ -102,7 +102,7 @@ class ManageDeviceAdvancedFragment : Fragment(), FragmentWithCustomTitle {
|
|||
|
||||
deviceEntry.observe(this, Observer { device ->
|
||||
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
|
||||
* 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.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.Device
|
||||
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.AuthenticationFab
|
||||
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 {
|
||||
companion object {
|
||||
|
@ -79,7 +80,6 @@ class ManageDeviceFeaturesFragment : Fragment(), FragmentWithCustomTitle {
|
|||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val navigation = Navigation.findNavController(container!!)
|
||||
val binding = ManageDeviceFeaturesFragmentBinding.inflate(inflater, container, false)
|
||||
|
||||
// auth
|
||||
|
@ -129,7 +129,7 @@ class ManageDeviceFeaturesFragment : Fragment(), FragmentWithCustomTitle {
|
|||
device ->
|
||||
|
||||
if (device == null) {
|
||||
navigation.popBackStack(R.id.overviewFragment, false)
|
||||
requireActivity().execute(UpdateStateCommand.ManageDevice.Leave)
|
||||
} else {
|
||||
val now = RealTime.newInstance()
|
||||
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
|
||||
* 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.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.Device
|
||||
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.AuthenticationFab
|
||||
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 {
|
||||
companion object {
|
||||
|
@ -84,7 +85,6 @@ class ManageDevicePermissionsFragment : Fragment(), FragmentWithCustomTitle {
|
|||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val navigation = Navigation.findNavController(container!!)
|
||||
val binding = ManageDevicePermissionsFragmentBinding.inflate(inflater, container, false)
|
||||
|
||||
// auth
|
||||
|
@ -180,7 +180,7 @@ class ManageDevicePermissionsFragment : Fragment(), FragmentWithCustomTitle {
|
|||
device ->
|
||||
|
||||
if (device == null) {
|
||||
navigation.popBackStack(R.id.overviewFragment, false)
|
||||
requireActivity().execute(UpdateStateCommand.ManageDevice.Leave)
|
||||
} else {
|
||||
binding.usageStatsAccess = device.currentUsageStatsPermission
|
||||
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
|
||||
* 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.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.Device
|
||||
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.FragmentWithCustomTitle
|
||||
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 {
|
||||
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? {
|
||||
val navigation = Navigation.findNavController(container!!)
|
||||
val binding = ManageDeviceUserFragmentBinding.inflate(inflater, container, false)
|
||||
val userEntries = logic.database.user().getAllUsersLive()
|
||||
|
||||
|
@ -137,7 +137,7 @@ class ManageDeviceUserFragment : Fragment(), FragmentWithCustomTitle {
|
|||
device ->
|
||||
|
||||
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
|
||||
* 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.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.User
|
||||
import io.timelimit.android.databinding.FragmentManageParentBinding
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
||||
import io.timelimit.android.livedata.map
|
||||
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.key.ManageUserKeyView
|
||||
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 {
|
||||
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? {
|
||||
val binding = FragmentManageParentBinding.inflate(inflater, container, false)
|
||||
val navigation = Navigation.findNavController(container!!)
|
||||
val model = ViewModelProviders.of(this).get(ManageParentModel::class.java)
|
||||
|
||||
AuthenticationFab.manageAuthenticationFab(
|
||||
|
@ -83,9 +82,7 @@ class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
|||
parentUser.observe(this, Observer {
|
||||
user ->
|
||||
|
||||
if (user == null) {
|
||||
navigation.popBackStack()
|
||||
}
|
||||
if (user == null) requireActivity().execute(UpdateStateCommand.ManageParent.Leave)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -140,45 +137,21 @@ class ManageParentFragment : Fragment(), FragmentWithCustomTitle {
|
|||
|
||||
binding.handlers = object: ManageParentFragmentHandlers {
|
||||
override fun onChangePasswordClicked() {
|
||||
navigation.safeNavigate(
|
||||
ManageParentFragmentDirections.
|
||||
actionManageParentFragmentToChangeParentPasswordFragment(
|
||||
params.parentId
|
||||
),
|
||||
R.id.manageParentFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.ManageParent.ChangePassword)
|
||||
}
|
||||
|
||||
override fun onRestorePasswordClicked() {
|
||||
navigation.safeNavigate(
|
||||
ManageParentFragmentDirections.
|
||||
actionManageParentFragmentToRestoreParentPasswordFragment(
|
||||
params.parentId
|
||||
),
|
||||
R.id.manageParentFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.ManageParent.RestorePassword)
|
||||
}
|
||||
|
||||
override fun onLinkMailClicked() {
|
||||
if (activity.getActivityViewModel().requestAuthenticationOrReturnTrue()) {
|
||||
navigation.safeNavigate(
|
||||
ManageParentFragmentDirections.
|
||||
actionManageParentFragmentToLinkParentMailFragment(
|
||||
params.parentId
|
||||
),
|
||||
R.id.manageParentFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.ManageParent.LinkMail)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onManageU2FClicked() {
|
||||
navigation.safeNavigate(
|
||||
ManageParentFragmentDirections.
|
||||
actionManageParentFragmentToManageParentU2FKeyFragment(
|
||||
params.parentId
|
||||
),
|
||||
R.id.manageParentFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.ManageParent.U2F)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
* 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.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.User
|
||||
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.AuthenticateByMailFragmentListener
|
||||
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 {
|
||||
companion object {
|
||||
|
@ -57,7 +58,6 @@ class LinkParentMailFragment : Fragment(), AuthenticateByMailFragmentListener, F
|
|||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = LinkParentMailFragmentBinding.inflate(inflater, container, false)
|
||||
val navigation = Navigation.findNavController(container!!)
|
||||
|
||||
model.status.observe(this, Observer {
|
||||
status ->
|
||||
|
@ -66,7 +66,7 @@ class LinkParentMailFragment : Fragment(), AuthenticateByMailFragmentListener, F
|
|||
LinkParentMailViewModelStatus.WaitForAuthentication -> binding.flipper.displayedChild = PAGE_LOGIN
|
||||
LinkParentMailViewModelStatus.WaitForConfirmationWithPassword -> binding.flipper.displayedChild = PAGE_READY
|
||||
LinkParentMailViewModelStatus.ShouldLeaveScreen -> {
|
||||
navigation.popBackStack()
|
||||
requireActivity().execute(UpdateStateCommand.ManageParent.LeaveLinkMail)
|
||||
|
||||
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
|
||||
* 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.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.navigation.Navigation
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.timelimit.android.R
|
||||
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.DefaultAppLogic
|
||||
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 {
|
||||
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? {
|
||||
val navigation = Navigation.findNavController(container!!)
|
||||
val binding = ChangeParentPasswordFragmentBinding.inflate(inflater, container, false)
|
||||
|
||||
parentUser.observe(this, Observer {
|
||||
parentUser ->
|
||||
|
||||
if (parentUser == null) {
|
||||
navigation.popBackStack(R.id.overviewFragment, false)
|
||||
requireActivity().execute(UpdateStateCommand.ManageParent.Leave)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -104,7 +104,7 @@ class ChangeParentPasswordFragment : Fragment(), FragmentWithCustomTitle {
|
|||
ChangeParentPasswordViewModelStatus.Done -> {
|
||||
Toast.makeText(context!!, R.string.manage_parent_change_password_toast_success, Toast.LENGTH_SHORT).show()
|
||||
|
||||
navigation.popBackStack()
|
||||
requireActivity().execute(UpdateStateCommand.ManageParent.LeaveChangePassword)
|
||||
|
||||
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
|
||||
* 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.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.User
|
||||
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.AuthenticateByMailFragmentListener
|
||||
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 {
|
||||
companion object {
|
||||
|
@ -56,7 +57,6 @@ class RestoreParentPasswordFragment : Fragment(), AuthenticateByMailFragmentList
|
|||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = RestoreParentPasswordFragmentBinding.inflate(inflater, container, false)
|
||||
val navigation = Navigation.findNavController(container!!)
|
||||
|
||||
model.status.observe(this, Observer {
|
||||
status ->
|
||||
|
@ -74,14 +74,14 @@ class RestoreParentPasswordFragment : Fragment(), AuthenticateByMailFragmentList
|
|||
RestoreParentPasswordStatus.NetworkError -> {
|
||||
Toast.makeText(context!!, R.string.error_network, Toast.LENGTH_SHORT).show()
|
||||
|
||||
navigation.popBackStack()
|
||||
requireActivity().execute(UpdateStateCommand.ManageParent.LeaveRestorePassword)
|
||||
|
||||
null
|
||||
}
|
||||
RestoreParentPasswordStatus.Done -> {
|
||||
Toast.makeText(context!!, R.string.manage_parent_change_password_toast_success, Toast.LENGTH_SHORT).show()
|
||||
|
||||
navigation.popBackStack()
|
||||
requireActivity().execute(UpdateStateCommand.ManageParent.LeaveRestorePassword)
|
||||
|
||||
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
|
||||
* 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.ViewGroup
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.databinding.ManageParentU2fKeyFragmentBinding
|
||||
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.remove.RemoveU2FKeyDialogFragment
|
||||
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 {
|
||||
val model: ManageParentU2FKeyModel by viewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val navigation = Navigation.findNavController(container!!)
|
||||
val params = ManageParentU2FKeyFragmentArgs.fromBundle(requireArguments())
|
||||
val binding = ManageParentU2fKeyFragmentBinding.inflate(inflater, container, false)
|
||||
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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
* 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
|
||||
* 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 androidx.navigation.NavDirections
|
||||
import android.app.Activity
|
||||
|
||||
fun NavController.safeNavigate(directions: NavDirections, currentScreen: Int) {
|
||||
if (this.currentDestination?.id == currentScreen) {
|
||||
navigate(directions)
|
||||
}
|
||||
interface MainModelActivity {
|
||||
fun execute(command: UpdateStateCommand)
|
||||
}
|
||||
|
||||
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
|
||||
* 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.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.databinding.FragmentSetupDevicePermissionsBinding
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
import io.timelimit.android.integration.platform.SystemPermission
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
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() {
|
||||
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? {
|
||||
val navigation = Navigation.findNavController(container!!)
|
||||
|
||||
binding = FragmentSetupDevicePermissionsBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.handlers = object: SetupDevicePermissionsHandlers {
|
||||
|
@ -71,11 +68,7 @@ class SetupDevicePermissionsFragment : Fragment() {
|
|||
}
|
||||
|
||||
override fun gotoNextStep() {
|
||||
navigation.safeNavigate(
|
||||
SetupDevicePermissionsFragmentDirections
|
||||
.actionSetupDevicePermissionsFragmentToSetupLocalModeFragment(),
|
||||
R.id.setupDevicePermissionsFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.Setup.LocalMode)
|
||||
}
|
||||
|
||||
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
|
||||
* 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.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.databinding.SetupHelpInfoFragmentBinding
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
import io.timelimit.android.ui.help.HelpDialogFragment
|
||||
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||
import io.timelimit.android.ui.model.execute
|
||||
|
||||
class SetupHelpInfoFragment: Fragment() {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
|
@ -41,10 +41,7 @@ class SetupHelpInfoFragment: Fragment() {
|
|||
}
|
||||
|
||||
binding.nextButton.setOnClickListener {
|
||||
Navigation.findNavController(view!!).safeNavigate(
|
||||
SetupHelpInfoFragmentDirections.actionSetupHelpInfoFragmentToSetupSelectModeFragment(),
|
||||
R.id.setupHelpInfoFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.Setup.SelectMode)
|
||||
}
|
||||
|
||||
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
|
||||
* 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.viewModels
|
||||
import androidx.lifecycle.*
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
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
|
||||
* 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.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.databinding.FragmentSetupSelectModeBinding
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
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.privacy.PrivacyInfoDialogFragment
|
||||
|
||||
|
@ -49,7 +48,6 @@ class SetupSelectModeFragment : Fragment() {
|
|||
private const val REQUEST_SETUP_PARENT_MODE = 3
|
||||
}
|
||||
|
||||
private lateinit var navigation: NavController
|
||||
private lateinit var binding: FragmentSetupSelectModeBinding
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
|
@ -61,13 +59,8 @@ class SetupSelectModeFragment : Fragment() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
navigation = Navigation.findNavController(view)
|
||||
|
||||
binding.btnLocalMode.setOnClickListener {
|
||||
navigation.safeNavigate(
|
||||
SetupSelectModeFragmentDirections.actionSetupSelectModeFragmentToSetupDevicePermissionsFragment(),
|
||||
R.id.setupSelectModeFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.Setup.DevicePermissions)
|
||||
}
|
||||
|
||||
binding.btnParentMode.setOnClickListener {
|
||||
|
@ -131,15 +124,9 @@ class SetupSelectModeFragment : Fragment() {
|
|||
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
if (requestCode == REQ_SETUP_CONNECTED_CHILD) {
|
||||
navigation.safeNavigate(
|
||||
SetupSelectModeFragmentDirections.actionSetupSelectModeFragmentToSetupRemoteChildFragment(),
|
||||
R.id.setupSelectModeFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.Setup.RemoteChild)
|
||||
} else if (requestCode == REQ_SETUP_CONNECTED_PARENT) {
|
||||
navigation.safeNavigate(
|
||||
SetupSelectModeFragmentDirections.actionSetupSelectModeFragmentToSetupParentModeFragment(),
|
||||
R.id.setupSelectModeFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.Setup.ParentMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
* 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 androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.databinding.FragmentSetupTermsBinding
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
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.setup.customserver.SelectCustomServerDialogFragment
|
||||
|
||||
|
@ -66,9 +66,6 @@ class SetupTermsFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun acceptTerms() {
|
||||
Navigation.findNavController(view!!).safeNavigate(
|
||||
SetupTermsFragmentDirections.actionSetupTermsFragmentToSetupHelpInfoFragment(),
|
||||
R.id.setupTermsFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.Setup.Help)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
* 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.Observer
|
||||
import androidx.lifecycle.observe
|
||||
import androidx.navigation.Navigation
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.coroutines.runAsync
|
||||
import io.timelimit.android.data.IdGenerator
|
||||
import io.timelimit.android.data.model.AppRecommendation
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.databinding.FragmentSetupDeviceBinding
|
||||
import io.timelimit.android.extensions.safeNavigate
|
||||
import io.timelimit.android.livedata.*
|
||||
import io.timelimit.android.logic.DefaultAppLogic
|
||||
import io.timelimit.android.ui.main.ActivityViewModelHolder
|
||||
import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||
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.overview.main.MainFragmentDirections
|
||||
import io.timelimit.android.ui.setup.SetupNetworkTimeVerification
|
||||
import io.timelimit.android.ui.update.UpdateConsentCard
|
||||
import io.timelimit.android.ui.view.NotifyPermissionCard
|
||||
|
@ -121,7 +120,6 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
val binding = FragmentSetupDeviceBinding.inflate(inflater, container, false)
|
||||
val logic = DefaultAppLogic.with(requireContext())
|
||||
val activity = activity as ActivityViewModelHolder
|
||||
val navigation = Navigation.findNavController(container!!)
|
||||
|
||||
binding.needsParent.authBtn.setOnClickListener {
|
||||
activity.showAuthenticationScreen()
|
||||
|
@ -147,13 +145,7 @@ class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
|
|||
|
||||
val ownDeviceId = logic.deviceId.waitForNullableValue()!!
|
||||
|
||||
navigation.popBackStack()
|
||||
navigation.safeNavigate(
|
||||
MainFragmentDirections.actionOverviewFragmentToManageDeviceFragment(
|
||||
ownDeviceId
|
||||
),
|
||||
R.id.overviewFragment
|
||||
)
|
||||
requireActivity().execute(UpdateStateCommand.ManageDevice.EnterFromDeviceSetup(ownDeviceId))
|
||||
}
|
||||
}
|
||||
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
|
||||
* 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.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.navigation.Navigation
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.timelimit.android.R
|
||||
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.ActivityViewModelHolder
|
||||
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 {
|
||||
companion object {
|
||||
|
@ -47,7 +48,6 @@ class AddUserFragment : Fragment(), FragmentWithCustomTitle {
|
|||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val binding = FragmentAddUserBinding.inflate(inflater, container, false)
|
||||
val navigation = Navigation.findNavController(container!!)
|
||||
|
||||
// user type
|
||||
|
||||
|
@ -121,7 +121,8 @@ class AddUserFragment : Fragment(), FragmentWithCustomTitle {
|
|||
}
|
||||
AddUserModelStatus.Done -> {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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_accept">Akzeptieren</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_runtime_permission_rejected">Berechtigung abgelehnt; Sie können die Berechtigungen in den Systemeinstellungen verwalten</string>
|
||||
|
@ -126,6 +128,7 @@
|
|||
</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_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.
|
||||
</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_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>
|
||||
|
@ -1202,10 +1207,12 @@
|
|||
<string name="overview_device_item_connected">Verbunden</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_until">gesperrt bis %s</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_role">Rolle</string>
|
||||
<string name="overview_user_item_role_child">wird eingeschränkt</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_accept">Accept</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_runtime_permission_rejected">Permission rejected; You can manage permissions in the system settings</string>
|
||||
|
@ -170,6 +172,7 @@
|
|||
</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_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.
|
||||
</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_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>
|
||||
|
@ -1246,10 +1251,12 @@
|
|||
<string name="overview_device_item_connected">Connected</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_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_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_parent">can change settings</string>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue