mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 17:59:51 +02:00
Add task system
This commit is contained in:
parent
0862f57c59
commit
e8fab30a4a
60 changed files with 3706 additions and 427 deletions
1190
app/schemas/io.timelimit.android.data.RoomDatabase/34.json
Normal file
1190
app/schemas/io.timelimit.android.data.RoomDatabase/34.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -39,6 +39,7 @@ interface Database {
|
||||||
fun derivedDataDao(): DerivedDataDao
|
fun derivedDataDao(): DerivedDataDao
|
||||||
fun userLimitLoginCategoryDao(): UserLimitLoginCategoryDao
|
fun userLimitLoginCategoryDao(): UserLimitLoginCategoryDao
|
||||||
fun categoryNetworkId(): CategoryNetworkIdDao
|
fun categoryNetworkId(): CategoryNetworkIdDao
|
||||||
|
fun childTasks(): ChildTaskDao
|
||||||
|
|
||||||
fun <T> runInTransaction(block: () -> T): T
|
fun <T> runInTransaction(block: () -> T): T
|
||||||
fun <T> runInUnobservedTransaction(block: () -> T): T
|
fun <T> runInUnobservedTransaction(block: () -> T): T
|
||||||
|
|
|
@ -243,4 +243,11 @@ object DatabaseMigrations {
|
||||||
database.execSQL("ALTER TABLE `category` ADD COLUMN `disable_limits_until` INTEGER NOT NULL DEFAULT 0")
|
database.execSQL("ALTER TABLE `category` ADD COLUMN `disable_limits_until` INTEGER NOT NULL DEFAULT 0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MIGRATE_TO_V34 = object: Migration(33, 34) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `child_task` (`task_id` TEXT NOT NULL, `category_id` TEXT NOT NULL, `task_title` TEXT NOT NULL, `extra_time_duration` INTEGER NOT NULL, `pending_request` INTEGER NOT NULL, `last_grant_timestamp` INTEGER NOT NULL, PRIMARY KEY(`task_id`), FOREIGN KEY(`category_id`) REFERENCES `category`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )")
|
||||||
|
database.execSQL("ALTER TABLE `category` ADD COLUMN `tasks_version` TEXT NOT NULL DEFAULT ''")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,8 +48,9 @@ import java.util.concurrent.TimeUnit
|
||||||
UserKey::class,
|
UserKey::class,
|
||||||
SessionDuration::class,
|
SessionDuration::class,
|
||||||
UserLimitLoginCategory::class,
|
UserLimitLoginCategory::class,
|
||||||
CategoryNetworkId::class
|
CategoryNetworkId::class,
|
||||||
], version = 33)
|
ChildTask::class
|
||||||
|
], version = 34)
|
||||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||||
companion object {
|
companion object {
|
||||||
private val lock = Object()
|
private val lock = Object()
|
||||||
|
@ -116,7 +117,8 @@ abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database
|
||||||
DatabaseMigrations.MIGRATE_TO_V30,
|
DatabaseMigrations.MIGRATE_TO_V30,
|
||||||
DatabaseMigrations.MIGRATE_TO_V31,
|
DatabaseMigrations.MIGRATE_TO_V31,
|
||||||
DatabaseMigrations.MIGRATE_TO_V32,
|
DatabaseMigrations.MIGRATE_TO_V32,
|
||||||
DatabaseMigrations.MIGRATE_TO_V33
|
DatabaseMigrations.MIGRATE_TO_V33,
|
||||||
|
DatabaseMigrations.MIGRATE_TO_V34
|
||||||
)
|
)
|
||||||
.setQueryExecutor(Threads.database)
|
.setQueryExecutor(Threads.database)
|
||||||
.build()
|
.build()
|
||||||
|
|
|
@ -44,6 +44,7 @@ object DatabaseBackupLowlevel {
|
||||||
private const val SESSION_DURATION = "sessionDuration"
|
private const val SESSION_DURATION = "sessionDuration"
|
||||||
private const val USER_LIMIT_LOGIN_CATEGORY = "userLimitLoginCategory"
|
private const val USER_LIMIT_LOGIN_CATEGORY = "userLimitLoginCategory"
|
||||||
private const val CATEGORY_NETWORK_ID = "categoryNetworkId"
|
private const val CATEGORY_NETWORK_ID = "categoryNetworkId"
|
||||||
|
private const val CHILD_TASK = "childTask"
|
||||||
|
|
||||||
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
||||||
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
val writer = JsonWriter(OutputStreamWriter(outputStream, Charsets.UTF_8))
|
||||||
|
@ -92,6 +93,7 @@ object DatabaseBackupLowlevel {
|
||||||
handleCollection(SESSION_DURATION) { offset, pageSize -> database.sessionDuration().getSessionDurationPageSync(offset, pageSize) }
|
handleCollection(SESSION_DURATION) { offset, pageSize -> database.sessionDuration().getSessionDurationPageSync(offset, pageSize) }
|
||||||
handleCollection(USER_LIMIT_LOGIN_CATEGORY) { offset, pageSize -> database.userLimitLoginCategoryDao().getAllowedContactPageSync(offset, pageSize) }
|
handleCollection(USER_LIMIT_LOGIN_CATEGORY) { offset, pageSize -> database.userLimitLoginCategoryDao().getAllowedContactPageSync(offset, pageSize) }
|
||||||
handleCollection(CATEGORY_NETWORK_ID) { offset, pageSize -> database.categoryNetworkId().getPageSync(offset, pageSize) }
|
handleCollection(CATEGORY_NETWORK_ID) { offset, pageSize -> database.categoryNetworkId().getPageSync(offset, pageSize) }
|
||||||
|
handleCollection(CHILD_TASK) { offset, pageSize -> database.childTasks().getPageSync(offset, pageSize) }
|
||||||
|
|
||||||
writer.endObject().flush()
|
writer.endObject().flush()
|
||||||
}
|
}
|
||||||
|
@ -101,6 +103,7 @@ object DatabaseBackupLowlevel {
|
||||||
|
|
||||||
var userLoginLimitCategories = emptyList<UserLimitLoginCategory>()
|
var userLoginLimitCategories = emptyList<UserLimitLoginCategory>()
|
||||||
var categoryNetworkId = emptyList<CategoryNetworkId>()
|
var categoryNetworkId = emptyList<CategoryNetworkId>()
|
||||||
|
var childTasks = emptyList<ChildTask>()
|
||||||
|
|
||||||
database.runInTransaction {
|
database.runInTransaction {
|
||||||
database.deleteAllData()
|
database.deleteAllData()
|
||||||
|
@ -267,6 +270,19 @@ object DatabaseBackupLowlevel {
|
||||||
|
|
||||||
reader.endArray()
|
reader.endArray()
|
||||||
}
|
}
|
||||||
|
CHILD_TASK -> {
|
||||||
|
reader.beginArray()
|
||||||
|
|
||||||
|
mutableListOf<ChildTask>().let { list ->
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
list.add(ChildTask.parse(reader))
|
||||||
|
}
|
||||||
|
|
||||||
|
childTasks = list
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.endArray()
|
||||||
|
}
|
||||||
else -> reader.skipValue()
|
else -> reader.skipValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -274,6 +290,7 @@ object DatabaseBackupLowlevel {
|
||||||
|
|
||||||
if (userLoginLimitCategories.isNotEmpty()) { database.userLimitLoginCategoryDao().addItemsSync(userLoginLimitCategories) }
|
if (userLoginLimitCategories.isNotEmpty()) { database.userLimitLoginCategoryDao().addItemsSync(userLoginLimitCategories) }
|
||||||
if (categoryNetworkId.isNotEmpty()) { database.categoryNetworkId().insertItemsSync(categoryNetworkId) }
|
if (categoryNetworkId.isNotEmpty()) { database.categoryNetworkId().insertItemsSync(categoryNetworkId) }
|
||||||
|
if (childTasks.isNotEmpty()) { database.childTasks().insertItemsSync(childTasks) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -65,7 +65,7 @@ abstract class CategoryDao {
|
||||||
@Query("UPDATE category SET temporarily_blocked = :blocked, temporarily_blocked_end_time = :endTime WHERE id = :categoryId")
|
@Query("UPDATE category SET temporarily_blocked = :blocked, temporarily_blocked_end_time = :endTime WHERE id = :categoryId")
|
||||||
abstract fun updateCategoryTemporarilyBlocked(categoryId: String, blocked: Boolean, endTime: Long)
|
abstract fun updateCategoryTemporarilyBlocked(categoryId: String, blocked: Boolean, endTime: Long)
|
||||||
|
|
||||||
@Query("SELECT id, base_version, apps_version, rules_version, usedtimes_version FROM category")
|
@Query("SELECT id, base_version, apps_version, rules_version, usedtimes_version, tasks_version FROM category")
|
||||||
abstract fun getCategoriesWithVersionNumbers(): LiveData<List<CategoryWithVersionNumbers>>
|
abstract fun getCategoriesWithVersionNumbers(): LiveData<List<CategoryWithVersionNumbers>>
|
||||||
|
|
||||||
@Query("UPDATE category SET apps_version = :assignedAppsVersion WHERE id = :categoryId")
|
@Query("UPDATE category SET apps_version = :assignedAppsVersion WHERE id = :categoryId")
|
||||||
|
@ -77,10 +77,13 @@ abstract class CategoryDao {
|
||||||
@Query("UPDATE category SET usedtimes_version = :usedTimesVersion WHERE id = :categoryId")
|
@Query("UPDATE category SET usedtimes_version = :usedTimesVersion WHERE id = :categoryId")
|
||||||
abstract fun updateCategoryUsedTimesVersion(categoryId: String, usedTimesVersion: String)
|
abstract fun updateCategoryUsedTimesVersion(categoryId: String, usedTimesVersion: String)
|
||||||
|
|
||||||
|
@Query("UPDATE category SET tasks_version = :tasksVersion WHERE id = :categoryId")
|
||||||
|
abstract fun updateCategoryTasksVersion(categoryId: String, tasksVersion: String)
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
abstract fun updateCategorySync(category: Category)
|
abstract fun updateCategorySync(category: Category)
|
||||||
|
|
||||||
@Query("UPDATE category SET apps_version = \"\", rules_version = \"\", usedtimes_version = \"\", base_version = \"\"")
|
@Query("UPDATE category SET apps_version = '', rules_version = '', usedtimes_version = '', base_version = '', tasks_version = ''")
|
||||||
abstract fun deleteAllCategoriesVersionNumbers()
|
abstract fun deleteAllCategoriesVersionNumbers()
|
||||||
|
|
||||||
@Query("SELECT * FROM category LIMIT :pageSize OFFSET :offset")
|
@Query("SELECT * FROM category LIMIT :pageSize OFFSET :offset")
|
||||||
|
@ -114,7 +117,9 @@ data class CategoryWithVersionNumbers(
|
||||||
@ColumnInfo(name = "rules_version")
|
@ColumnInfo(name = "rules_version")
|
||||||
val timeLimitRulesVersion: String,
|
val timeLimitRulesVersion: String,
|
||||||
@ColumnInfo(name = "usedtimes_version")
|
@ColumnInfo(name = "usedtimes_version")
|
||||||
val usedTimeItemsVersion: String
|
val usedTimeItemsVersion: String,
|
||||||
|
@ColumnInfo(name = "tasks_version")
|
||||||
|
val taskListVersion: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class CategoryShortInfo(
|
data class CategoryShortInfo(
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.data.dao
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
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
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface ChildTaskDao {
|
||||||
|
@Query("SELECT * FROM child_task LIMIT :pageSize OFFSET :offset")
|
||||||
|
fun getPageSync(offset: Int, pageSize: Int): List<ChildTask>
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun insertItemsSync(items: List<ChildTask>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun forceInsertItemsSync(items: List<ChildTask>)
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun insertItemSync(item: ChildTask)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
fun updateItemSync(item: ChildTask)
|
||||||
|
|
||||||
|
@Query("SELECT child_task.*, category.title as category_title FROM child_task JOIN category ON (child_task.category_id = category.id) WHERE category.child_id = :userId")
|
||||||
|
fun getTasksByUserIdWithCategoryTitlesLive(userId: String): LiveData<List<ChildTaskWithCategoryTitle>>
|
||||||
|
|
||||||
|
@Query("SELECT child_task.*, category.title as category_title, user.name as child_name 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 * FROM child_task WHERE category_id = :categoryId")
|
||||||
|
fun getTasksByCategoryId(categoryId: String): LiveData<List<ChildTask>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM child_task WHERE task_id = :taskId")
|
||||||
|
fun getTaskByTaskId(taskId: String): ChildTask?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM child_task WHERE task_id = :taskId")
|
||||||
|
fun getTaskByTaskIdLive(taskId: String): LiveData<ChildTask?>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM child_task WHERE task_id = :taskId")
|
||||||
|
suspend fun getTaskByTaskIdCoroutine(taskId: String): ChildTask?
|
||||||
|
|
||||||
|
@Query("DELETE FROM child_task WHERE task_id = :taskId")
|
||||||
|
fun removeTaskById(taskId: String)
|
||||||
|
|
||||||
|
@Query("DELETE FROM child_task WHERE category_id = :categoryId")
|
||||||
|
fun removeTasksByCategoryId(categoryId: String)
|
||||||
|
}
|
|
@ -56,6 +56,8 @@ data class Category(
|
||||||
val timeLimitRulesVersion: String,
|
val timeLimitRulesVersion: String,
|
||||||
@ColumnInfo(name = "usedtimes_version")
|
@ColumnInfo(name = "usedtimes_version")
|
||||||
val usedTimesVersion: String,
|
val usedTimesVersion: String,
|
||||||
|
@ColumnInfo(name = "tasks_version", defaultValue = "")
|
||||||
|
val tasksVersion: String,
|
||||||
@ColumnInfo(name = "parent_category_id")
|
@ColumnInfo(name = "parent_category_id")
|
||||||
val parentCategoryId: String,
|
val parentCategoryId: String,
|
||||||
@ColumnInfo(name = "block_all_notifications")
|
@ColumnInfo(name = "block_all_notifications")
|
||||||
|
@ -95,6 +97,7 @@ data class Category(
|
||||||
private const val SORT = "sort"
|
private const val SORT = "sort"
|
||||||
private const val EXTRA_TIME_DAY = "extraTimeDay"
|
private const val EXTRA_TIME_DAY = "extraTimeDay"
|
||||||
private const val DISABLE_LIMIITS_UNTIL = "dlu"
|
private const val DISABLE_LIMIITS_UNTIL = "dlu"
|
||||||
|
private const val TASKS_VERSION = "tv"
|
||||||
|
|
||||||
fun parse(reader: JsonReader): Category {
|
fun parse(reader: JsonReader): Category {
|
||||||
var id: String? = null
|
var id: String? = null
|
||||||
|
@ -117,6 +120,7 @@ data class Category(
|
||||||
var sort = 0
|
var sort = 0
|
||||||
var extraTimeDay = -1
|
var extraTimeDay = -1
|
||||||
var disableLimitsUntil = 0L
|
var disableLimitsUntil = 0L
|
||||||
|
var tasksVersion = ""
|
||||||
|
|
||||||
reader.beginObject()
|
reader.beginObject()
|
||||||
|
|
||||||
|
@ -141,6 +145,7 @@ data class Category(
|
||||||
SORT -> sort = reader.nextInt()
|
SORT -> sort = reader.nextInt()
|
||||||
EXTRA_TIME_DAY -> extraTimeDay = reader.nextInt()
|
EXTRA_TIME_DAY -> extraTimeDay = reader.nextInt()
|
||||||
DISABLE_LIMIITS_UNTIL -> disableLimitsUntil = reader.nextLong()
|
DISABLE_LIMIITS_UNTIL -> disableLimitsUntil = reader.nextLong()
|
||||||
|
TASKS_VERSION -> tasksVersion = reader.nextString()
|
||||||
else -> reader.skipValue()
|
else -> reader.skipValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,6 +164,7 @@ data class Category(
|
||||||
assignedAppsVersion = assignedAppsVersion!!,
|
assignedAppsVersion = assignedAppsVersion!!,
|
||||||
timeLimitRulesVersion = timeLimitRulesVersion!!,
|
timeLimitRulesVersion = timeLimitRulesVersion!!,
|
||||||
usedTimesVersion = usedTimesVersion!!,
|
usedTimesVersion = usedTimesVersion!!,
|
||||||
|
tasksVersion = tasksVersion,
|
||||||
parentCategoryId = parentCategoryId,
|
parentCategoryId = parentCategoryId,
|
||||||
blockAllNotifications = blockAllNotifications,
|
blockAllNotifications = blockAllNotifications,
|
||||||
timeWarnings = timeWarnings,
|
timeWarnings = timeWarnings,
|
||||||
|
@ -214,6 +220,7 @@ data class Category(
|
||||||
writer.name(ASSIGNED_APPS_VERSION).value(assignedAppsVersion)
|
writer.name(ASSIGNED_APPS_VERSION).value(assignedAppsVersion)
|
||||||
writer.name(RULES_VERSION).value(timeLimitRulesVersion)
|
writer.name(RULES_VERSION).value(timeLimitRulesVersion)
|
||||||
writer.name(USED_TIMES_VERSION).value(usedTimesVersion)
|
writer.name(USED_TIMES_VERSION).value(usedTimesVersion)
|
||||||
|
writer.name(TASKS_VERSION).value(tasksVersion)
|
||||||
writer.name(PARENT_CATEGORY_ID).value(parentCategoryId)
|
writer.name(PARENT_CATEGORY_ID).value(parentCategoryId)
|
||||||
writer.name(BlOCK_ALL_NOTIFICATIONS).value(blockAllNotifications)
|
writer.name(BlOCK_ALL_NOTIFICATIONS).value(blockAllNotifications)
|
||||||
writer.name(TIME_WARNINGS).value(timeWarnings)
|
writer.name(TIME_WARNINGS).value(timeWarnings)
|
||||||
|
|
121
app/src/main/java/io/timelimit/android/data/model/ChildTask.kt
Normal file
121
app/src/main/java/io/timelimit/android/data/model/ChildTask.kt
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.data.model
|
||||||
|
|
||||||
|
import android.util.JsonReader
|
||||||
|
import android.util.JsonWriter
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.JsonSerializable
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = "child_task",
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = Category::class,
|
||||||
|
childColumns = ["category_id"],
|
||||||
|
parentColumns = ["id"],
|
||||||
|
onUpdate = ForeignKey.CASCADE,
|
||||||
|
onDelete = ForeignKey.CASCADE
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class ChildTask(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "task_id")
|
||||||
|
val taskId: String,
|
||||||
|
@ColumnInfo(name = "category_id")
|
||||||
|
val categoryId: String,
|
||||||
|
@ColumnInfo(name = "task_title")
|
||||||
|
val taskTitle: String,
|
||||||
|
@ColumnInfo(name = "extra_time_duration")
|
||||||
|
val extraTimeDuration: Int,
|
||||||
|
@ColumnInfo(name = "pending_request")
|
||||||
|
val pendingRequest: Boolean,
|
||||||
|
// 0 = not yet granted
|
||||||
|
@ColumnInfo(name = "last_grant_timestamp")
|
||||||
|
val lastGrantTimestamp: Long
|
||||||
|
): JsonSerializable {
|
||||||
|
companion object {
|
||||||
|
private const val TASK_ID = "taskId"
|
||||||
|
private const val CATEGORY_ID = "categoryId"
|
||||||
|
private const val TASK_TITLE = "taskTitle"
|
||||||
|
private const val EXTRA_TIME_DURATION = "extraTimeDuration"
|
||||||
|
private const val PENDING_REQUEST = "pendingRequest"
|
||||||
|
private const val LAST_GRANT_TIMESTAMP = "lastGrantTimestamp"
|
||||||
|
|
||||||
|
const val MAX_EXTRA_TIME = 1000 * 60 * 60 * 24
|
||||||
|
const val MAX_TASK_TITLE_LENGTH = 50
|
||||||
|
|
||||||
|
fun parse(reader: JsonReader): ChildTask {
|
||||||
|
var taskId: String? = null
|
||||||
|
var categoryId: String? = null
|
||||||
|
var taskTitle: String? = null
|
||||||
|
var extraTimeDuration: Int? = null
|
||||||
|
var pendingRequest: Boolean? = null
|
||||||
|
var lastGrantTimestamp: Long? = null
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when (reader.nextName()) {
|
||||||
|
TASK_ID -> taskId = reader.nextString()
|
||||||
|
CATEGORY_ID -> categoryId = reader.nextString()
|
||||||
|
TASK_TITLE -> taskTitle = reader.nextString()
|
||||||
|
EXTRA_TIME_DURATION -> extraTimeDuration = reader.nextInt()
|
||||||
|
PENDING_REQUEST -> pendingRequest = reader.nextBoolean()
|
||||||
|
LAST_GRANT_TIMESTAMP -> lastGrantTimestamp = reader.nextLong()
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
return ChildTask(
|
||||||
|
taskId = taskId!!,
|
||||||
|
categoryId = categoryId!!,
|
||||||
|
taskTitle = taskTitle!!,
|
||||||
|
extraTimeDuration = extraTimeDuration!!,
|
||||||
|
pendingRequest = pendingRequest!!,
|
||||||
|
lastGrantTimestamp = lastGrantTimestamp!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(taskId)
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
|
||||||
|
if (taskTitle.isEmpty() || taskTitle.length > MAX_TASK_TITLE_LENGTH) throw IllegalArgumentException()
|
||||||
|
if (extraTimeDuration <= 0 || extraTimeDuration > MAX_EXTRA_TIME) throw IllegalArgumentException()
|
||||||
|
if (lastGrantTimestamp < 0) throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(TASK_ID).value(taskId)
|
||||||
|
writer.name(CATEGORY_ID).value(categoryId)
|
||||||
|
writer.name(TASK_TITLE).value(taskTitle)
|
||||||
|
writer.name(EXTRA_TIME_DURATION).value(extraTimeDuration)
|
||||||
|
writer.name(PENDING_REQUEST).value(pendingRequest)
|
||||||
|
writer.name(LAST_GRANT_TIMESTAMP).value(lastGrantTimestamp)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
|
@ -213,6 +213,7 @@ object HintsToShow {
|
||||||
const val CONTACTS_INTRO = 16L
|
const val CONTACTS_INTRO = 16L
|
||||||
const val TIMELIMIT_RULE_MUSTREAD = 32L
|
const val TIMELIMIT_RULE_MUSTREAD = 32L
|
||||||
const val BLOCKED_TIME_AREAS_OBSOLETE = 64L
|
const val BLOCKED_TIME_AREAS_OBSOLETE = 64L
|
||||||
|
const val TASKS_INTRODUCTION = 128L
|
||||||
}
|
}
|
||||||
|
|
||||||
object ExperimentalFlags {
|
object ExperimentalFlags {
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.data.model.derived
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import io.timelimit.android.data.model.ChildTask
|
||||||
|
|
||||||
|
data class ChildTaskWithCategoryTitle(
|
||||||
|
@Embedded
|
||||||
|
val childTask: ChildTask,
|
||||||
|
@ColumnInfo(name = "category_title")
|
||||||
|
val categoryTitle: String
|
||||||
|
)
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.data.model.derived
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import io.timelimit.android.data.model.ChildTask
|
||||||
|
|
||||||
|
data class FullChildTask(
|
||||||
|
@Embedded
|
||||||
|
val childTask: ChildTask,
|
||||||
|
@ColumnInfo(name = "category_title")
|
||||||
|
val categoryTitle: String,
|
||||||
|
@ColumnInfo(name = "child_name")
|
||||||
|
val childName: String
|
||||||
|
)
|
|
@ -179,6 +179,7 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
||||||
assignedAppsVersion = "",
|
assignedAppsVersion = "",
|
||||||
timeLimitRulesVersion = "",
|
timeLimitRulesVersion = "",
|
||||||
usedTimesVersion = "",
|
usedTimesVersion = "",
|
||||||
|
tasksVersion = "",
|
||||||
parentCategoryId = "",
|
parentCategoryId = "",
|
||||||
blockAllNotifications = false,
|
blockAllNotifications = false,
|
||||||
timeWarnings = 0,
|
timeWarnings = 0,
|
||||||
|
@ -201,6 +202,7 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
||||||
assignedAppsVersion = "",
|
assignedAppsVersion = "",
|
||||||
timeLimitRulesVersion = "",
|
timeLimitRulesVersion = "",
|
||||||
usedTimesVersion = "",
|
usedTimesVersion = "",
|
||||||
|
tasksVersion = "",
|
||||||
parentCategoryId = "",
|
parentCategoryId = "",
|
||||||
blockAllNotifications = false,
|
blockAllNotifications = false,
|
||||||
timeWarnings = 0,
|
timeWarnings = 0,
|
||||||
|
|
|
@ -105,8 +105,21 @@ data class CategoryItselfHandling (
|
||||||
else
|
else
|
||||||
categoryRelatedData.networks.find { CategoryNetworkId.anonymizeNetworkId(itemId = it.networkItemId, networkId = currentNetworkId) == it.hashedNetworkId } != null
|
categoryRelatedData.networks.find { CategoryNetworkId.anonymizeNetworkId(itemId = it.networkItemId, networkId = currentNetworkId) == it.hashedNetworkId } != null
|
||||||
|
|
||||||
|
val allRelatedRules = if (areLimitsTemporarilyDisabled)
|
||||||
|
emptyList()
|
||||||
|
else
|
||||||
|
RemainingTime.getRulesRelatedToDay(
|
||||||
|
dayOfWeek = dayOfWeek,
|
||||||
|
minuteOfDay = minuteInWeek % MinuteOfDay.LENGTH,
|
||||||
|
rules = categoryRelatedData.rules
|
||||||
|
)
|
||||||
|
|
||||||
|
val regularRelatedRules = allRelatedRules.filter { it.maximumTimeInMillis != 0 }
|
||||||
|
val hasBlockedTimeAreaRelatedRule = allRelatedRules.find { it.maximumTimeInMillis == 0 } != null
|
||||||
|
|
||||||
val missingNetworkTimeForBlockedTimeAreas = !categoryRelatedData.category.blockedMinutesInWeek.dataNotToModify.isEmpty
|
val missingNetworkTimeForBlockedTimeAreas = !categoryRelatedData.category.blockedMinutesInWeek.dataNotToModify.isEmpty
|
||||||
val okByBlockedTimeAreas = areLimitsTemporarilyDisabled || !categoryRelatedData.category.blockedMinutesInWeek.read(minuteInWeek)
|
val okByBlockedTimeAreas = areLimitsTemporarilyDisabled || (
|
||||||
|
(!categoryRelatedData.category.blockedMinutesInWeek.read(minuteInWeek)) && (!hasBlockedTimeAreaRelatedRule))
|
||||||
val dependsOnMaxMinuteOfWeekByBlockedTimeAreas = categoryRelatedData.category.blockedMinutesInWeek.let { blockedTimeAreas ->
|
val dependsOnMaxMinuteOfWeekByBlockedTimeAreas = categoryRelatedData.category.blockedMinutesInWeek.let { blockedTimeAreas ->
|
||||||
if (blockedTimeAreas.dataNotToModify[minuteInWeek]) {
|
if (blockedTimeAreas.dataNotToModify[minuteInWeek]) {
|
||||||
blockedTimeAreas.dataNotToModify.nextClearBit(minuteInWeek)
|
blockedTimeAreas.dataNotToModify.nextClearBit(minuteInWeek)
|
||||||
|
@ -121,27 +134,18 @@ data class CategoryItselfHandling (
|
||||||
else
|
else
|
||||||
dependsOnMaxMinuteOfWeekByBlockedTimeAreas % MinuteOfDay.LENGTH
|
dependsOnMaxMinuteOfWeekByBlockedTimeAreas % MinuteOfDay.LENGTH
|
||||||
|
|
||||||
val relatedRules = if (areLimitsTemporarilyDisabled)
|
|
||||||
emptyList()
|
|
||||||
else
|
|
||||||
RemainingTime.getRulesRelatedToDay(
|
|
||||||
dayOfWeek = dayOfWeek,
|
|
||||||
minuteOfDay = minuteInWeek % MinuteOfDay.LENGTH,
|
|
||||||
rules = categoryRelatedData.rules
|
|
||||||
)
|
|
||||||
|
|
||||||
val remainingTime = RemainingTime.getRemainingTime(
|
val remainingTime = RemainingTime.getRemainingTime(
|
||||||
usedTimes = categoryRelatedData.usedTimes,
|
usedTimes = categoryRelatedData.usedTimes,
|
||||||
// dependsOnMaxTimeByRules always depends on the day so that this is invalidated correctly
|
// dependsOnMaxTimeByRules always depends on the day so that this is invalidated correctly
|
||||||
extraTime = categoryRelatedData.category.getExtraTime(dayOfEpoch = dayOfEpoch),
|
extraTime = categoryRelatedData.category.getExtraTime(dayOfEpoch = dayOfEpoch),
|
||||||
rules = relatedRules,
|
rules = regularRelatedRules,
|
||||||
dayOfWeek = dayOfWeek,
|
dayOfWeek = dayOfWeek,
|
||||||
minuteOfDay = minuteInWeek % MinuteOfDay.LENGTH,
|
minuteOfDay = minuteInWeek % MinuteOfDay.LENGTH,
|
||||||
firstDayOfWeekAsEpochDay = firstDayOfWeekAsEpochDay
|
firstDayOfWeekAsEpochDay = firstDayOfWeekAsEpochDay
|
||||||
)
|
)
|
||||||
|
|
||||||
val remainingSessionDuration = RemainingSessionDuration.getRemainingSessionDuration(
|
val remainingSessionDuration = RemainingSessionDuration.getRemainingSessionDuration(
|
||||||
rules = relatedRules,
|
rules = regularRelatedRules,
|
||||||
minuteOfDay = minuteInWeek % MinuteOfDay.LENGTH,
|
minuteOfDay = minuteInWeek % MinuteOfDay.LENGTH,
|
||||||
dayOfWeek = dayOfWeek,
|
dayOfWeek = dayOfWeek,
|
||||||
timestamp = timeInMillis,
|
timestamp = timeInMillis,
|
||||||
|
@ -149,8 +153,8 @@ data class CategoryItselfHandling (
|
||||||
)
|
)
|
||||||
|
|
||||||
val missingNetworkTimeForRules = categoryRelatedData.rules.isNotEmpty()
|
val missingNetworkTimeForRules = categoryRelatedData.rules.isNotEmpty()
|
||||||
val okByTimeLimitRules = relatedRules.isEmpty() || (remainingTime != null && remainingTime.hasRemainingTime)
|
val okByTimeLimitRules = regularRelatedRules.isEmpty() || (remainingTime != null && remainingTime.hasRemainingTime)
|
||||||
val dependsOnMaxTimeByMinuteOfDay = (relatedRules.minBy { it.endMinuteOfDay }?.endMinuteOfDay ?: Int.MAX_VALUE).coerceAtMost(
|
val dependsOnMaxTimeByMinuteOfDay = (allRelatedRules.minBy { it.endMinuteOfDay }?.endMinuteOfDay ?: Int.MAX_VALUE).coerceAtMost(
|
||||||
categoryRelatedData.rules
|
categoryRelatedData.rules
|
||||||
.filter {
|
.filter {
|
||||||
// related to today
|
// related to today
|
||||||
|
@ -195,12 +199,12 @@ data class CategoryItselfHandling (
|
||||||
val missingNetworkTime = !shouldTrustTimeTemporarily &&
|
val missingNetworkTime = !shouldTrustTimeTemporarily &&
|
||||||
(missingNetworkTimeForDisableTempBlocking || missingNetworkTimeForBlockedTimeAreas || missingNetworkTimeForRules)
|
(missingNetworkTimeForDisableTempBlocking || missingNetworkTimeForBlockedTimeAreas || missingNetworkTimeForRules)
|
||||||
|
|
||||||
val shouldCountTime = relatedRules.isNotEmpty()
|
val shouldCountTime = regularRelatedRules.isNotEmpty()
|
||||||
val shouldCountExtraTime = remainingTime?.usingExtraTime == true
|
val shouldCountExtraTime = remainingTime?.usingExtraTime == true
|
||||||
val sessionDurationSlotsToCount = if (remainingSessionDuration != null && remainingSessionDuration <= 0)
|
val sessionDurationSlotsToCount = if (remainingSessionDuration != null && remainingSessionDuration <= 0)
|
||||||
emptySet()
|
emptySet()
|
||||||
else
|
else
|
||||||
relatedRules.filter { it.sessionDurationLimitEnabled }.map {
|
regularRelatedRules.filter { it.sessionDurationLimitEnabled }.map {
|
||||||
AddUsedTimeActionItemSessionDurationLimitSlot(
|
AddUsedTimeActionItemSessionDurationLimitSlot(
|
||||||
startMinuteOfDay = it.startMinuteOfDay,
|
startMinuteOfDay = it.startMinuteOfDay,
|
||||||
endMinuteOfDay = it.endMinuteOfDay,
|
endMinuteOfDay = it.endMinuteOfDay,
|
||||||
|
@ -219,7 +223,7 @@ data class CategoryItselfHandling (
|
||||||
val maxTimeToAdd = maxTimeToAddByRegularTime.coerceAtMost(maxTimeToAddBySessionDuration)
|
val maxTimeToAdd = maxTimeToAddByRegularTime.coerceAtMost(maxTimeToAddBySessionDuration)
|
||||||
|
|
||||||
val additionalTimeCountingSlots = if (shouldCountTime)
|
val additionalTimeCountingSlots = if (shouldCountTime)
|
||||||
relatedRules
|
regularRelatedRules
|
||||||
.filterNot { it.appliesToWholeDay }
|
.filterNot { it.appliesToWholeDay }
|
||||||
.map { AddUsedTimeActionItemAdditionalCountingSlot(it.startMinuteOfDay, it.endMinuteOfDay) }
|
.map { AddUsedTimeActionItemAdditionalCountingSlot(it.startMinuteOfDay, it.endMinuteOfDay) }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
|
@ -325,6 +325,7 @@ object ApplyServerDataStatus {
|
||||||
assignedAppsVersion = "",
|
assignedAppsVersion = "",
|
||||||
timeLimitRulesVersion = "",
|
timeLimitRulesVersion = "",
|
||||||
usedTimesVersion = "",
|
usedTimesVersion = "",
|
||||||
|
tasksVersion = "",
|
||||||
parentCategoryId = newCategory.parentCategoryId,
|
parentCategoryId = newCategory.parentCategoryId,
|
||||||
timeWarnings = newCategory.timeWarnings,
|
timeWarnings = newCategory.timeWarnings,
|
||||||
minBatteryLevelMobile = newCategory.minBatteryLevelMobile,
|
minBatteryLevelMobile = newCategory.minBatteryLevelMobile,
|
||||||
|
@ -495,6 +496,25 @@ object ApplyServerDataStatus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
status.newCategoryTasks.forEach { tasks ->
|
||||||
|
database.childTasks().removeTasksByCategoryId(tasks.categoryId)
|
||||||
|
|
||||||
|
database.childTasks().forceInsertItemsSync(
|
||||||
|
tasks.tasks.map { task ->
|
||||||
|
ChildTask(
|
||||||
|
taskId = task.taskId,
|
||||||
|
categoryId = tasks.categoryId,
|
||||||
|
taskTitle = task.taskTitle,
|
||||||
|
extraTimeDuration = task.extraTimeDuration,
|
||||||
|
pendingRequest = task.pendingRequest,
|
||||||
|
lastGrantTimestamp = task.lastGrantTimestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
database.category().updateCategoryTasksVersion(categoryId = tasks.categoryId, tasksVersion = tasks.version)
|
||||||
|
}
|
||||||
|
|
||||||
status.newUserList?.data?.forEach { user ->
|
status.newUserList?.data?.forEach { user ->
|
||||||
if (user.limitLoginCategory == null) {
|
if (user.limitLoginCategory == null) {
|
||||||
database.userLimitLoginCategoryDao().removeItemSync(user.id)
|
database.userLimitLoginCategoryDao().removeItemSync(user.id)
|
||||||
|
|
|
@ -492,6 +492,24 @@ object ForceSyncAction: AppLogicAction() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class MarkTaskPendingAction(val taskId: String): AppLogicAction() {
|
||||||
|
companion object {
|
||||||
|
const val TYPE_VALUE = "MARK_TASK_PENDING"
|
||||||
|
private const val TASK_ID = "taskId"
|
||||||
|
}
|
||||||
|
|
||||||
|
init { IdGenerator.assertIdValid(taskId) }
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(TYPE).value(TYPE_VALUE)
|
||||||
|
writer.name(TASK_ID).value(taskId)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class AddCategoryAppsAction(val categoryId: String, val packageNames: List<String>): ParentAction() {
|
data class AddCategoryAppsAction(val categoryId: String, val packageNames: List<String>): ParentAction() {
|
||||||
companion object {
|
companion object {
|
||||||
const val TYPE_VALUE = "ADD_CATEGORY_APPS"
|
const val TYPE_VALUE = "ADD_CATEGORY_APPS"
|
||||||
|
@ -928,6 +946,81 @@ data class UpdateCategoryDisableLimitsAction(val categoryId: String, val endTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class UpdateChildTaskAction(val isNew: Boolean, val taskId: String, val categoryId: String, val taskTitle: String, val extraTimeDuration: Int): ParentAction() {
|
||||||
|
companion object {
|
||||||
|
private const val TYPE_VALUE = "UPDATE_CHILD_TASK"
|
||||||
|
private const val IS_NEW = "isNew"
|
||||||
|
private const val TASK_ID = "taskId"
|
||||||
|
private const val CATEGORY_ID = "categoryId"
|
||||||
|
private const val TASK_TITLE = "taskTitle"
|
||||||
|
private const val EXTRA_TIME_DURATION = "extraTimeDuration"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
IdGenerator.assertIdValid(taskId)
|
||||||
|
IdGenerator.assertIdValid(categoryId)
|
||||||
|
|
||||||
|
if (taskTitle.isEmpty() || taskTitle.length > ChildTask.MAX_TASK_TITLE_LENGTH) throw IllegalArgumentException()
|
||||||
|
if (extraTimeDuration <= 0 || extraTimeDuration > ChildTask.MAX_EXTRA_TIME) throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(TYPE).value(TYPE_VALUE)
|
||||||
|
writer.name(IS_NEW).value(isNew)
|
||||||
|
writer.name(TASK_ID).value(taskId)
|
||||||
|
writer.name(CATEGORY_ID).value(categoryId)
|
||||||
|
writer.name(TASK_TITLE).value(taskTitle)
|
||||||
|
writer.name(EXTRA_TIME_DURATION).value(extraTimeDuration)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DeleteChildTaskAction(val taskId: String): ParentAction() {
|
||||||
|
companion object {
|
||||||
|
private const val TYPE_VALUE = "DELETE_CHILD_TASK"
|
||||||
|
private const val TASK_ID = "taskId"
|
||||||
|
}
|
||||||
|
|
||||||
|
init { IdGenerator.assertIdValid(taskId) }
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(TYPE).value(TYPE_VALUE)
|
||||||
|
writer.name(TASK_ID).value(taskId)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ReviewChildTaskAction(val taskId: String, val ok: Boolean, val time: Long): ParentAction() {
|
||||||
|
companion object {
|
||||||
|
private const val TYPE_VALUE = "REVIEW_CHILD_TASK"
|
||||||
|
private const val TASK_ID = "taskId"
|
||||||
|
private const val OK = "ok"
|
||||||
|
private const val TIME = "time"
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (time <= 0) throw IllegalArgumentException()
|
||||||
|
IdGenerator.assertIdValid(taskId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(writer: JsonWriter) {
|
||||||
|
writer.beginObject()
|
||||||
|
|
||||||
|
writer.name(TYPE).value(TYPE_VALUE)
|
||||||
|
writer.name(TASK_ID).value(taskId)
|
||||||
|
writer.name(OK).value(ok)
|
||||||
|
writer.name(TIME).value(time)
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DeviceDao
|
// DeviceDao
|
||||||
|
|
||||||
data class UpdateDeviceStatusAction(
|
data class UpdateDeviceStatusAction(
|
||||||
|
|
|
@ -381,6 +381,15 @@ object LocalDatabaseAppLogicActionDispatcher {
|
||||||
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
is MarkTaskPendingAction -> {
|
||||||
|
val task = database.childTasks().getTaskByTaskId(action.taskId) ?: throw RuntimeException()
|
||||||
|
val category = database.category().getCategoryByIdSync(task.categoryId)!!
|
||||||
|
val device = database.device().getDeviceByIdSync(deviceId)!!
|
||||||
|
|
||||||
|
if (category.childId != device.currentUserId) throw IllegalStateException()
|
||||||
|
|
||||||
|
database.childTasks().updateItemSync(task.copy(pendingRequest = true))
|
||||||
|
}
|
||||||
}.let { }
|
}.let { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,6 +139,7 @@ object LocalDatabaseParentActionDispatcher {
|
||||||
assignedAppsVersion = "",
|
assignedAppsVersion = "",
|
||||||
timeLimitRulesVersion = "",
|
timeLimitRulesVersion = "",
|
||||||
usedTimesVersion = "",
|
usedTimesVersion = "",
|
||||||
|
tasksVersion = "",
|
||||||
parentCategoryId = "",
|
parentCategoryId = "",
|
||||||
blockAllNotifications = false,
|
blockAllNotifications = false,
|
||||||
timeWarnings = 0,
|
timeWarnings = 0,
|
||||||
|
@ -767,6 +768,64 @@ object LocalDatabaseParentActionDispatcher {
|
||||||
|
|
||||||
database.category().updateCategorySync(category.copy(disableLimitsUntil = action.endTime))
|
database.category().updateCategorySync(category.copy(disableLimitsUntil = action.endTime))
|
||||||
}
|
}
|
||||||
|
is UpdateChildTaskAction -> {
|
||||||
|
val task = database.childTasks().getTaskByTaskId(taskId = action.taskId)
|
||||||
|
val notFound = task == null
|
||||||
|
|
||||||
|
if (notFound != action.isNew) {
|
||||||
|
if (action.isNew) {
|
||||||
|
throw IllegalArgumentException("task exists already")
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("task not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task == null) {
|
||||||
|
database.childTasks().insertItemSync(
|
||||||
|
ChildTask(
|
||||||
|
taskId = action.taskId,
|
||||||
|
taskTitle = action.taskTitle,
|
||||||
|
categoryId = action.categoryId,
|
||||||
|
extraTimeDuration = action.extraTimeDuration,
|
||||||
|
lastGrantTimestamp = 0,
|
||||||
|
pendingRequest = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
database.childTasks().updateItemSync(
|
||||||
|
task.copy(
|
||||||
|
taskTitle = action.taskTitle,
|
||||||
|
categoryId = action.categoryId,
|
||||||
|
extraTimeDuration = action.extraTimeDuration,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is DeleteChildTaskAction -> {
|
||||||
|
val task = database.childTasks().getTaskByTaskId(taskId = action.taskId) ?: throw IllegalArgumentException("task not found")
|
||||||
|
|
||||||
|
database.childTasks().removeTaskById(taskId = task.taskId)
|
||||||
|
}
|
||||||
|
is ReviewChildTaskAction -> {
|
||||||
|
val task = database.childTasks().getTaskByTaskId(taskId = action.taskId) ?: throw IllegalArgumentException("task not found")
|
||||||
|
|
||||||
|
if (!task.pendingRequest) throw IllegalArgumentException("did review of a task which is not pending")
|
||||||
|
|
||||||
|
if (action.ok) {
|
||||||
|
val category = database.category().getCategoryByIdSync(task.categoryId)!!
|
||||||
|
|
||||||
|
if (category.extraTimeDay != 0 && category.extraTimeInMillis > 0) {
|
||||||
|
// if the current time is daily, then extend the daily time only
|
||||||
|
database.category().updateCategoryExtraTime(categoryId = category.id, extraTimeDay = category.extraTimeDay, newExtraTime = category.extraTimeInMillis + task.extraTimeDuration)
|
||||||
|
} else {
|
||||||
|
database.category().updateCategoryExtraTime(categoryId = category.id, extraTimeDay = -1, newExtraTime = category.extraTimeInMillis + task.extraTimeDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
database.childTasks().updateItemSync(task.copy(pendingRequest = false, lastGrantTimestamp = action.time))
|
||||||
|
} else {
|
||||||
|
database.childTasks().updateItemSync(task.copy(pendingRequest = false))
|
||||||
|
}
|
||||||
|
}
|
||||||
}.let { }
|
}.let { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ data class ClientDataStatus(
|
||||||
private const val CATEGORIES = "categories"
|
private const val CATEGORIES = "categories"
|
||||||
private const val USERS = "users"
|
private const val USERS = "users"
|
||||||
private const val CLIENT_LEVEL = "clientLevel"
|
private const val CLIENT_LEVEL = "clientLevel"
|
||||||
private const val CLIENT_LEVEL_VALUE = 2
|
private const val CLIENT_LEVEL_VALUE = 3
|
||||||
|
|
||||||
val empty = ClientDataStatus(
|
val empty = ClientDataStatus(
|
||||||
deviceListVersion = "",
|
deviceListVersion = "",
|
||||||
|
@ -63,7 +63,8 @@ data class ClientDataStatus(
|
||||||
baseVersion = it.baseVersion,
|
baseVersion = it.baseVersion,
|
||||||
assignedAppsVersion = it.assignedAppsVersion,
|
assignedAppsVersion = it.assignedAppsVersion,
|
||||||
timeLimitRulesVersion = it.timeLimitRulesVersion,
|
timeLimitRulesVersion = it.timeLimitRulesVersion,
|
||||||
usedTimeItemsVersion = it.usedTimeItemsVersion
|
usedTimeItemsVersion = it.usedTimeItemsVersion,
|
||||||
|
taskListVersion = it.taskListVersion
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,13 +105,15 @@ data class CategoryDataStatus(
|
||||||
val baseVersion: String,
|
val baseVersion: String,
|
||||||
val assignedAppsVersion: String,
|
val assignedAppsVersion: String,
|
||||||
val timeLimitRulesVersion: String,
|
val timeLimitRulesVersion: String,
|
||||||
val usedTimeItemsVersion: String
|
val usedTimeItemsVersion: String,
|
||||||
|
val taskListVersion: String
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
private const val BASE_VERSION = "base"
|
private const val BASE_VERSION = "base"
|
||||||
private const val ASSIGNED_APPS_VERSION = "apps"
|
private const val ASSIGNED_APPS_VERSION = "apps"
|
||||||
private const val TIME_LIMIT_RULES_VERSION = "rules"
|
private const val TIME_LIMIT_RULES_VERSION = "rules"
|
||||||
private const val USED_TIME_ITEMS_VERSION = "usedTime"
|
private const val USED_TIME_ITEMS_VERSION = "usedTime"
|
||||||
|
private const val TASK_LIST_VERSION = "tasks"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun serialize(writer: JsonWriter) {
|
fun serialize(writer: JsonWriter) {
|
||||||
|
@ -121,6 +124,8 @@ data class CategoryDataStatus(
|
||||||
writer.name(TIME_LIMIT_RULES_VERSION).value(timeLimitRulesVersion)
|
writer.name(TIME_LIMIT_RULES_VERSION).value(timeLimitRulesVersion)
|
||||||
writer.name(USED_TIME_ITEMS_VERSION).value(usedTimeItemsVersion)
|
writer.name(USED_TIME_ITEMS_VERSION).value(usedTimeItemsVersion)
|
||||||
|
|
||||||
|
if (taskListVersion.isNotEmpty()) writer.name(TASK_LIST_VERSION).value(taskListVersion)
|
||||||
|
|
||||||
writer.endObject()
|
writer.endObject()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -40,6 +40,7 @@ data class ServerDataStatus(
|
||||||
val newCategoryAssignedApps: List<ServerUpdatedCategoryAssignedApps>,
|
val newCategoryAssignedApps: List<ServerUpdatedCategoryAssignedApps>,
|
||||||
val newCategoryUsedTimes: List<ServerUpdatedCategoryUsedTimes>,
|
val newCategoryUsedTimes: List<ServerUpdatedCategoryUsedTimes>,
|
||||||
val newCategoryTimeLimitRules: List<ServerUpdatedTimeLimitRules>,
|
val newCategoryTimeLimitRules: List<ServerUpdatedTimeLimitRules>,
|
||||||
|
val newCategoryTasks: List<ServerUpdatedCategoryTasks>,
|
||||||
val newUserList: ServerUserList?,
|
val newUserList: ServerUserList?,
|
||||||
val fullVersionUntil: Long,
|
val fullVersionUntil: Long,
|
||||||
val message: String?
|
val message: String?
|
||||||
|
@ -52,6 +53,7 @@ data class ServerDataStatus(
|
||||||
private const val NEW_CATEGORY_ASSIGNED_APPS = "categoryApp"
|
private const val NEW_CATEGORY_ASSIGNED_APPS = "categoryApp"
|
||||||
private const val NEW_CATEGORY_USED_TIMES = "usedTimes"
|
private const val NEW_CATEGORY_USED_TIMES = "usedTimes"
|
||||||
private const val NEW_CATEGORY_TIME_LIMIT_RULES = "rules"
|
private const val NEW_CATEGORY_TIME_LIMIT_RULES = "rules"
|
||||||
|
private const val NEW_CATEGORY_TASKS = "tasks"
|
||||||
private const val NEW_USER_LIST = "users"
|
private const val NEW_USER_LIST = "users"
|
||||||
private const val FULL_VERSION_UNTIL = "fullVersion"
|
private const val FULL_VERSION_UNTIL = "fullVersion"
|
||||||
private const val MESSAGE = "message"
|
private const val MESSAGE = "message"
|
||||||
|
@ -64,6 +66,7 @@ data class ServerDataStatus(
|
||||||
var newCategoryAssignedApps: List<ServerUpdatedCategoryAssignedApps> = Collections.emptyList()
|
var newCategoryAssignedApps: List<ServerUpdatedCategoryAssignedApps> = Collections.emptyList()
|
||||||
var newCategoryUsedTimes: List<ServerUpdatedCategoryUsedTimes> = Collections.emptyList()
|
var newCategoryUsedTimes: List<ServerUpdatedCategoryUsedTimes> = Collections.emptyList()
|
||||||
var newCategoryTimeLimitRules: List<ServerUpdatedTimeLimitRules> = Collections.emptyList()
|
var newCategoryTimeLimitRules: List<ServerUpdatedTimeLimitRules> = Collections.emptyList()
|
||||||
|
var newCategoryTasks: List<ServerUpdatedCategoryTasks> = emptyList()
|
||||||
var newUserList: ServerUserList? = null
|
var newUserList: ServerUserList? = null
|
||||||
var fullVersionUntil: Long? = null
|
var fullVersionUntil: Long? = null
|
||||||
var message: String? = null
|
var message: String? = null
|
||||||
|
@ -78,6 +81,7 @@ data class ServerDataStatus(
|
||||||
NEW_CATEGORY_ASSIGNED_APPS -> newCategoryAssignedApps = ServerUpdatedCategoryAssignedApps.parseList(reader)
|
NEW_CATEGORY_ASSIGNED_APPS -> newCategoryAssignedApps = ServerUpdatedCategoryAssignedApps.parseList(reader)
|
||||||
NEW_CATEGORY_USED_TIMES -> newCategoryUsedTimes = ServerUpdatedCategoryUsedTimes.parseList(reader)
|
NEW_CATEGORY_USED_TIMES -> newCategoryUsedTimes = ServerUpdatedCategoryUsedTimes.parseList(reader)
|
||||||
NEW_CATEGORY_TIME_LIMIT_RULES -> newCategoryTimeLimitRules = ServerUpdatedTimeLimitRules.parseList(reader)
|
NEW_CATEGORY_TIME_LIMIT_RULES -> newCategoryTimeLimitRules = ServerUpdatedTimeLimitRules.parseList(reader)
|
||||||
|
NEW_CATEGORY_TASKS -> newCategoryTasks = ServerUpdatedCategoryTasks.parseList(reader)
|
||||||
NEW_USER_LIST -> newUserList = ServerUserList.parse(reader)
|
NEW_USER_LIST -> newUserList = ServerUserList.parse(reader)
|
||||||
FULL_VERSION_UNTIL -> fullVersionUntil = reader.nextLong()
|
FULL_VERSION_UNTIL -> fullVersionUntil = reader.nextLong()
|
||||||
MESSAGE -> message = reader.nextString()
|
MESSAGE -> message = reader.nextString()
|
||||||
|
@ -94,6 +98,7 @@ data class ServerDataStatus(
|
||||||
newCategoryAssignedApps = newCategoryAssignedApps,
|
newCategoryAssignedApps = newCategoryAssignedApps,
|
||||||
newCategoryUsedTimes = newCategoryUsedTimes,
|
newCategoryUsedTimes = newCategoryUsedTimes,
|
||||||
newCategoryTimeLimitRules = newCategoryTimeLimitRules,
|
newCategoryTimeLimitRules = newCategoryTimeLimitRules,
|
||||||
|
newCategoryTasks = newCategoryTasks,
|
||||||
newUserList = newUserList,
|
newUserList = newUserList,
|
||||||
fullVersionUntil = fullVersionUntil!!,
|
fullVersionUntil = fullVersionUntil!!,
|
||||||
message = message
|
message = message
|
||||||
|
@ -892,6 +897,90 @@ data class ServerTimeLimitRule(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class ServerUpdatedCategoryTasks (
|
||||||
|
val categoryId: String,
|
||||||
|
val version: String,
|
||||||
|
val tasks: List<ServerUpdatedCategoryTask>
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private const val CATEGORY_ID = "categoryId"
|
||||||
|
private const val VERSION = "version"
|
||||||
|
private const val TASKS = "tasks"
|
||||||
|
|
||||||
|
fun parse(reader: JsonReader): ServerUpdatedCategoryTasks {
|
||||||
|
var categoryId: String? = null
|
||||||
|
var version: String? = null
|
||||||
|
var tasks: List<ServerUpdatedCategoryTask>? = null
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when (reader.nextName()) {
|
||||||
|
CATEGORY_ID -> categoryId = reader.nextString()
|
||||||
|
VERSION -> version = reader.nextString()
|
||||||
|
TASKS -> tasks = ServerUpdatedCategoryTask.parseList(reader)
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
return ServerUpdatedCategoryTasks(
|
||||||
|
categoryId = categoryId!!,
|
||||||
|
version = version!!,
|
||||||
|
tasks = tasks!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseList(reader: JsonReader): List<ServerUpdatedCategoryTasks> = parseJsonArray(reader) { parse(reader) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ServerUpdatedCategoryTask (
|
||||||
|
val taskId: String,
|
||||||
|
val taskTitle: String,
|
||||||
|
val extraTimeDuration: Int,
|
||||||
|
val pendingRequest: Boolean,
|
||||||
|
val lastGrantTimestamp: Long
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
private const val TASK_ID = "i"
|
||||||
|
private const val TASK_TITLE = "t"
|
||||||
|
private const val EXTRA_TIME_DURATION = "d"
|
||||||
|
private const val PENDING_REQUEST = "p"
|
||||||
|
private const val LAST_GRANT_TIMESTAMP = "l"
|
||||||
|
|
||||||
|
fun parse(reader: JsonReader): ServerUpdatedCategoryTask {
|
||||||
|
var taskId: String? = null
|
||||||
|
var taskTitle: String? = null
|
||||||
|
var extraTimeDuration: Int? = null
|
||||||
|
var pendingRequest: Boolean? = null
|
||||||
|
var lastGrantTimestamp: Long? = null
|
||||||
|
|
||||||
|
reader.beginObject()
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
when (reader.nextName()) {
|
||||||
|
TASK_ID -> taskId = reader.nextString()
|
||||||
|
TASK_TITLE -> taskTitle = reader.nextString()
|
||||||
|
EXTRA_TIME_DURATION -> extraTimeDuration = reader.nextInt()
|
||||||
|
PENDING_REQUEST -> pendingRequest = reader.nextBoolean()
|
||||||
|
LAST_GRANT_TIMESTAMP -> lastGrantTimestamp = reader.nextLong()
|
||||||
|
else -> reader.skipValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.endObject()
|
||||||
|
|
||||||
|
return ServerUpdatedCategoryTask(
|
||||||
|
taskId = taskId!!,
|
||||||
|
taskTitle = taskTitle!!,
|
||||||
|
extraTimeDuration = extraTimeDuration!!,
|
||||||
|
pendingRequest = pendingRequest!!,
|
||||||
|
lastGrantTimestamp = lastGrantTimestamp!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseList(reader: JsonReader): List<ServerUpdatedCategoryTask> = parseJsonArray(reader) { parse(reader) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class ServerInstalledAppsData(
|
data class ServerInstalledAppsData(
|
||||||
val deviceId: String,
|
val deviceId: String,
|
||||||
val version: String,
|
val version: String,
|
||||||
|
|
|
@ -25,6 +25,7 @@ import io.timelimit.android.ui.main.FragmentWithCustomTitle
|
||||||
import io.timelimit.android.ui.manage.category.usagehistory.UsageHistoryFragment
|
import io.timelimit.android.ui.manage.category.usagehistory.UsageHistoryFragment
|
||||||
import io.timelimit.android.ui.manage.child.advanced.ManageChildAdvancedFragment
|
import io.timelimit.android.ui.manage.child.advanced.ManageChildAdvancedFragment
|
||||||
import io.timelimit.android.ui.manage.child.apps.ChildAppsFragment
|
import io.timelimit.android.ui.manage.child.apps.ChildAppsFragment
|
||||||
|
import io.timelimit.android.ui.manage.child.tasks.ManageChildTasksFragment
|
||||||
|
|
||||||
abstract class ChildFragmentWrapper: SingleFragmentWrapper() {
|
abstract class ChildFragmentWrapper: SingleFragmentWrapper() {
|
||||||
abstract val childId: String
|
abstract val childId: String
|
||||||
|
@ -65,3 +66,12 @@ class ChildUsageHistoryFragmentWrapper: ChildFragmentWrapper(), FragmentWithCust
|
||||||
override fun createChildFragment(): Fragment = UsageHistoryFragment.newInstance(userId = childId, categoryId = null)
|
override fun createChildFragment(): Fragment = UsageHistoryFragment.newInstance(userId = childId, categoryId = null)
|
||||||
override fun getCustomTitle() = child.map { it?.let { "${it.name} - ${getString(R.string.usage_history_title)}" } }
|
override fun getCustomTitle() = child.map { it?.let { "${it.name} - ${getString(R.string.usage_history_title)}" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ChildTasksFragmentWrapper: ChildFragmentWrapper(), FragmentWithCustomTitle {
|
||||||
|
private val params by lazy { ChildTasksFragmentWrapperArgs.fromBundle(requireArguments()) }
|
||||||
|
override val childId: String get() = params.childId
|
||||||
|
override val showAuthButton: Boolean = true
|
||||||
|
|
||||||
|
override fun createChildFragment(): Fragment = ManageChildTasksFragment.newInstance(childId = childId)
|
||||||
|
override fun getCustomTitle() = child.map { it?.let { "${it.name} - ${getString(R.string.manage_child_tasks)}" } }
|
||||||
|
}
|
|
@ -85,6 +85,8 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val adapter = LockActivityAdapter(supportFragmentManager, this)
|
||||||
|
|
||||||
setContentView(R.layout.lock_activity)
|
setContentView(R.layout.lock_activity)
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
|
@ -97,7 +99,7 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
|
||||||
|
|
||||||
model.init(blockedPackageName, blockedActivityName)
|
model.init(blockedPackageName, blockedActivityName)
|
||||||
|
|
||||||
pager.adapter = LockActivityAdapter(supportFragmentManager, this)
|
pager.adapter = adapter
|
||||||
|
|
||||||
model.content.observe(this) {
|
model.content.observe(this) {
|
||||||
if (isResumed && it is LockscreenContent.Blocked.BlockedCategory && it.reason == BlockingReason.RequiresCurrentDevice && !model.didOpenSetCurrentDeviceScreen) {
|
if (isResumed && it is LockscreenContent.Blocked.BlockedCategory && it.reason == BlockingReason.RequiresCurrentDevice && !model.didOpenSetCurrentDeviceScreen) {
|
||||||
|
@ -123,11 +125,17 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
|
||||||
override fun onPageSelected(position: Int) {
|
override fun onPageSelected(position: Int) {
|
||||||
super.onPageSelected(position)
|
super.onPageSelected(position)
|
||||||
|
|
||||||
showAuth.value = position > 0
|
showAuth.value = position == 1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
tabs.setupWithViewPager(pager)
|
tabs.setupWithViewPager(pager)
|
||||||
|
|
||||||
|
model.content.observe(this) {
|
||||||
|
val isTimeOver = it is LockscreenContent.Blocked.BlockedCategory && it.blockingHandling.activityBlockingReason == BlockingReason.TimeOver
|
||||||
|
|
||||||
|
adapter.showTasksFragment = isTimeOver
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|
|
@ -21,19 +21,24 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.FragmentPagerAdapter
|
import androidx.fragment.app.FragmentPagerAdapter
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
class LockActivityAdapter(fragmentManager: FragmentManager, private val context: Context): FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
class LockActivityAdapter(fragmentManager: FragmentManager, private val context: Context): FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||||
override fun getCount(): Int = 2
|
var showTasksFragment: Boolean by Delegates.observable(false) { _, _, _ -> notifyDataSetChanged() }
|
||||||
|
|
||||||
|
override fun getCount(): Int = if (showTasksFragment) 3 else 2
|
||||||
|
|
||||||
override fun getItem(position: Int): Fragment = when (position) {
|
override fun getItem(position: Int): Fragment = when (position) {
|
||||||
0 -> LockReasonFragment()
|
0 -> LockReasonFragment()
|
||||||
1 -> LockActionFragment()
|
1 -> LockActionFragment()
|
||||||
|
2 -> LockTaskFragment()
|
||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPageTitle(position: Int): CharSequence? = context.getString(when (position) {
|
override fun getPageTitle(position: Int): CharSequence? = context.getString(when (position) {
|
||||||
0 -> R.string.lock_tab_reason
|
0 -> R.string.lock_tab_reason
|
||||||
1 -> R.string.lock_tab_action
|
1 -> R.string.lock_tab_action
|
||||||
|
2 -> R.string.lock_tab_task
|
||||||
else -> throw IllegalArgumentException()
|
else -> throw IllegalArgumentException()
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -171,6 +171,18 @@ class LockModel(application: Application): AndroidViewModel(application) {
|
||||||
|
|
||||||
val osClockInMillis = liveDataFromFunction { logic.timeApi.getCurrentTimeInMillis() }
|
val osClockInMillis = liveDataFromFunction { logic.timeApi.getCurrentTimeInMillis() }
|
||||||
|
|
||||||
|
private val categoryIdForTasks = content.map {
|
||||||
|
if (it is LockscreenContent.Blocked.BlockedCategory && it.blockingHandling.activityBlockingReason == BlockingReason.TimeOver)
|
||||||
|
it.blockedCategoryId
|
||||||
|
else null
|
||||||
|
}.ignoreUnchanged()
|
||||||
|
|
||||||
|
val blockedCategoryTasks = categoryIdForTasks.switchMap { categoryId ->
|
||||||
|
if (categoryId != null)
|
||||||
|
logic.database.childTasks().getTasksByCategoryId(categoryId)
|
||||||
|
else liveDataFromValue(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
fun confirmLocalTime() {
|
fun confirmLocalTime() {
|
||||||
logic.realTimeLogic.confirmLocalTime()
|
logic.realTimeLogic.confirmLocalTime()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.lock
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.model.ChildTask
|
||||||
|
import io.timelimit.android.databinding.ChildTaskItemBinding
|
||||||
|
import io.timelimit.android.databinding.IntroCardBinding
|
||||||
|
import io.timelimit.android.util.TimeTextUtil
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
class LockTaskAdapter: RecyclerView.Adapter<LockTaskAdapter.Holder>() {
|
||||||
|
companion object {
|
||||||
|
private const val TYPE_INTRODUCTION = 1
|
||||||
|
private const val TYPE_ITEM = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
var content: List<LockTaskItem> by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() }
|
||||||
|
var listener: Listener? = null
|
||||||
|
|
||||||
|
init { setHasStableIds(true) }
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = content.size
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long = content[position].let { item ->
|
||||||
|
when (item) {
|
||||||
|
is LockTaskItem.Task -> item.task.taskId.hashCode()
|
||||||
|
else -> item.hashCode()
|
||||||
|
}
|
||||||
|
}.toLong()
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int = when (content[position]) {
|
||||||
|
is LockTaskItem.Task -> TYPE_ITEM
|
||||||
|
LockTaskItem.Introduction -> TYPE_INTRODUCTION
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = LayoutInflater.from(parent.context).let { inflater ->
|
||||||
|
LockTaskAdapter.Holder(
|
||||||
|
when (viewType) {
|
||||||
|
TYPE_INTRODUCTION -> IntroCardBinding.inflate(inflater, parent, false).also {
|
||||||
|
it.title = parent.context.getString(R.string.lock_tab_task)
|
||||||
|
it.text = parent.context.getString(R.string.lock_task_introduction)
|
||||||
|
it.noSwipe = true
|
||||||
|
}.root
|
||||||
|
TYPE_ITEM -> ChildTaskItemBinding.inflate(inflater, parent, false).also {
|
||||||
|
it.root.tag = it
|
||||||
|
}.root
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: LockTaskAdapter.Holder, position: Int) {
|
||||||
|
val context = holder.itemView.context
|
||||||
|
val item = content[position]
|
||||||
|
|
||||||
|
when (item) {
|
||||||
|
LockTaskItem.Introduction -> {/* nothing to do */}
|
||||||
|
is LockTaskItem.Task -> {
|
||||||
|
val binding = holder.itemView.tag as ChildTaskItemBinding
|
||||||
|
|
||||||
|
binding.title = item.task.taskTitle
|
||||||
|
binding.duration = TimeTextUtil.time(item.task.extraTimeDuration, context)
|
||||||
|
binding.pendingReview = item.task.pendingRequest
|
||||||
|
|
||||||
|
binding.executePendingBindings()
|
||||||
|
|
||||||
|
binding.root.setOnClickListener { listener?.onTaskClicked(item.task) }
|
||||||
|
}
|
||||||
|
}.let {/* require handling all paths */}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder(view: View): RecyclerView.ViewHolder(view)
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onTaskClicked(task: ChildTask)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.lock
|
||||||
|
|
||||||
|
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.activityViewModels
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.model.ChildTask
|
||||||
|
import io.timelimit.android.ui.manage.child.tasks.ConfirmTaskDialogFragment
|
||||||
|
import kotlinx.android.synthetic.main.recycler_fragment.*
|
||||||
|
|
||||||
|
class LockTaskFragment: Fragment() {
|
||||||
|
private val model: LockModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.recycler_fragment, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val adapter = LockTaskAdapter()
|
||||||
|
|
||||||
|
recycler.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
recycler.adapter = adapter
|
||||||
|
|
||||||
|
model.blockedCategoryTasks.observe(viewLifecycleOwner) { tasks ->
|
||||||
|
adapter.content = listOf(LockTaskItem.Introduction) + tasks.map { LockTaskItem.Task(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter.listener = object: LockTaskAdapter.Listener {
|
||||||
|
override fun onTaskClicked(task: ChildTask) {
|
||||||
|
if (task.pendingRequest)
|
||||||
|
TaskReviewPendingDialogFragment.newInstance().show(parentFragmentManager)
|
||||||
|
else
|
||||||
|
ConfirmTaskDialogFragment.newInstance(taskId = task.taskId, taskTitle = task.taskTitle).show(parentFragmentManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.lock
|
||||||
|
|
||||||
|
import io.timelimit.android.data.model.ChildTask
|
||||||
|
|
||||||
|
sealed class LockTaskItem {
|
||||||
|
object Introduction: LockTaskItem()
|
||||||
|
data class Task(val task: ChildTask): LockTaskItem()
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.lock
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.extensions.showSafe
|
||||||
|
|
||||||
|
class TaskReviewPendingDialogFragment: DialogFragment() {
|
||||||
|
companion object {
|
||||||
|
private const val DIALOG_TAG = "TaskReviewPendingDialogFragment"
|
||||||
|
|
||||||
|
fun newInstance() = TaskReviewPendingDialogFragment()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(requireContext(), theme)
|
||||||
|
.setMessage(R.string.lock_task_review_pending_dialog)
|
||||||
|
.setPositiveButton(R.string.generic_ok, null)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||||
|
}
|
|
@ -25,7 +25,6 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.async.Threads
|
|
||||||
import io.timelimit.android.data.extensions.mapToTimezone
|
import io.timelimit.android.data.extensions.mapToTimezone
|
||||||
import io.timelimit.android.databinding.FragmentCategorySettingsBinding
|
import io.timelimit.android.databinding.FragmentCategorySettingsBinding
|
||||||
import io.timelimit.android.date.DateInTimezone
|
import io.timelimit.android.date.DateInTimezone
|
||||||
|
@ -39,7 +38,7 @@ import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
import io.timelimit.android.ui.manage.category.settings.addusedtime.AddUsedTimeDialogFragment
|
import io.timelimit.android.ui.manage.category.settings.addusedtime.AddUsedTimeDialogFragment
|
||||||
import io.timelimit.android.ui.manage.category.settings.networks.ManageCategoryNetworksView
|
import io.timelimit.android.ui.manage.category.settings.networks.ManageCategoryNetworksView
|
||||||
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
||||||
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
import io.timelimit.android.ui.util.bind
|
||||||
|
|
||||||
class CategorySettingsFragment : Fragment() {
|
class CategorySettingsFragment : Fragment() {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -210,16 +209,8 @@ class CategorySettingsFragment : Fragment() {
|
||||||
binding.extraTimeSelection.enablePickerMode(it)
|
binding.extraTimeSelection.enablePickerMode(it)
|
||||||
})
|
})
|
||||||
|
|
||||||
binding.extraTimeSelection.listener = object: SelectTimeSpanViewListener {
|
binding.extraTimeSelection.bind(appLogic.database, viewLifecycleOwner) {
|
||||||
override fun onTimeSpanChanged(newTimeInMillis: Long) {
|
updateEditExtraTimeConfirmButtonVisibility()
|
||||||
updateEditExtraTimeConfirmButtonVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setEnablePickerMode(enable: Boolean) {
|
|
||||||
Threads.database.execute {
|
|
||||||
appLogic.database.config().setEnableAlternativeDurationSelectionSync(enable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.switchLimitExtraTimeToToday.setOnCheckedChangeListener { _, _ ->
|
binding.switchLimitExtraTimeToToday.setOnCheckedChangeListener { _, _ ->
|
||||||
|
|
|
@ -123,10 +123,10 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment(), DurationPic
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
view.applyToExtraTime = newRule.applyToExtraTimeUsage
|
view.applyToExtraTime = newRule.applyToExtraTimeUsage
|
||||||
view.timeSpan.timeInMillis = newRule.maximumTimeInMillis.toLong()
|
|
||||||
|
|
||||||
val affectedDays = Math.max(0, (0..6).map { (newRule.dayMask.toInt() shr it) and 1 }.sum())
|
val affectedDays = Math.max(0, (0..6).map { (newRule.dayMask.toInt() shr it) and 1 }.sum())
|
||||||
view.timeSpan.maxDays = Math.max(0, affectedDays - 1) // max prevents crash
|
view.timeSpan.maxDays = Math.max(0, affectedDays - 1) // max prevents crash
|
||||||
|
view.timeSpan.timeInMillis = newRule.maximumTimeInMillis.toLong()
|
||||||
view.affectsMultipleDays = affectedDays >= 2
|
view.affectsMultipleDays = affectedDays >= 2
|
||||||
|
|
||||||
view.applyToWholeDay = newRule.appliesToWholeDay
|
view.applyToWholeDay = newRule.appliesToWholeDay
|
||||||
|
|
|
@ -89,6 +89,14 @@ class ManageChildFragment : ChildFragmentWrapper(), FragmentWithCustomTitle {
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.menu_manage_child_tasks -> {
|
||||||
|
navigation.safeNavigate(
|
||||||
|
ManageChildFragmentDirections.actionManageChildFragmentToManageChildTasksFragment(childId = childId),
|
||||||
|
R.id.manageChildFragment
|
||||||
|
)
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import io.timelimit.android.R
|
||||||
import io.timelimit.android.data.model.Category
|
import io.timelimit.android.data.model.Category
|
||||||
import io.timelimit.android.databinding.AddItemViewBinding
|
import io.timelimit.android.databinding.AddItemViewBinding
|
||||||
import io.timelimit.android.databinding.CategoryRichCardBinding
|
import io.timelimit.android.databinding.CategoryRichCardBinding
|
||||||
|
import io.timelimit.android.databinding.IntroCardBinding
|
||||||
import io.timelimit.android.ui.util.DateUtil
|
import io.timelimit.android.ui.util.DateUtil
|
||||||
import io.timelimit.android.util.TimeTextUtil
|
import io.timelimit.android.util.TimeTextUtil
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
|
@ -91,8 +92,10 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
|
||||||
|
|
||||||
TYPE_INTRO ->
|
TYPE_INTRO ->
|
||||||
IntroViewHolder(
|
IntroViewHolder(
|
||||||
LayoutInflater.from(parent.context)
|
IntroCardBinding.inflate(LayoutInflater.from(parent.context), parent, false).also {
|
||||||
.inflate(R.layout.category_list_intro, parent, false)
|
it.title = parent.context.getString(R.string.manage_child_categories_intro_title)
|
||||||
|
it.text = parent.context.getString(R.string.manage_child_categories_intro_text)
|
||||||
|
}.root
|
||||||
)
|
)
|
||||||
|
|
||||||
TYPE_MANIPULATION_WARNING ->
|
TYPE_MANIPULATION_WARNING ->
|
||||||
|
|
|
@ -42,7 +42,7 @@ import io.timelimit.android.ui.manage.child.ManageChildFragmentArgs
|
||||||
import io.timelimit.android.ui.manage.child.ManageChildFragmentDirections
|
import io.timelimit.android.ui.manage.child.ManageChildFragmentDirections
|
||||||
import io.timelimit.android.ui.manage.child.category.create.CreateCategoryDialogFragment
|
import io.timelimit.android.ui.manage.child.category.create.CreateCategoryDialogFragment
|
||||||
import io.timelimit.android.ui.manage.child.category.specialmode.SetCategorySpecialModeFragment
|
import io.timelimit.android.ui.manage.child.category.specialmode.SetCategorySpecialModeFragment
|
||||||
import kotlinx.android.synthetic.main.fragment_manage_child_categories.*
|
import kotlinx.android.synthetic.main.recycler_fragment.*
|
||||||
|
|
||||||
class ManageChildCategoriesFragment : Fragment() {
|
class ManageChildCategoriesFragment : Fragment() {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -57,7 +57,7 @@ class ManageChildCategoriesFragment : Fragment() {
|
||||||
private val model: ManageChildCategoriesModel by viewModels()
|
private val model: ManageChildCategoriesModel by viewModels()
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
return inflater.inflate(R.layout.fragment_manage_child_categories, container, false)
|
return inflater.inflate(R.layout.recycler_fragment, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.manage.child.tasks
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.model.ChildTask
|
||||||
|
import io.timelimit.android.databinding.AddItemViewBinding
|
||||||
|
import io.timelimit.android.databinding.ChildTaskItemBinding
|
||||||
|
import io.timelimit.android.databinding.IntroCardBinding
|
||||||
|
import io.timelimit.android.ui.util.DateUtil
|
||||||
|
import io.timelimit.android.util.TimeTextUtil
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
class ChildTaskAdapter: RecyclerView.Adapter<ChildTaskAdapter.Holder>() {
|
||||||
|
companion object {
|
||||||
|
private const val TYPE_ADD = 1
|
||||||
|
private const val TYPE_INTRO = 2
|
||||||
|
private const val TYPE_TASK = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
var data: List<ChildTaskItem> by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() }
|
||||||
|
var listener: Listener? = null
|
||||||
|
|
||||||
|
init { setHasStableIds(true) }
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = data.size
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long = data[position].let { item ->
|
||||||
|
if (item is ChildTaskItem.Task) item.taskItem.taskId.hashCode() else item.hashCode()
|
||||||
|
}.toLong()
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int = when (data[position]) {
|
||||||
|
ChildTaskItem.Add -> TYPE_ADD
|
||||||
|
ChildTaskItem.Intro -> TYPE_INTRO
|
||||||
|
is ChildTaskItem.Task -> TYPE_TASK
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder = LayoutInflater.from(parent.context).let { inflater ->
|
||||||
|
Holder(when (viewType) {
|
||||||
|
TYPE_ADD -> AddItemViewBinding.inflate(inflater, parent, false).also {
|
||||||
|
it.label = parent.context.getString(R.string.manage_child_tasks_add)
|
||||||
|
it.root.setOnClickListener { listener?.onAddClicked() }
|
||||||
|
}.root
|
||||||
|
TYPE_INTRO -> IntroCardBinding.inflate(inflater, parent, false).also {
|
||||||
|
it.title = parent.context.getString(R.string.manage_child_tasks)
|
||||||
|
it.text = parent.context.getString(R.string.manage_child_tasks_intro)
|
||||||
|
}.root
|
||||||
|
TYPE_TASK -> ChildTaskItemBinding.inflate(inflater, parent, false).also {
|
||||||
|
it.root.tag = it
|
||||||
|
}.root
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||||
|
val item = data[position]
|
||||||
|
|
||||||
|
if (item is ChildTaskItem.Task) {
|
||||||
|
val context = holder.itemView.context
|
||||||
|
val binding = holder.itemView.tag as ChildTaskItemBinding
|
||||||
|
|
||||||
|
binding.title = item.taskItem.taskTitle
|
||||||
|
binding.category = item.categoryTitle
|
||||||
|
binding.duration = TimeTextUtil.time(item.taskItem.extraTimeDuration, context)
|
||||||
|
binding.lastGrant = item.taskItem.lastGrantTimestamp.let { time ->
|
||||||
|
if (time == 0L) null else DateUtil.formatAbsoluteDate(context, time)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.executePendingBindings()
|
||||||
|
|
||||||
|
binding.root.setOnClickListener { listener?.onTaskClicked(item.taskItem) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder(view: View): RecyclerView.ViewHolder(view)
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onAddClicked()
|
||||||
|
fun onTaskClicked(task: ChildTask)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.manage.child.tasks
|
||||||
|
|
||||||
|
import io.timelimit.android.data.model.ChildTask
|
||||||
|
|
||||||
|
sealed class ChildTaskItem {
|
||||||
|
object Add: ChildTaskItem()
|
||||||
|
object Intro: ChildTaskItem()
|
||||||
|
class Task(val taskItem: ChildTask, val categoryTitle: String): ChildTaskItem()
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.manage.child.tasks
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.data.model.HintsToShow
|
||||||
|
import io.timelimit.android.livedata.map
|
||||||
|
import io.timelimit.android.livedata.switchMap
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
|
||||||
|
class ChildTaskModel (application: Application): AndroidViewModel(application) {
|
||||||
|
private val logic = DefaultAppLogic.with(application)
|
||||||
|
private val childIdLive = MutableLiveData<String>()
|
||||||
|
private val data = childIdLive.switchMap { childId -> logic.database.childTasks().getTasksByUserIdWithCategoryTitlesLive(userId = childId) }
|
||||||
|
private val dataListItemsLive: LiveData<List<ChildTaskItem>> = data.map { items -> items.map { ChildTaskItem.Task(it.childTask, it.categoryTitle) } }
|
||||||
|
private val didHideIntroductionLive = logic.database.config().wereHintsShown(HintsToShow.TASKS_INTRODUCTION)
|
||||||
|
private var didInit = false
|
||||||
|
|
||||||
|
val listContent = didHideIntroductionLive.switchMap { didHideIntroduction ->
|
||||||
|
dataListItemsLive.map { dataListItems ->
|
||||||
|
if (didHideIntroduction)
|
||||||
|
dataListItems + listOf(ChildTaskItem.Add)
|
||||||
|
else
|
||||||
|
listOf(ChildTaskItem.Intro) + dataListItems + listOf(ChildTaskItem.Add)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun init(childId: String) {
|
||||||
|
if (didInit) return
|
||||||
|
|
||||||
|
didInit = true
|
||||||
|
|
||||||
|
childIdLive.value = childId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideIntro() {
|
||||||
|
Threads.database.submit { logic.database.config().setHintsShownSync(HintsToShow.TASKS_INTRODUCTION) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.manage.child.tasks
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.coroutines.runAsync
|
||||||
|
import io.timelimit.android.sync.actions.MarkTaskPendingAction
|
||||||
|
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||||
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
|
|
||||||
|
class ConfirmTaskDialogFragment: DialogFragment() {
|
||||||
|
companion object {
|
||||||
|
private const val DIALOG_TAG = "ConfirmTaskDialogFragment"
|
||||||
|
private const val TASK_TITLE = "taskTitle"
|
||||||
|
private const val TASK_ID = "taskId"
|
||||||
|
|
||||||
|
fun newInstance(taskId: String, taskTitle: String) = ConfirmTaskDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString(TASK_ID, taskId)
|
||||||
|
putString(TASK_TITLE, taskTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val taskId = requireArguments().getString(TASK_ID)!!
|
||||||
|
val taskTitle = requireArguments().getString(TASK_TITLE)!!
|
||||||
|
val logic = getActivityViewModel(requireActivity()).logic
|
||||||
|
|
||||||
|
return AlertDialog.Builder(requireContext(), theme)
|
||||||
|
.setTitle(taskTitle)
|
||||||
|
.setMessage(R.string.lock_task_confirm_dialog)
|
||||||
|
.setNegativeButton(R.string.generic_no, null)
|
||||||
|
.setPositiveButton(R.string.generic_yes) { _, _ ->
|
||||||
|
runAsync {
|
||||||
|
ApplyActionUtil.applyAppLogicAction(
|
||||||
|
action = MarkTaskPendingAction(taskId = taskId),
|
||||||
|
appLogic = logic,
|
||||||
|
ignoreIfDeviceIsNotConfigured = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(fragmentManager: FragmentManager) = show(fragmentManager, DIALOG_TAG)
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.manage.child.tasks
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import io.timelimit.android.extensions.showSafe
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.ui.fragment.BottomSheetSelectionListDialog
|
||||||
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
|
|
||||||
|
class EditTaskCategoryDialogFragment: BottomSheetSelectionListDialog() {
|
||||||
|
companion object {
|
||||||
|
private const val DIALOG_TAG = "EditTaskCategoryDialogFragment"
|
||||||
|
private const val CHILD_ID = "childId"
|
||||||
|
private const val CATEGORY_ID = "categoryId"
|
||||||
|
|
||||||
|
fun newInstance(childId: String, categoryId: String?, target: Fragment) = EditTaskCategoryDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString(CHILD_ID, childId)
|
||||||
|
if (categoryId != null) putString(CATEGORY_ID, categoryId)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTargetFragment(target, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val title: String? = null
|
||||||
|
private val listener: Listener get() = targetFragment as Listener
|
||||||
|
private val auth get() = getActivityViewModel(requireActivity())
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val database = DefaultAppLogic.with(requireContext()).database
|
||||||
|
val childId = requireArguments().getString(CHILD_ID)!!
|
||||||
|
val currentCategoryId = if (requireArguments().containsKey(CATEGORY_ID)) requireArguments().getString(CATEGORY_ID) else null
|
||||||
|
|
||||||
|
database.user().getChildUserByIdLive(childId).observe(viewLifecycleOwner) {
|
||||||
|
if (it == null) dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
|
||||||
|
auth.authenticatedUser.observe(viewLifecycleOwner) {
|
||||||
|
if (it == null) dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
|
||||||
|
database.category().getCategoriesByChildId(childId).observe(viewLifecycleOwner) { categories ->
|
||||||
|
clearList()
|
||||||
|
|
||||||
|
categories.forEach { category ->
|
||||||
|
addListItem(
|
||||||
|
label = category.title,
|
||||||
|
checked = category.id == currentCategoryId,
|
||||||
|
click = {
|
||||||
|
listener.onCategorySelected(category.id)
|
||||||
|
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onCategorySelected(categoryId: String)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.manage.child.tasks
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.InputFilter
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.model.ChildTask
|
||||||
|
import io.timelimit.android.databinding.EditTaskFragmentBinding
|
||||||
|
import io.timelimit.android.extensions.addOnTextChangedListener
|
||||||
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
|
import io.timelimit.android.ui.util.bind
|
||||||
|
|
||||||
|
class EditTaskDialogFragment: BottomSheetDialogFragment(), EditTaskCategoryDialogFragment.Listener {
|
||||||
|
companion object {
|
||||||
|
private const val DIALOG_TAG = "EditTaskDialogFragment"
|
||||||
|
|
||||||
|
private const val CHILD_ID = "childId"
|
||||||
|
private const val TASK_ID = "taskId"
|
||||||
|
|
||||||
|
fun newInstance(childId: String, taskId: String?, listener: Fragment) = EditTaskDialogFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString(CHILD_ID, childId)
|
||||||
|
if (taskId != null) putString(TASK_ID, taskId)
|
||||||
|
|
||||||
|
setTargetFragment(listener, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val auth get() = getActivityViewModel(requireActivity())
|
||||||
|
private val model by viewModels<EditTaskModel>()
|
||||||
|
private val target get() = targetFragment as Listener
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
val binding = EditTaskFragmentBinding.inflate(inflater, container, false)
|
||||||
|
val args = requireArguments()
|
||||||
|
val childId = args.getString(CHILD_ID)!!
|
||||||
|
val taskId = if (args.containsKey(TASK_ID)) args.getString(TASK_ID) else null
|
||||||
|
|
||||||
|
model.init(childId = childId, taskId = taskId)
|
||||||
|
|
||||||
|
binding.isNewTask = taskId == null
|
||||||
|
|
||||||
|
binding.taskTitle.filters = arrayOf(InputFilter.LengthFilter(ChildTask.MAX_TASK_TITLE_LENGTH))
|
||||||
|
|
||||||
|
binding.taskTitle.addOnTextChangedListener {
|
||||||
|
val value = binding.taskTitle.text.toString()
|
||||||
|
|
||||||
|
if (model.taskTitleLive.value != value) model.taskTitleLive.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
model.taskTitleLive.observe(viewLifecycleOwner) { value ->
|
||||||
|
if (value != binding.taskTitle.text.toString()) binding.taskTitle.setText(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
model.selectedCategoryTitle.observe(viewLifecycleOwner) { categoryTitle ->
|
||||||
|
binding.categoryDropdown.text = categoryTitle ?: getString(R.string.manage_child_tasks_select_category)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.categoryDropdown.setOnClickListener {
|
||||||
|
EditTaskCategoryDialogFragment.newInstance(childId = childId, categoryId = model.categoryIdLive.value, target = this).show(parentFragmentManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.timespan.bind(model.logic.database, viewLifecycleOwner) {
|
||||||
|
if (model.durationLive.value != it) model.durationLive.value = it
|
||||||
|
}
|
||||||
|
|
||||||
|
model.durationLive.observe(viewLifecycleOwner) {
|
||||||
|
if (it != binding.timespan.timeInMillis) binding.timespan.timeInMillis = it
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.confirmButton.isEnabled = false
|
||||||
|
model.valid.observe(viewLifecycleOwner) { binding.confirmButton.isEnabled = it }
|
||||||
|
|
||||||
|
model.shouldClose.observe(viewLifecycleOwner) { if (it) dismissAllowingStateLoss() }
|
||||||
|
model.isBusy.observe(viewLifecycleOwner) { binding.flipper.displayedChild = if (it) 1 else 0 }
|
||||||
|
|
||||||
|
binding.deleteButton.setOnClickListener { model.deleteRule(auth) { target.onTaskRemoved(it) } }
|
||||||
|
binding.confirmButton.setOnClickListener { model.saveRule(auth); target.onTaskSaved() }
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
auth.authenticatedUser.observe(this) {
|
||||||
|
if (it == null) dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCategorySelected(categoryId: String) { model.categoryIdLive.value = categoryId }
|
||||||
|
|
||||||
|
fun show(fragmentManager: FragmentManager) = show(fragmentManager, DIALOG_TAG)
|
||||||
|
|
||||||
|
interface Listener {
|
||||||
|
fun onTaskRemoved(task: ChildTask)
|
||||||
|
fun onTaskSaved()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.manage.child.tasks
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.coroutines.runAsync
|
||||||
|
import io.timelimit.android.data.IdGenerator
|
||||||
|
import io.timelimit.android.data.model.ChildTask
|
||||||
|
import io.timelimit.android.livedata.*
|
||||||
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.sync.actions.DeleteChildTaskAction
|
||||||
|
import io.timelimit.android.sync.actions.UpdateChildTaskAction
|
||||||
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
|
import java.lang.IllegalArgumentException
|
||||||
|
|
||||||
|
class EditTaskModel(application: Application): AndroidViewModel(application) {
|
||||||
|
val logic = DefaultAppLogic.with(application)
|
||||||
|
|
||||||
|
private var didInit = false
|
||||||
|
private var originalTask: ChildTask? = null
|
||||||
|
private val childIdLive = MutableLiveData<String>()
|
||||||
|
private val taskIdLive = MutableLiveData<String?>()
|
||||||
|
private val isBusyInternal = MutableLiveData<Boolean>().apply { value = true }
|
||||||
|
private val shouldCloseInternal = MutableLiveData<Boolean>().apply { value = false }
|
||||||
|
private val isMissingTask = taskIdLive.switchMap { taskId ->
|
||||||
|
if (taskId == null) liveDataFromValue(false)
|
||||||
|
else logic.database.childTasks().getTaskByTaskIdLive(taskId).map { it == null}
|
||||||
|
}
|
||||||
|
val categoryIdLive = MutableLiveData<String?>()
|
||||||
|
val taskTitleLive = MutableLiveData<String>()
|
||||||
|
val durationLive = MutableLiveData<Long>()
|
||||||
|
val isBusy = isBusyInternal.or(isMissingTask)
|
||||||
|
val shouldClose = shouldCloseInternal.castDown()
|
||||||
|
|
||||||
|
private val selectedCategory = childIdLive.switchMap { childId ->
|
||||||
|
categoryIdLive.switchMap { categoryId ->
|
||||||
|
if (categoryId != null)
|
||||||
|
logic.database.category().getCategoryByChildIdAndId(childId = childId, categoryId = categoryId)
|
||||||
|
else
|
||||||
|
liveDataFromValue(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectedCategoryTitle = selectedCategory.map { it?.title }
|
||||||
|
|
||||||
|
private val validCategory = selectedCategory.map { it != null }
|
||||||
|
private val validTitle = taskTitleLive.map { it.isNotBlank() && it.length <= ChildTask.MAX_TASK_TITLE_LENGTH }
|
||||||
|
private val durationValid = durationLive.map { it > 0 && it <= ChildTask.MAX_EXTRA_TIME }
|
||||||
|
val valid = validCategory.and(validTitle).and(durationValid)
|
||||||
|
|
||||||
|
fun init(childId: String, taskId: String?) {
|
||||||
|
if (didInit) return; didInit = true
|
||||||
|
|
||||||
|
childIdLive.value = childId
|
||||||
|
taskIdLive.value = taskId
|
||||||
|
categoryIdLive.value = null
|
||||||
|
taskTitleLive.value = ""
|
||||||
|
durationLive.value = 1000L * 60 * 15
|
||||||
|
|
||||||
|
runAsync {
|
||||||
|
if (taskId != null) {
|
||||||
|
val task = logic.database.childTasks().getTaskByTaskIdCoroutine(taskId)
|
||||||
|
|
||||||
|
if (task != null) {
|
||||||
|
categoryIdLive.value = task.categoryId
|
||||||
|
taskTitleLive.value = task.taskTitle
|
||||||
|
durationLive.value = task.extraTimeDuration.toLong()
|
||||||
|
|
||||||
|
originalTask = task
|
||||||
|
} else {
|
||||||
|
shouldCloseInternal.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isBusyInternal.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteRule(auth: ActivityViewModel, onTaskRemoved: (ChildTask) -> Unit) {
|
||||||
|
val taskId = taskIdLive.value
|
||||||
|
val oldTask = originalTask
|
||||||
|
|
||||||
|
if (taskId != null && oldTask != null) {
|
||||||
|
isBusyInternal.value = true
|
||||||
|
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
DeleteChildTaskAction(taskId = taskId)
|
||||||
|
)
|
||||||
|
|
||||||
|
onTaskRemoved(oldTask)
|
||||||
|
|
||||||
|
shouldCloseInternal.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveRule(auth: ActivityViewModel) {
|
||||||
|
isBusyInternal.value = true
|
||||||
|
|
||||||
|
val taskId = taskIdLive.value
|
||||||
|
val categoryId = categoryIdLive.value ?: return
|
||||||
|
val duration = durationLive.value ?: return
|
||||||
|
val taskTitle = taskTitleLive.value ?: return
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (taskId == null) {
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
UpdateChildTaskAction(
|
||||||
|
taskId = IdGenerator.generateId(),
|
||||||
|
categoryId = categoryId,
|
||||||
|
extraTimeDuration = duration.toInt(),
|
||||||
|
isNew = true,
|
||||||
|
taskTitle = taskTitle
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
UpdateChildTaskAction(
|
||||||
|
taskId = taskId,
|
||||||
|
categoryId = categoryId,
|
||||||
|
extraTimeDuration = duration.toInt(),
|
||||||
|
isNew = false,
|
||||||
|
taskTitle = taskTitle
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (ex: IllegalArgumentException) {
|
||||||
|
Toast.makeText(getApplication(), R.string.error_general, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldCloseInternal.value = true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.manage.child.tasks
|
||||||
|
|
||||||
|
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.recyclerview.widget.ItemTouchHelper
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import io.timelimit.android.R
|
||||||
|
import io.timelimit.android.data.model.ChildTask
|
||||||
|
import io.timelimit.android.sync.actions.UpdateChildTaskAction
|
||||||
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
|
import kotlinx.android.synthetic.main.recycler_fragment.*
|
||||||
|
|
||||||
|
class ManageChildTasksFragment: Fragment(), EditTaskDialogFragment.Listener {
|
||||||
|
companion object {
|
||||||
|
private const val CHILD_ID = "childId"
|
||||||
|
|
||||||
|
fun newInstance(childId: String) = ManageChildTasksFragment().apply {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString(CHILD_ID, childId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val childId get() = requireArguments().getString(CHILD_ID)!!
|
||||||
|
private val auth get() = getActivityViewModel(requireActivity())
|
||||||
|
private val model: ChildTaskModel by viewModels()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
model.init(childId = childId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.recycler_fragment, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
val adapter = ChildTaskAdapter()
|
||||||
|
|
||||||
|
recycler.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
recycler.adapter = adapter
|
||||||
|
|
||||||
|
model.listContent.observe(viewLifecycleOwner) { adapter.data = it }
|
||||||
|
|
||||||
|
adapter.listener = object: ChildTaskAdapter.Listener {
|
||||||
|
override fun onAddClicked() {
|
||||||
|
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||||
|
EditTaskDialogFragment.newInstance(childId = childId, taskId = null, listener = this@ManageChildTasksFragment).show(parentFragmentManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTaskClicked(task: ChildTask) {
|
||||||
|
if (auth.requestAuthenticationOrReturnTrue()) {
|
||||||
|
EditTaskDialogFragment.newInstance(childId = childId, taskId = task.taskId, listener = this@ManageChildTasksFragment).show(parentFragmentManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemTouchHelper(object: ItemTouchHelper.SimpleCallback(0, 0) {
|
||||||
|
override fun getSwipeDirs(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||||
|
val index = viewHolder.adapterPosition
|
||||||
|
val item = if (index == RecyclerView.NO_POSITION) null else adapter.data[index]
|
||||||
|
|
||||||
|
return if (item == ChildTaskItem.Intro) {
|
||||||
|
ItemTouchHelper.START or ItemTouchHelper.END
|
||||||
|
} else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { model.hideIntro() }
|
||||||
|
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean = throw IllegalStateException()
|
||||||
|
}).attachToRecyclerView(recycler)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTaskRemoved(task: ChildTask) {
|
||||||
|
Snackbar.make(requireView(), R.string.manage_child_tasks_toast_removed, Snackbar.LENGTH_SHORT)
|
||||||
|
.setAction(R.string.generic_undo) {
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
UpdateChildTaskAction(
|
||||||
|
isNew = true,
|
||||||
|
taskId = task.taskId,
|
||||||
|
taskTitle = task.taskTitle,
|
||||||
|
extraTimeDuration = task.extraTimeDuration,
|
||||||
|
categoryId = task.categoryId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTaskSaved() {
|
||||||
|
Snackbar.make(requireView(), R.string.manage_child_tasks_toast_saved, Snackbar.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,21 +19,18 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.lifecycle.Observer
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.ViewModelProviders
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.async.Threads
|
import io.timelimit.android.async.Threads
|
||||||
import io.timelimit.android.coroutines.CoroutineFragment
|
import io.timelimit.android.coroutines.CoroutineFragment
|
||||||
import io.timelimit.android.data.model.Device
|
import io.timelimit.android.data.model.*
|
||||||
import io.timelimit.android.data.model.HintsToShow
|
|
||||||
import io.timelimit.android.data.model.User
|
|
||||||
import io.timelimit.android.data.model.UserType
|
|
||||||
import io.timelimit.android.livedata.waitForNonNullValue
|
import io.timelimit.android.livedata.waitForNonNullValue
|
||||||
import io.timelimit.android.logic.AppLogic
|
import io.timelimit.android.logic.AppLogic
|
||||||
import io.timelimit.android.logic.DefaultAppLogic
|
import io.timelimit.android.logic.DefaultAppLogic
|
||||||
|
import io.timelimit.android.sync.actions.ReviewChildTaskAction
|
||||||
import io.timelimit.android.ui.main.ActivityViewModel
|
import io.timelimit.android.ui.main.ActivityViewModel
|
||||||
import io.timelimit.android.ui.main.getActivityViewModel
|
import io.timelimit.android.ui.main.getActivityViewModel
|
||||||
import kotlinx.android.synthetic.main.fragment_overview.*
|
import kotlinx.android.synthetic.main.fragment_overview.*
|
||||||
|
@ -41,11 +38,9 @@ import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class OverviewFragment : CoroutineFragment() {
|
class OverviewFragment : CoroutineFragment() {
|
||||||
private val handlers: OverviewFragmentParentHandlers by lazy { parentFragment as OverviewFragmentParentHandlers }
|
private val handlers: OverviewFragmentParentHandlers by lazy { parentFragment as OverviewFragmentParentHandlers }
|
||||||
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
private val logic: AppLogic by lazy { DefaultAppLogic.with(requireContext()) }
|
||||||
private val auth: ActivityViewModel by lazy { getActivityViewModel(activity!!) }
|
private val auth: ActivityViewModel by lazy { getActivityViewModel(requireActivity()) }
|
||||||
private val model: OverviewFragmentModel by lazy {
|
private val model: OverviewFragmentModel by viewModels()
|
||||||
ViewModelProviders.of(this).get(OverviewFragmentModel::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
return inflater.inflate(R.layout.fragment_overview, container, false)
|
return inflater.inflate(R.layout.fragment_overview, container, false)
|
||||||
|
@ -57,7 +52,7 @@ class OverviewFragment : CoroutineFragment() {
|
||||||
val adapter = OverviewFragmentAdapter()
|
val adapter = OverviewFragmentAdapter()
|
||||||
|
|
||||||
recycler.adapter = adapter
|
recycler.adapter = adapter
|
||||||
recycler.layoutManager = LinearLayoutManager(context!!)
|
recycler.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
|
||||||
adapter.handlers = object: OverviewFragmentHandlers {
|
adapter.handlers = object: OverviewFragmentHandlers {
|
||||||
override fun onAddUserClicked() {
|
override fun onAddUserClicked() {
|
||||||
|
@ -106,9 +101,33 @@ class OverviewFragment : CoroutineFragment() {
|
||||||
override fun onSetDeviceListVisibility(level: DeviceListItemVisibility) {
|
override fun onSetDeviceListVisibility(level: DeviceListItemVisibility) {
|
||||||
model.showMoreDevices(level)
|
model.showMoreDevices(level)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTaskConfirmed(task: ChildTask) {
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
ReviewChildTaskAction(
|
||||||
|
taskId = task.taskId,
|
||||||
|
ok = true,
|
||||||
|
time = logic.timeApi.getCurrentTimeInMillis()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTaskRejected(task: ChildTask) {
|
||||||
|
auth.tryDispatchParentAction(
|
||||||
|
ReviewChildTaskAction(
|
||||||
|
taskId = task.taskId,
|
||||||
|
ok = false,
|
||||||
|
time = logic.timeApi.getCurrentTimeInMillis()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSkipTaskReviewClicked(task: ChildTask) {
|
||||||
|
if (auth.requestAuthenticationOrReturnTrue()) model.hideTask(task.taskId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
model.listEntries.observe(this, Observer { adapter.data = it })
|
model.listEntries.observe(viewLifecycleOwner) { adapter.data = it }
|
||||||
|
|
||||||
ItemTouchHelper(
|
ItemTouchHelper(
|
||||||
object: ItemTouchHelper.Callback() {
|
object: ItemTouchHelper.Callback() {
|
||||||
|
|
|
@ -21,10 +21,13 @@ import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.timelimit.android.BuildConfig
|
import io.timelimit.android.BuildConfig
|
||||||
import io.timelimit.android.R
|
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.Device
|
||||||
import io.timelimit.android.data.model.User
|
import io.timelimit.android.data.model.User
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
import io.timelimit.android.databinding.*
|
import io.timelimit.android.databinding.*
|
||||||
|
import io.timelimit.android.ui.util.DateUtil
|
||||||
|
import io.timelimit.android.util.TimeTextUtil
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
class OverviewFragmentAdapter : RecyclerView.Adapter<OverviewFragmentViewHolder>() {
|
class OverviewFragmentAdapter : RecyclerView.Adapter<OverviewFragmentViewHolder>() {
|
||||||
|
@ -45,6 +48,7 @@ class OverviewFragmentAdapter : RecyclerView.Adapter<OverviewFragmentViewHolder>
|
||||||
return when (item) {
|
return when (item) {
|
||||||
is OverviewFragmentItemDevice -> "device ${item.device.id}".hashCode().toLong()
|
is OverviewFragmentItemDevice -> "device ${item.device.id}".hashCode().toLong()
|
||||||
is OverviewFragmentItemUser -> "user ${item.user.id}".hashCode().toLong()
|
is OverviewFragmentItemUser -> "user ${item.user.id}".hashCode().toLong()
|
||||||
|
is TaskReviewOverviewItem -> "task ${item.task.taskId}".hashCode().toLong()
|
||||||
else -> item.hashCode().toLong()
|
else -> item.hashCode().toLong()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,6 +74,7 @@ class OverviewFragmentAdapter : RecyclerView.Adapter<OverviewFragmentViewHolder>
|
||||||
is OverviewFragmentHeaderFinishSetup -> OverviewFragmentViewType.FinishSetup
|
is OverviewFragmentHeaderFinishSetup -> OverviewFragmentViewType.FinishSetup
|
||||||
is OverviewFragmentItemMessage -> OverviewFragmentViewType.ServerMessage
|
is OverviewFragmentItemMessage -> OverviewFragmentViewType.ServerMessage
|
||||||
is ShowMoreOverviewFragmentItem -> OverviewFragmentViewType.ShowMoreButton
|
is ShowMoreOverviewFragmentItem -> OverviewFragmentViewType.ShowMoreButton
|
||||||
|
is TaskReviewOverviewItem -> OverviewFragmentViewType.TaskReview
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) = getItemType(getItem(position)).ordinal
|
override fun getItemViewType(position: Int) = getItemType(getItem(position)).ordinal
|
||||||
|
@ -155,6 +160,10 @@ class OverviewFragmentAdapter : RecyclerView.Adapter<OverviewFragmentViewHolder>
|
||||||
.inflate(R.layout.show_more_list_item, parent, false)
|
.inflate(R.layout.show_more_list_item, parent, false)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
OverviewFragmentViewType.TaskReview.ordinal -> TaskReviewHolder(
|
||||||
|
FragmentOverviewTaskReviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
)
|
||||||
|
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,6 +250,23 @@ class OverviewFragmentAdapter : RecyclerView.Adapter<OverviewFragmentViewHolder>
|
||||||
}
|
}
|
||||||
}.let { }
|
}.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(item.task) }
|
||||||
|
it.noButton.setOnClickListener { handlers?.onTaskRejected(item.task) }
|
||||||
|
it.skipButton.setOnClickListener { handlers?.onSkipTaskReviewClicked(item.task) }
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.binding.executePendingBindings()
|
||||||
|
}
|
||||||
}.let { }
|
}.let { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -254,7 +280,8 @@ enum class OverviewFragmentViewType {
|
||||||
Introduction,
|
Introduction,
|
||||||
FinishSetup,
|
FinishSetup,
|
||||||
ServerMessage,
|
ServerMessage,
|
||||||
ShowMoreButton
|
ShowMoreButton,
|
||||||
|
TaskReview
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class OverviewFragmentViewHolder(view: View): RecyclerView.ViewHolder(view)
|
sealed class OverviewFragmentViewHolder(view: View): RecyclerView.ViewHolder(view)
|
||||||
|
@ -267,6 +294,7 @@ class IntroViewHolder(view: View): OverviewFragmentViewHolder(view)
|
||||||
class FinishSetupViewHolder(view: View): OverviewFragmentViewHolder(view)
|
class FinishSetupViewHolder(view: View): OverviewFragmentViewHolder(view)
|
||||||
class ServerMessageViewHolder(val binding: FragmentOverviewServerMessageBinding): OverviewFragmentViewHolder(binding.root)
|
class ServerMessageViewHolder(val binding: FragmentOverviewServerMessageBinding): OverviewFragmentViewHolder(binding.root)
|
||||||
class ShowMoreViewHolder(view: View): OverviewFragmentViewHolder(view)
|
class ShowMoreViewHolder(view: View): OverviewFragmentViewHolder(view)
|
||||||
|
class TaskReviewHolder(val binding: FragmentOverviewTaskReviewBinding): OverviewFragmentViewHolder(binding.root)
|
||||||
|
|
||||||
interface OverviewFragmentHandlers {
|
interface OverviewFragmentHandlers {
|
||||||
fun onAddUserClicked()
|
fun onAddUserClicked()
|
||||||
|
@ -276,4 +304,7 @@ interface OverviewFragmentHandlers {
|
||||||
fun onFinishSetupClicked()
|
fun onFinishSetupClicked()
|
||||||
fun onShowAllUsersClicked()
|
fun onShowAllUsersClicked()
|
||||||
fun onSetDeviceListVisibility(level: DeviceListItemVisibility)
|
fun onSetDeviceListVisibility(level: DeviceListItemVisibility)
|
||||||
|
fun onSkipTaskReviewClicked(task: ChildTask)
|
||||||
|
fun onTaskConfirmed(task: ChildTask)
|
||||||
|
fun onTaskRejected(task: ChildTask)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package io.timelimit.android.ui.overview.overview
|
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.Device
|
||||||
import io.timelimit.android.data.model.User
|
import io.timelimit.android.data.model.User
|
||||||
import io.timelimit.android.data.model.UserType
|
import io.timelimit.android.data.model.UserType
|
||||||
|
@ -40,3 +41,4 @@ sealed class ShowMoreOverviewFragmentItem: OverviewFragmentItem() {
|
||||||
object ShowAllUsers: ShowMoreOverviewFragmentItem()
|
object ShowAllUsers: ShowMoreOverviewFragmentItem()
|
||||||
data class ShowMoreDevices(val level: DeviceListItemVisibility): ShowMoreOverviewFragmentItem()
|
data class ShowMoreDevices(val level: DeviceListItemVisibility): ShowMoreOverviewFragmentItem()
|
||||||
}
|
}
|
||||||
|
data class TaskReviewOverviewItem(val task: ChildTask, val childTitle: String, val categoryTitle: String): OverviewFragmentItem()
|
|
@ -115,42 +115,63 @@ class OverviewFragmentModel(application: Application): AndroidViewModel(applicat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 pendingTaskItemLive = pendingTasksToShowLive.map { tasks ->
|
||||||
|
tasks.firstOrNull()?.let {
|
||||||
|
TaskReviewOverviewItem(task = it.childTask, childTitle = it.childName, categoryTitle = it.categoryTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideTask(taskId: String) {
|
||||||
|
hiddenTaskIdsLive.value = (hiddenTaskIdsLive.value ?: emptySet()) + setOf(taskId)
|
||||||
|
}
|
||||||
|
|
||||||
val listEntries = introEntries.switchMap { introEntries ->
|
val listEntries = introEntries.switchMap { introEntries ->
|
||||||
deviceEntries.switchMap { deviceEntries ->
|
deviceEntries.switchMap { deviceEntries ->
|
||||||
userEntries.switchMap { userEntries ->
|
userEntries.switchMap { userEntries ->
|
||||||
itemVisibility.map { itemVisibility ->
|
pendingTaskItemLive.switchMap { pendingTaskItem ->
|
||||||
mutableListOf<OverviewFragmentItem>().apply {
|
itemVisibility.map { itemVisibility ->
|
||||||
addAll(introEntries)
|
mutableListOf<OverviewFragmentItem>().apply {
|
||||||
|
addAll(introEntries)
|
||||||
|
|
||||||
add(OverviewFragmentHeaderDevices)
|
if (pendingTaskItem != null) add(pendingTaskItem)
|
||||||
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 -> if (deviceEntries.find { it.deviceUser?.type == UserType.Child } != null)
|
|
||||||
DeviceListItemVisibility.AllChildDevices else DeviceListItemVisibility.AllDevices
|
|
||||||
DeviceListItemVisibility.AllChildDevices -> DeviceListItemVisibility.AllDevices
|
|
||||||
DeviceListItemVisibility.AllDevices -> DeviceListItemVisibility.AllDevices
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
add(OverviewFragmentHeaderUsers)
|
add(OverviewFragmentHeaderDevices)
|
||||||
if (itemVisibility.showParentUsers) {
|
val shownDevices = when (itemVisibility.devices) {
|
||||||
addAll(userEntries)
|
DeviceListItemVisibility.BareMinimum -> deviceEntries.filter { it.isCurrentDevice || it.isImportant }
|
||||||
add(OverviewFragmentActionAddUser)
|
DeviceListItemVisibility.AllChildDevices -> deviceEntries.filter { it.isCurrentDevice || it.isImportant || it.deviceUser?.type == UserType.Child }
|
||||||
} else {
|
DeviceListItemVisibility.AllDevices -> deviceEntries
|
||||||
userEntries.forEach { if (it.user.type != UserType.Parent) add(it) }
|
}
|
||||||
add(ShowMoreOverviewFragmentItem.ShowAllUsers)
|
addAll(shownDevices)
|
||||||
}
|
if (shownDevices.size == deviceEntries.size) {
|
||||||
}.toList()
|
add(OverviewFragmentActionAddDevice)
|
||||||
}
|
} else {
|
||||||
} as LiveData<List<OverviewFragmentItem>>
|
add(ShowMoreOverviewFragmentItem.ShowMoreDevices(when (itemVisibility.devices) {
|
||||||
|
DeviceListItemVisibility.BareMinimum -> if (deviceEntries.find { it.deviceUser?.type == UserType.Child } != null)
|
||||||
|
DeviceListItemVisibility.AllChildDevices else DeviceListItemVisibility.AllDevices
|
||||||
|
DeviceListItemVisibility.AllChildDevices -> DeviceListItemVisibility.AllDevices
|
||||||
|
DeviceListItemVisibility.AllDevices -> DeviceListItemVisibility.AllDevices
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
add(OverviewFragmentHeaderUsers)
|
||||||
|
if (itemVisibility.showParentUsers) {
|
||||||
|
addAll(userEntries)
|
||||||
|
add(OverviewFragmentActionAddUser)
|
||||||
|
} else {
|
||||||
|
userEntries.forEach { if (it.user.type != UserType.Parent) add(it) }
|
||||||
|
add(ShowMoreOverviewFragmentItem.ShowAllUsers)
|
||||||
|
}
|
||||||
|
}.toList()
|
||||||
|
}
|
||||||
|
} as LiveData<List<OverviewFragmentItem>>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.timelimit.android.ui.util
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import io.timelimit.android.async.Threads
|
||||||
|
import io.timelimit.android.data.Database
|
||||||
|
import io.timelimit.android.ui.view.SelectTimeSpanView
|
||||||
|
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
||||||
|
|
||||||
|
fun SelectTimeSpanView.bind(database: Database, lifecycleOwner: LifecycleOwner, listener: (Long) -> Unit) {
|
||||||
|
database.config().getEnableAlternativeDurationSelectionAsync().observe(lifecycleOwner) {
|
||||||
|
enablePickerMode(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listener = object: SelectTimeSpanViewListener {
|
||||||
|
override fun onTimeSpanChanged(newTimeInMillis: Long) { listener(timeInMillis) }
|
||||||
|
|
||||||
|
override fun setEnablePickerMode(enable: Boolean) {
|
||||||
|
Threads.database.execute {
|
||||||
|
database.config().setEnableAlternativeDurationSelectionSync(enable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* TimeLimit Copyright <C> 2019 Jonas Lochmann
|
* TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -19,33 +19,44 @@ import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.*
|
||||||
import android.widget.SeekBar
|
|
||||||
import io.timelimit.android.R
|
import io.timelimit.android.R
|
||||||
import io.timelimit.android.databinding.ViewSelectTimeSpanBinding
|
|
||||||
import io.timelimit.android.util.TimeTextUtil
|
import io.timelimit.android.util.TimeTextUtil
|
||||||
import kotlin.properties.Delegates
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
class SelectTimeSpanView(context: Context, attributeSet: AttributeSet? = null): FrameLayout(context, attributeSet) {
|
class SelectTimeSpanView(context: Context, attributeSet: AttributeSet? = null): FrameLayout(context, attributeSet) {
|
||||||
private val binding = ViewSelectTimeSpanBinding.inflate(LayoutInflater.from(context), this, false)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
addView(binding.root)
|
LayoutInflater.from(context).inflate(R.layout.view_select_time_span, this, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val seekbarContainer = findViewById<View>(R.id.seekbar_container)
|
||||||
|
private val pickerContainer = findViewById<View>(R.id.picker_container)
|
||||||
|
|
||||||
|
private val switchToPickerButton = findViewById<ImageButton>(R.id.switch_to_picker_button)
|
||||||
|
private val switchToSeekbarButton = findViewById<ImageButton>(R.id.switch_to_seekbar_button)
|
||||||
|
|
||||||
|
private val daysText = findViewById<TextView>(R.id.days_text)
|
||||||
|
private val dayPickerContainer = findViewById<View>(R.id.day_picker_container)
|
||||||
|
private val dayPicker = findViewById<NumberPicker>(R.id.day_picker)
|
||||||
|
private val daySeekbar = findViewById<SeekBar>(R.id.days_seek)
|
||||||
|
|
||||||
|
private val hoursText = findViewById<TextView>(R.id.hours_text)
|
||||||
|
private val hourPicker = findViewById<NumberPicker>(R.id.hour_picker)
|
||||||
|
private val hourSeekbar = findViewById<SeekBar>(R.id.hours_seek)
|
||||||
|
|
||||||
|
private val minutesText = findViewById<TextView>(R.id.minutes_text)
|
||||||
|
private val minutePicker = findViewById<NumberPicker>(R.id.minute_picker)
|
||||||
|
private val minuteSeekbar = findViewById<SeekBar>(R.id.minutes_seek)
|
||||||
|
|
||||||
var listener: SelectTimeSpanViewListener? = null
|
var listener: SelectTimeSpanViewListener? = null
|
||||||
|
|
||||||
var timeInMillis: Long by Delegates.observable(0L) { _, _, _ ->
|
var timeInMillis: Long by Delegates.observable(0L) { _, oldValue, newValue ->
|
||||||
bindTime()
|
if (oldValue != newValue) { bindTime() }
|
||||||
|
|
||||||
listener?.onTimeSpanChanged(timeInMillis)
|
listener?.onTimeSpanChanged(timeInMillis)
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxDays: Int by Delegates.observable(0) { _, _, _ ->
|
var maxDays: Int by Delegates.observable(0) { _, _, newValue -> bindMaxDays(newValue) }
|
||||||
binding.maxDays = maxDays
|
|
||||||
|
|
||||||
binding.dayPicker.maxValue = maxDays
|
|
||||||
binding.dayPickerContainer.visibility = if (maxDays > 0) View.VISIBLE else View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val attributes = context.obtainStyledAttributes(attributeSet, R.styleable.SelectTimeSpanView)
|
val attributes = context.obtainStyledAttributes(attributeSet, R.styleable.SelectTimeSpanView)
|
||||||
|
@ -56,77 +67,76 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet? = null):
|
||||||
attributes.recycle()
|
attributes.recycle()
|
||||||
|
|
||||||
bindTime()
|
bindTime()
|
||||||
|
enablePickerMode(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindMaxDays(newValue: Int) {
|
||||||
|
val multipleDays = newValue > 0
|
||||||
|
val vis = if (multipleDays) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
dayPicker.maxValue = newValue
|
||||||
|
daySeekbar.max = newValue
|
||||||
|
|
||||||
|
dayPickerContainer.visibility = vis
|
||||||
|
daysText.visibility = vis
|
||||||
|
daySeekbar.visibility = vis
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindTime() {
|
private fun bindTime() {
|
||||||
val totalMinutes = (timeInMillis / (1000 * 60)).toInt()
|
val duration = Duration.decode(timeInMillis)
|
||||||
val totalHours = totalMinutes / 60
|
|
||||||
val totalDays = totalHours / 24
|
|
||||||
val minutes = totalMinutes % 60
|
|
||||||
val hours = totalHours % 24
|
|
||||||
|
|
||||||
binding.days = totalDays
|
daysText.text = TimeTextUtil.days(duration.days, context!!)
|
||||||
binding.minutes = minutes
|
minutesText.text = TimeTextUtil.minutes(duration.minutes, context!!)
|
||||||
binding.hours = hours
|
hoursText.text = TimeTextUtil.hours(duration.hours, context!!)
|
||||||
|
|
||||||
binding.daysText = TimeTextUtil.days(totalDays, context!!)
|
minutePicker.value = duration.minutes
|
||||||
binding.minutesText = TimeTextUtil.minutes(minutes, context!!)
|
minuteSeekbar.progress = duration.minutes
|
||||||
binding.hoursText = TimeTextUtil.hours(hours, context!!)
|
|
||||||
|
|
||||||
binding.minutePicker.value = binding.minutes ?: 0
|
hourPicker.value = duration.hours
|
||||||
binding.hourPicker.value = binding.hours ?: 0
|
hourSeekbar.progress = duration.hours
|
||||||
binding.dayPicker.value = binding.days ?: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun readStatusFromBinding() {
|
dayPicker.value = duration.days
|
||||||
val days = binding.days!!.toLong()
|
daySeekbar.progress = duration.days
|
||||||
val hours = binding.hours!!.toLong()
|
|
||||||
val minutes = binding.minutes!!.toLong()
|
|
||||||
|
|
||||||
timeInMillis = (((days * 24) + hours) * 60 + minutes) * 1000 * 60
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearNumberPickerFocus() {
|
fun clearNumberPickerFocus() {
|
||||||
binding.minutePicker.clearFocus()
|
minutePicker.clearFocus()
|
||||||
binding.hourPicker.clearFocus()
|
hourPicker.clearFocus()
|
||||||
binding.dayPicker.clearFocus()
|
dayPicker.clearFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun enablePickerMode(enable: Boolean) {
|
fun enablePickerMode(enable: Boolean) {
|
||||||
binding.seekbarContainer.visibility = if (enable) View.GONE else View.VISIBLE
|
seekbarContainer.visibility = if (enable) View.GONE else View.VISIBLE
|
||||||
binding.pickerContainer.visibility = if (enable) View.VISIBLE else View.GONE
|
pickerContainer.visibility = if (enable) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
binding.minutePicker.minValue = 0
|
minutePicker.minValue = 0
|
||||||
binding.minutePicker.maxValue = 59
|
minutePicker.maxValue = 59
|
||||||
|
|
||||||
binding.hourPicker.minValue = 0
|
hourPicker.minValue = 0
|
||||||
binding.hourPicker.maxValue = 23
|
hourPicker.maxValue = 23
|
||||||
|
|
||||||
binding.dayPicker.minValue = 0
|
dayPicker.minValue = 0
|
||||||
binding.dayPicker.maxValue = 1
|
dayPicker.maxValue = 1
|
||||||
binding.dayPickerContainer.visibility = View.GONE
|
dayPickerContainer.visibility = View.GONE
|
||||||
|
|
||||||
binding.minutePicker.setOnValueChangedListener { _, _, newValue ->
|
minutePicker.setOnValueChangedListener { _, _, newValue ->
|
||||||
binding.minutes = newValue
|
timeInMillis = Duration.decode(timeInMillis).copy(minutes = newValue).timeInMillis
|
||||||
readStatusFromBinding()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.hourPicker.setOnValueChangedListener { _, _, newValue ->
|
hourPicker.setOnValueChangedListener { _, _, newValue ->
|
||||||
binding.hours = newValue
|
timeInMillis = Duration.decode(timeInMillis).copy(hours = newValue).timeInMillis
|
||||||
readStatusFromBinding()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.dayPicker.setOnValueChangedListener { _, _, newValue ->
|
dayPicker.setOnValueChangedListener { _, _, newValue ->
|
||||||
binding.days = newValue
|
timeInMillis = Duration.decode(timeInMillis).copy(days = newValue).timeInMillis
|
||||||
readStatusFromBinding()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.daysSeek.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
|
daySeekbar.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
|
||||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||||
binding.days = progress
|
timeInMillis = Duration.decode(timeInMillis).copy(days = progress).timeInMillis
|
||||||
readStatusFromBinding()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {
|
override fun onStartTrackingTouch(seekBar: SeekBar?) {
|
||||||
|
@ -138,10 +148,9 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet? = null):
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
binding.hoursSeek.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
|
hourSeekbar.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
|
||||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||||
binding.hours = progress
|
timeInMillis = Duration.decode(timeInMillis).copy(hours = progress).timeInMillis
|
||||||
readStatusFromBinding()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {
|
override fun onStartTrackingTouch(seekBar: SeekBar?) {
|
||||||
|
@ -153,10 +162,9 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet? = null):
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
binding.minutesSeek.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
|
minuteSeekbar.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
|
||||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||||
binding.minutes = progress
|
timeInMillis = Duration.decode(timeInMillis).copy(minutes = progress).timeInMillis
|
||||||
readStatusFromBinding()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {
|
override fun onStartTrackingTouch(seekBar: SeekBar?) {
|
||||||
|
@ -168,10 +176,26 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet? = null):
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
binding.pickerContainer.visibility = GONE
|
pickerContainer.visibility = GONE
|
||||||
|
|
||||||
binding.switchToPickerButton.setOnClickListener { listener?.setEnablePickerMode(true) }
|
switchToPickerButton.setOnClickListener { listener?.setEnablePickerMode(true) }
|
||||||
binding.switchToSeekbarButton.setOnClickListener { listener?.setEnablePickerMode(false) }
|
switchToSeekbarButton.setOnClickListener { listener?.setEnablePickerMode(false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class Duration (val days: Int, val hours: Int, val minutes: Int) {
|
||||||
|
companion object {
|
||||||
|
fun decode(timeInMillis: Long): Duration {
|
||||||
|
val totalMinutes = (timeInMillis / (1000 * 60)).toInt()
|
||||||
|
val totalHours = totalMinutes / 60
|
||||||
|
val totalDays = totalHours / 24
|
||||||
|
val minutes = totalMinutes % 60
|
||||||
|
val hours = totalHours % 24
|
||||||
|
|
||||||
|
return Duration(days = totalDays, hours = hours, minutes = minutes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val timeInMillis = ((((days * 24L) + hours) * 60 + minutes) * 1000 * 60)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M15.5,5.5c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM5,12c-2.8,0 -5,2.2 -5,5s2.2,5 5,5 5,-2.2 5,-5 -2.2,-5 -5,-5zM5,20.5c-1.9,0 -3.5,-1.6 -3.5,-3.5s1.6,-3.5 3.5,-3.5 3.5,1.6 3.5,3.5 -1.6,3.5 -3.5,3.5zM10.8,10.5l2.4,-2.4 0.8,0.8c1.3,1.3 3,2.1 5.1,2.1L19.1,9c-1.5,0 -2.7,-0.6 -3.6,-1.5l-1.9,-1.9c-0.5,-0.4 -1,-0.6 -1.6,-0.6s-1.1,0.2 -1.4,0.6L7.8,8.4c-0.4,0.4 -0.6,0.9 -0.6,1.4 0,0.6 0.2,1.1 0.6,1.4L11,14v5h2v-6.2l-2.2,-2.3zM19,12c-2.8,0 -5,2.2 -5,5s2.2,5 5,5 5,-2.2 5,-5 -2.2,-5 -5,-5zM19,20.5c-1.9,0 -3.5,-1.6 -3.5,-3.5s1.6,-3.5 3.5,-3.5 3.5,1.6 3.5,3.5 -1.6,3.5 -3.5,3.5z"/>
|
||||||
|
</vector>
|
|
@ -1,50 +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
|
|
||||||
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/manage_child_categories_intro_title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:textAppearance="?android:textAppearanceMedium"
|
|
||||||
android:text="@string/manage_child_categories_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>
|
|
96
app/src/main/res/layout/child_task_item.xml
Normal file
96
app/src/main/res/layout/child_task_item.xml
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
<?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="title"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="category"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="duration"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="pendingReview"
|
||||||
|
type="boolean" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="lastGrant"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
|
<import type="android.text.TextUtils" />
|
||||||
|
<import type="android.view.View" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:foreground="?selectableItemBackground"
|
||||||
|
android:id="@+id/card"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:cardUseCompatPadding="true">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:textAppearance="?android:textAppearanceLarge"
|
||||||
|
tools:text="Zimmer aufräumen"
|
||||||
|
android:text="@{title}"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<!-- hidden at the lock screen where only items of one category are shown -->
|
||||||
|
<TextView
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:visibility="@{category == null ? View.GONE : View.VISIBLE}"
|
||||||
|
tools:text="Erlaubte Spiele"
|
||||||
|
android:text="@{category}"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
tools:text="15 Minuten"
|
||||||
|
android:text="@{duration}"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:visibility="@{pendingReview ? View.VISIBLE : View.GONE}"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:text="@string/lock_task_review_pending_hint"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:visibility="@{TextUtils.isEmpty(lastGrant) ? View.GONE : View.VISIBLE}"
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
tools:text="@string/manage_child_task_last_grant"
|
||||||
|
android:text="@{@string/manage_child_task_last_grant(lastGrant)}"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
</layout>
|
113
app/src/main/res/layout/edit_task_fragment.xml
Normal file
113
app/src/main/res/layout/edit_task_fragment.xml
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
<?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:tools="http://schemas.android.com/tools">
|
||||||
|
<data>
|
||||||
|
<variable
|
||||||
|
name="isNewTask"
|
||||||
|
type="boolean" />
|
||||||
|
|
||||||
|
<import type="android.view.View" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<ViewFlipper
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/flipper">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:padding="8dp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
tools:text="@string/manage_child_tasks_edit"
|
||||||
|
android:text="@{isNewTask ? @string/manage_child_tasks_add : @string/manage_child_tasks_edit}"
|
||||||
|
android:textAppearance="?android:textAppearanceLarge"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/task_title"
|
||||||
|
tools:text="Zimmer aufräumen"
|
||||||
|
android:hint="@string/manage_child_tasks_title_hint"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/category_dropdown"
|
||||||
|
tools:text="@string/manage_child_tasks_select_category"
|
||||||
|
style="?borderlessButtonStyle"
|
||||||
|
android:drawableEnd="@drawable/ic_baseline_expand_more_24"
|
||||||
|
android:drawableTint="?colorOnSurface"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<io.timelimit.android.ui.view.SelectTimeSpanView
|
||||||
|
android:id="@+id/timespan"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:paddingStart="4dp"
|
||||||
|
android:paddingEnd="4dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="1px" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:visibility="@{isNewTask ? View.GONE : View.VISIBLE}"
|
||||||
|
android:id="@+id/delete_button"
|
||||||
|
style="?borderlessButtonStyle"
|
||||||
|
android:textColor="@color/text_red"
|
||||||
|
android:text="@string/generic_delete"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:id="@+id/confirm_button"
|
||||||
|
android:text="@string/generic_save"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<ProgressBar
|
||||||
|
style="?android:progressBarStyleLarge"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
</RelativeLayout>
|
||||||
|
</ViewFlipper>
|
||||||
|
</layout>
|
|
@ -1,26 +0,0 @@
|
||||||
<!--
|
|
||||||
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"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context="io.timelimit.android.ui.manage.child.category.ManageChildCategoriesFragment">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recycler"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
128
app/src/main/res/layout/fragment_overview_task_review.xml
Normal file
128
app/src/main/res/layout/fragment_overview_task_review.xml
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
<?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>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
</FrameLayout>
|
||||||
|
</layout>
|
71
app/src/main/res/layout/intro_card.xml
Normal file
71
app/src/main/res/layout/intro_card.xml
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<?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:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
<variable
|
||||||
|
name="title"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="text"
|
||||||
|
type="String" />
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="noSwipe"
|
||||||
|
type="boolean" />
|
||||||
|
|
||||||
|
<import type="android.view.View" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
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="@{title}"
|
||||||
|
tools:text="@string/manage_child_categories_intro_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:textAppearance="?android:textAppearanceMedium"
|
||||||
|
android:text="@{text}"
|
||||||
|
tools:text="@string/manage_child_categories_intro_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:visibility="@{noSwipe ? View.GONE : View.VISIBLE}"
|
||||||
|
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>
|
||||||
|
</layout>
|
20
app/src/main/res/layout/recycler_fragment.xml
Normal file
20
app/src/main/res/layout/recycler_fragment.xml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<!--
|
||||||
|
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/>.
|
||||||
|
-->
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:id="@+id/recycler" />
|
|
@ -13,191 +13,150 @@
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
<data>
|
android:orientation="vertical"
|
||||||
<variable
|
android:layout_width="match_parent"
|
||||||
name="minutes"
|
android:layout_height="wrap_content">
|
||||||
type="Integer" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="hours"
|
|
||||||
type="Integer" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="days"
|
|
||||||
type="Integer" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="maxDays"
|
|
||||||
type="Integer" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="minutesText"
|
|
||||||
type="String" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="hoursText"
|
|
||||||
type="String" />
|
|
||||||
|
|
||||||
<variable
|
|
||||||
name="daysText"
|
|
||||||
type="String" />
|
|
||||||
|
|
||||||
<import type="android.view.View" />
|
|
||||||
</data>
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:orientation="vertical"
|
android:id="@+id/seekbar_container"
|
||||||
|
android:orientation="horizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/seekbar_container"
|
android:layout_weight="1"
|
||||||
android:orientation="horizontal"
|
android:orientation="vertical"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<LinearLayout
|
<SeekBar
|
||||||
android:layout_weight="1"
|
android:id="@+id/minutes_seek"
|
||||||
android:orientation="vertical"
|
android:max="59"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<SeekBar
|
<TextView
|
||||||
android:id="@+id/minutes_seek"
|
android:id="@+id/minutes_text"
|
||||||
android:progress="@{safeUnbox(minutes)}"
|
tools:text="5 Minuten"
|
||||||
android:max="59"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content" />
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
<SeekBar
|
||||||
tools:text="5 Minuten"
|
android:id="@+id/hours_seek"
|
||||||
android:text="@{minutesText}"
|
android:max="23"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<SeekBar
|
<TextView
|
||||||
android:id="@+id/hours_seek"
|
tools:text="1 Stunde"
|
||||||
android:progress="@{safeUnbox(hours)}"
|
android:id="@+id/hours_text"
|
||||||
android:max="23"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content" />
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
<SeekBar
|
||||||
tools:text="1 Stunde"
|
android:id="@+id/days_seek"
|
||||||
android:text="@{hoursText}"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content" />
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<SeekBar
|
<TextView
|
||||||
android:visibility="@{safeUnbox(maxDays) > 0 ? View.VISIBLE : View.GONE}"
|
tools:text="2 Tage"
|
||||||
android:id="@+id/days_seek"
|
android:id="@+id/days_text"
|
||||||
android:progress="@{safeUnbox(days)}"
|
android:layout_width="match_parent"
|
||||||
android:max="@{safeUnbox(maxDays)}"
|
android:layout_height="wrap_content" />
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
</LinearLayout>
|
||||||
android:visibility="@{safeUnbox(maxDays) > 0 ? View.VISIBLE : View.GONE}"
|
|
||||||
tools:text="2 Tage"
|
|
||||||
android:text="@{daysText}"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
<ImageButton
|
||||||
|
android:tint="?colorOnSurface"
|
||||||
|
android:id="@+id/switch_to_picker_button"
|
||||||
|
android:src="@drawable/ic_unfold_more_black_24dp"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp" />
|
||||||
|
|
||||||
<ImageButton
|
</LinearLayout>
|
||||||
android:tint="?colorOnSurface"
|
|
||||||
android:id="@+id/switch_to_picker_button"
|
<LinearLayout
|
||||||
android:src="@drawable/ic_unfold_more_black_24dp"
|
android:id="@+id/picker_container"
|
||||||
android:background="?selectableItemBackground"
|
android:orientation="horizontal"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="32dp"
|
android:layout_height="wrap_content">
|
||||||
android:layout_height="32dp" />
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/day_picker_container"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<NumberPicker
|
||||||
|
android:id="@+id/day_picker"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:text="@string/select_time_span_view_days"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/picker_container"
|
android:orientation="vertical"
|
||||||
android:orientation="horizontal"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<LinearLayout
|
<NumberPicker
|
||||||
android:id="@+id/day_picker_container"
|
android:id="@+id/hour_picker"
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<NumberPicker
|
<TextView
|
||||||
android:id="@+id/day_picker"
|
android:layout_gravity="center_horizontal"
|
||||||
android:layout_width="wrap_content"
|
android:text="@string/select_time_span_view_hours"
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:text="@string/select_time_span_view_days"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<NumberPicker
|
|
||||||
android:id="@+id/hour_picker"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:text="@string/select_time_span_view_hours"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<NumberPicker
|
|
||||||
android:id="@+id/minute_picker"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:text="@string/select_time_span_view_minutes"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:tint="?colorOnSurface"
|
|
||||||
android:id="@+id/switch_to_seekbar_button"
|
|
||||||
android:rotation="90"
|
|
||||||
android:layout_margin="16dp"
|
|
||||||
android:src="@drawable/ic_unfold_more_black_24dp"
|
|
||||||
android:background="?selectableItemBackground"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:layout_width="32dp"
|
|
||||||
android:layout_height="32dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<NumberPicker
|
||||||
|
android:id="@+id/minute_picker"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:text="@string/select_time_span_view_minutes"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:tint="?colorOnSurface"
|
||||||
|
android:id="@+id/switch_to_seekbar_button"
|
||||||
|
android:rotation="90"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:src="@drawable/ic_unfold_more_black_24dp"
|
||||||
|
android:background="?selectableItemBackground"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</layout>
|
</LinearLayout>
|
||||||
|
|
|
@ -19,6 +19,13 @@
|
||||||
<item
|
<item
|
||||||
app:showAsAction="ifRoom"
|
app:showAsAction="ifRoom"
|
||||||
app:iconTint="?colorOnPrimary"
|
app:iconTint="?colorOnPrimary"
|
||||||
|
android:icon="@drawable/ic_baseline_directions_bike_24"
|
||||||
|
android:title="@string/manage_child_tasks"
|
||||||
|
android:id="@+id/menu_manage_child_tasks" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
app:showAsAction="never"
|
||||||
|
app:iconTint="?colorOnPrimary"
|
||||||
android:icon="@drawable/ic_phone_black_24dp"
|
android:icon="@drawable/ic_phone_black_24dp"
|
||||||
android:title="@string/contacts_title_long"
|
android:title="@string/contacts_title_long"
|
||||||
android:id="@+id/menu_manage_child_phone" />
|
android:id="@+id/menu_manage_child_phone" />
|
||||||
|
|
|
@ -107,8 +107,7 @@
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/manageChildFragment"
|
android:id="@+id/manageChildFragment"
|
||||||
android:name="io.timelimit.android.ui.manage.child.ManageChildFragment"
|
android:name="io.timelimit.android.ui.manage.child.ManageChildFragment"
|
||||||
android:label="fragment_manage_child"
|
android:label="fragment_manage_child">
|
||||||
tools:layout="@layout/fragment_manage_child" >
|
|
||||||
<action
|
<action
|
||||||
app:enterAnim="@anim/nav_default_enter_anim"
|
app:enterAnim="@anim/nav_default_enter_anim"
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
app:exitAnim="@anim/nav_default_exit_anim"
|
||||||
|
@ -150,12 +149,18 @@
|
||||||
app:exitAnim="@anim/nav_default_exit_anim"
|
app:exitAnim="@anim/nav_default_exit_anim"
|
||||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_manageChildFragment_to_manageChildTasksFragment"
|
||||||
|
app:destination="@id/manageChildTasksFragment"
|
||||||
|
app:enterAnim="@anim/nav_default_enter_anim"
|
||||||
|
app:exitAnim="@anim/nav_default_exit_anim"
|
||||||
|
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||||
|
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/manageCategoryFragment"
|
android:id="@+id/manageCategoryFragment"
|
||||||
android:name="io.timelimit.android.ui.manage.category.ManageCategoryFragment"
|
android:name="io.timelimit.android.ui.manage.category.ManageCategoryFragment"
|
||||||
android:label="fragment_manage_category"
|
android:label="fragment_manage_category">
|
||||||
tools:layout="@layout/fragment_manage_category" >
|
|
||||||
<argument
|
<argument
|
||||||
android:name="childId"
|
android:name="childId"
|
||||||
app:argType="string" />
|
app:argType="string" />
|
||||||
|
@ -571,4 +576,12 @@
|
||||||
android:name="categoryId"
|
android:name="categoryId"
|
||||||
app:argType="string" />
|
app:argType="string" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/manageChildTasksFragment"
|
||||||
|
android:name="io.timelimit.android.ui.fragment.ChildTasksFragmentWrapper"
|
||||||
|
android:label="ManageChildTasksFragment" >
|
||||||
|
<argument
|
||||||
|
android:name="childId"
|
||||||
|
app:argType="string" />
|
||||||
|
</fragment>
|
||||||
</navigation>
|
</navigation>
|
||||||
|
|
|
@ -26,6 +26,9 @@
|
||||||
<string name="generic_manage_card">Verwaltung</string>
|
<string name="generic_manage_card">Verwaltung</string>
|
||||||
<string name="generic_enable">Aktivieren</string>
|
<string name="generic_enable">Aktivieren</string>
|
||||||
<string name="generic_show_more">Mehr anzeigen</string>
|
<string name="generic_show_more">Mehr anzeigen</string>
|
||||||
|
<string name="generic_no">Nein</string>
|
||||||
|
<string name="generic_yes">Ja</string>
|
||||||
|
<string name="generic_skip">Überspringen</string>
|
||||||
|
|
||||||
<string name="generic_swipe_to_dismiss">Sie können diesen Hinweis entfernen, indem Sie ihn zur Seite wischen</string>
|
<string name="generic_swipe_to_dismiss">Sie können diesen Hinweis entfernen, indem Sie ihn zur Seite wischen</string>
|
||||||
<string name="generic_runtime_permission_rejected">Berechtigung abgelehnt; Sie können die Berechtigungen in den Systemeinstellungen verwalten</string>
|
<string name="generic_runtime_permission_rejected">Berechtigung abgelehnt; Sie können die Berechtigungen in den Systemeinstellungen verwalten</string>
|
||||||
|
@ -526,6 +529,7 @@
|
||||||
|
|
||||||
<string name="lock_tab_reason">Grund</string>
|
<string name="lock_tab_reason">Grund</string>
|
||||||
<string name="lock_tab_action">Aktionen</string>
|
<string name="lock_tab_action">Aktionen</string>
|
||||||
|
<string name="lock_tab_task">Aufgaben</string>
|
||||||
<string name="lock_header_blocked">Gesperrt!</string>
|
<string name="lock_header_blocked">Gesperrt!</string>
|
||||||
|
|
||||||
<string name="lock_header_what">Was wurde gesperrt?</string>
|
<string name="lock_header_what">Was wurde gesperrt?</string>
|
||||||
|
@ -619,6 +623,18 @@
|
||||||
</string>
|
</string>
|
||||||
<string name="lock_overlay_text">Sperre %s</string>
|
<string name="lock_overlay_text">Sperre %s</string>
|
||||||
|
|
||||||
|
<string name="lock_task_introduction">Aufgaben ermöglichen eine Zeitverlängerung.
|
||||||
|
Dafür muss ein Elternteil Aufgaben für die entsprechende Kategorie hinterlegen.
|
||||||
|
Ein Kind kann angeben, eine Aufgabe erledigt zu haben und nach der Bestätigung eines
|
||||||
|
Elternteils wird eine entsprechende Extrazeit vergeben.
|
||||||
|
</string>
|
||||||
|
<string name="lock_task_confirm_dialog">Hast Du diese Aufgabe erledigt? Die Zeit gibt es
|
||||||
|
sowieso erst, wenn ein Elternteil das bestätigt hat.</string>
|
||||||
|
<string name="lock_task_review_pending_dialog">Die Erledigung der Aufgabe wurde markiert.
|
||||||
|
Jetzt muss nur noch ein Elternteil das auf der Startseite von TimeLimit besätigen.
|
||||||
|
</string>
|
||||||
|
<string name="lock_task_review_pending_hint">warte auf Bestätigung</string>
|
||||||
|
|
||||||
<string name="login_user_selection_text">Wählen Sie einen Benutzer, um sich anzumelden</string>
|
<string name="login_user_selection_text">Wählen Sie einen Benutzer, um sich anzumelden</string>
|
||||||
<string name="login_password_hint">Passwort</string>
|
<string name="login_password_hint">Passwort</string>
|
||||||
<string name="login_snackbar_wrong">Dieses Passwort ist nicht korrekt</string>
|
<string name="login_snackbar_wrong">Dieses Passwort ist nicht korrekt</string>
|
||||||
|
@ -1480,4 +1496,22 @@
|
||||||
<string name="admin_description_direct">Dies ermöglicht es TimeLimit zu verhindern, ausgeschaltet zu werden</string>
|
<string name="admin_description_direct">Dies ermöglicht es TimeLimit zu verhindern, ausgeschaltet zu werden</string>
|
||||||
|
|
||||||
<string name="admin_disable_warning">Soll der Geräteadministrator wirklich deaktiviert werden?</string>
|
<string name="admin_disable_warning">Soll der Geräteadministrator wirklich deaktiviert werden?</string>
|
||||||
|
|
||||||
|
<string name="manage_child_tasks">Aufgaben</string>
|
||||||
|
<string name="manage_child_tasks_add">Aufgabe erstellen</string>
|
||||||
|
<string name="manage_child_tasks_edit">Aufgabe bearbeiten</string>
|
||||||
|
<string name="manage_child_tasks_intro">Aufgaben sind Tätigkeiten, die ein Kind machen kann,
|
||||||
|
um Extrazeit zu bekommen. Ein Elternteil muss die Erledigung bestätigen, bevor die Extrazeit
|
||||||
|
wirksam wird.
|
||||||
|
</string>
|
||||||
|
<string name="manage_child_task_last_grant">Letzte Bestätigung: %s</string>
|
||||||
|
<string name="manage_child_tasks_select_category">Kategorie wählen</string>
|
||||||
|
<string name="manage_child_tasks_title_hint">Aufgabentitel</string>
|
||||||
|
<string name="manage_child_tasks_toast_saved">Aufgabe gespeichert</string>
|
||||||
|
<string name="manage_child_tasks_toast_removed">Aufgabe wurde gelöscht</string>
|
||||||
|
|
||||||
|
<string name="task_review_title">Aufgabenbestätigung</string>
|
||||||
|
<string name="task_review_text">%1$s hat angegeben, %2$s erledigt zu haben. Ist das richtig?</string>
|
||||||
|
<string name="task_review_category">Dafür wird es %1$s Extrazeit für %2$s geben</string>
|
||||||
|
<string name="task_review_last_grant">Diese Aufgabe wurde zuletzt bestätigt am %s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -13,7 +13,10 @@
|
||||||
You should have received a copy of the GNU General Public License
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"
|
||||||
|
tools:ignore="MissingTranslation">
|
||||||
<string name="generic_ok">OK</string>
|
<string name="generic_ok">OK</string>
|
||||||
<string name="generic_cancel">Cancel</string>
|
<string name="generic_cancel">Cancel</string>
|
||||||
<string name="generic_help">Help</string>
|
<string name="generic_help">Help</string>
|
||||||
|
@ -25,7 +28,10 @@
|
||||||
<string name="generic_go">Go</string>
|
<string name="generic_go">Go</string>
|
||||||
<string name="generic_manage_card">Manage</string>
|
<string name="generic_manage_card">Manage</string>
|
||||||
<string name="generic_enable">Enable</string>
|
<string name="generic_enable">Enable</string>
|
||||||
<string name="generic_show_more" tools:ignore="MissingTranslation">Show more</string>
|
<string name="generic_show_more">Show more</string>
|
||||||
|
<string name="generic_no">No</string>
|
||||||
|
<string name="generic_yes">Yes</string>
|
||||||
|
<string name="generic_skip">Skip</string>
|
||||||
|
|
||||||
<string name="generic_swipe_to_dismiss">Swipe to the side to remove this message</string>
|
<string name="generic_swipe_to_dismiss">Swipe to the side to remove this message</string>
|
||||||
<string name="generic_runtime_permission_rejected">Permission rejected; You can manage permissions in the system settings</string>
|
<string name="generic_runtime_permission_rejected">Permission rejected; You can manage permissions in the system settings</string>
|
||||||
|
@ -211,42 +217,42 @@
|
||||||
|
|
||||||
<string name="background_logic_toast_sync_apps">TimeLimit: Failure while trying to update the app list</string>
|
<string name="background_logic_toast_sync_apps">TimeLimit: Failure while trying to update the app list</string>
|
||||||
|
|
||||||
<string name="blocked_time_areas" tools:ignore="MissingTranslation">
|
<string name="blocked_time_areas">
|
||||||
Blocked time areas
|
Blocked time areas
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="blocked_time_areas_help_about" tools:ignore="MissingTranslation">
|
<string name="blocked_time_areas_help_about">
|
||||||
You can set blocked time areas here.
|
You can set blocked time areas here.
|
||||||
During a blocked time area, no Apps of the category can be used.
|
During a blocked time area, no Apps of the category can be used.
|
||||||
You can define the time area to the minute.
|
You can define the time area to the minute.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="blocked_time_areas_help_colors" tools:ignore="MissingTranslation">
|
<string name="blocked_time_areas_help_colors">
|
||||||
There are 2 colors in this view: green means allowed, red means blocked.
|
There are 2 colors in this view: green means allowed, red means blocked.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="blocked_time_areas_help_structure" tools:ignore="MissingTranslation">
|
<string name="blocked_time_areas_help_structure">
|
||||||
The days and hour are displayed as headlines, the minutes as tiles.
|
The days and hour are displayed as headlines, the minutes as tiles.
|
||||||
You can jump to a day using the day dropdown.
|
You can jump to a day using the day dropdown.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="blocked_time_areas_help_modify" tools:ignore="MissingTranslation">
|
<string name="blocked_time_areas_help_modify">
|
||||||
To change one area, tap on the start time. It will become blue.
|
To change one area, tap on the start time. It will become blue.
|
||||||
Then select the end time.
|
Then select the end time.
|
||||||
Now, the area colors will be inverted.
|
Now, the area colors will be inverted.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="blocked_time_areas_snackbar_modified" tools:ignore="MissingTranslation">
|
<string name="blocked_time_areas_snackbar_modified">
|
||||||
The blocked time areas were changed
|
The blocked time areas were changed
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="blocked_time_areas_snackbar_child_hint" tools:ignore="MissingTranslation">You can only add limits without parent; Undoing is not possible</string>
|
<string name="blocked_time_areas_snackbar_child_hint">You can only add limits without parent; Undoing is not possible</string>
|
||||||
|
|
||||||
<string name="blocked_time_areas_checkbox_detailed" tools:ignore="MissingTranslation">show detailed settings</string>
|
<string name="blocked_time_areas_checkbox_detailed">show detailed settings</string>
|
||||||
|
|
||||||
<string name="blocked_time_areas_copy_to_other_days" tools:ignore="MissingTranslation">Copy settings from one day to other days</string>
|
<string name="blocked_time_areas_copy_to_other_days">Copy settings from one day to other days</string>
|
||||||
<string name="blocked_time_areas_copy_from" tools:ignore="MissingTranslation">Copy from</string>
|
<string name="blocked_time_areas_copy_from">Copy from</string>
|
||||||
<string name="blocked_time_areas_copy_to" tools:ignore="MissingTranslation">Copy from %s to</string>
|
<string name="blocked_time_areas_copy_to">Copy from %s to</string>
|
||||||
|
|
||||||
<string name="category_apps_title">assigned Apps</string>
|
<string name="category_apps_title">assigned Apps</string>
|
||||||
|
|
||||||
|
@ -312,7 +318,7 @@
|
||||||
when the Apps itself are blocked AND when the Apps itself are not blocked
|
when the Apps itself are blocked AND when the Apps itself are not blocked
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="category_settings" tools:ignore="MissingTranslation">Advanced Settings</string>
|
<string name="category_settings">Advanced Settings</string>
|
||||||
<string name="category_settings_rename">Rename category</string>
|
<string name="category_settings_rename">Rename category</string>
|
||||||
<string name="category_settings_rename_empty">The new name must not be empty</string>
|
<string name="category_settings_rename_empty">The new name must not be empty</string>
|
||||||
<string name="category_settings_delete">Delete category</string>
|
<string name="category_settings_delete">Delete category</string>
|
||||||
|
@ -528,8 +534,8 @@
|
||||||
<string name="diagnose_exf_mad">Try to detect multiple active Apps (splitscreen support)</string>
|
<string name="diagnose_exf_mad">Try to detect multiple active Apps (splitscreen support)</string>
|
||||||
<string name="diagnose_exf_rsl">Only allow login of parent users with limit login category after syncing</string>
|
<string name="diagnose_exf_rsl">Only allow login of parent users with limit login category after syncing</string>
|
||||||
<string name="diagnose_exf_bss">Automatically disable splitscreen using the accessibility service</string>
|
<string name="diagnose_exf_bss">Automatically disable splitscreen using the accessibility service</string>
|
||||||
<string name="diagnose_exf_hmw" tools:ignore="MissingTranslation">Hide manipulation warning in the category list</string>
|
<string name="diagnose_exf_hmw">Hide manipulation warning in the category list</string>
|
||||||
<string name="diagnose_exf_esb" tools:ignore="MissingTranslation">Do not use a overlay or the home button for blocking</string>
|
<string name="diagnose_exf_esb">Do not use a overlay or the home button for blocking</string>
|
||||||
|
|
||||||
<string name="diagnose_bg_task_loop_ex">Background task loop exception</string>
|
<string name="diagnose_bg_task_loop_ex">Background task loop exception</string>
|
||||||
|
|
||||||
|
@ -574,8 +580,9 @@
|
||||||
and it is not one - using an older version is enough to circumvent this.
|
and it is not one - using an older version is enough to circumvent this.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="lock_tab_reason" tools:ignore="MissingTranslation">Reason</string>
|
<string name="lock_tab_reason">Reason</string>
|
||||||
<string name="lock_tab_action" tools:ignore="MissingTranslation">Actions</string>
|
<string name="lock_tab_action">Actions</string>
|
||||||
|
<string name="lock_tab_task">Tasks</string>
|
||||||
<string name="lock_header_blocked">Blocked!</string>
|
<string name="lock_header_blocked">Blocked!</string>
|
||||||
|
|
||||||
<string name="lock_header_what">What was blocked?</string>
|
<string name="lock_header_what">What was blocked?</string>
|
||||||
|
@ -673,6 +680,17 @@
|
||||||
</string>
|
</string>
|
||||||
<string name="lock_overlay_text">Blocking %s</string>
|
<string name="lock_overlay_text">Blocking %s</string>
|
||||||
|
|
||||||
|
<string name="lock_task_introduction">Doing tasks allows getting extra time.
|
||||||
|
Parent users must add tasks and they must confirm that a task was done
|
||||||
|
before the extra time is granted.
|
||||||
|
</string>
|
||||||
|
<string name="lock_task_confirm_dialog">Have you finished this task? The time
|
||||||
|
will only be granted if a parent confirms it.</string>
|
||||||
|
<string name="lock_task_review_pending_dialog">It is noted that this task is finished.
|
||||||
|
The only missing part is a parent which confirms this at the start screen of TimeLimit.
|
||||||
|
</string>
|
||||||
|
<string name="lock_task_review_pending_hint">waiting for confirmation</string>
|
||||||
|
|
||||||
<string name="login_user_selection_text">Choose an user to authenticate</string>
|
<string name="login_user_selection_text">Choose an user to authenticate</string>
|
||||||
<string name="login_password_hint">Password</string>
|
<string name="login_password_hint">Password</string>
|
||||||
<string name="login_snackbar_wrong">The password is invalid</string>
|
<string name="login_snackbar_wrong">The password is invalid</string>
|
||||||
|
@ -732,7 +750,7 @@
|
||||||
|
|
||||||
<string name="category_networks_toast_enable_location_service">Please enable location access</string>
|
<string name="category_networks_toast_enable_location_service">Please enable location access</string>
|
||||||
|
|
||||||
<string name="manage_child_tab_other" tools:ignore="MissingTranslation">Advanced settings</string>
|
<string name="manage_child_tab_other">Advanced settings</string>
|
||||||
|
|
||||||
<string name="manage_child_category_no_time_limits">no time limit</string>
|
<string name="manage_child_category_no_time_limits">no time limit</string>
|
||||||
|
|
||||||
|
@ -767,10 +785,10 @@
|
||||||
|
|
||||||
<string name="manage_child_redirected_toast">Press back to show the overview screen of TimeLimit</string>
|
<string name="manage_child_redirected_toast">Press back to show the overview screen of TimeLimit</string>
|
||||||
|
|
||||||
<string name="manage_child_special_mode_wizard_block_title" tools:ignore="MissingTranslation">Block %s temporarily</string>
|
<string name="manage_child_special_mode_wizard_block_title">Block %s temporarily</string>
|
||||||
<string name="manage_child_special_mode_wizard_block_option" tools:ignore="MissingTranslation">block temporarily</string>
|
<string name="manage_child_special_mode_wizard_block_option">block temporarily</string>
|
||||||
<string name="manage_child_special_mode_wizard_disable_limits_title" tools:ignore="MissingTranslation">Disable limits for %s</string>
|
<string name="manage_child_special_mode_wizard_disable_limits_title">Disable limits for %s</string>
|
||||||
<string name="manage_child_special_mode_wizard_disable_limits_option" tools:ignore="MissingTranslation">disable limits temporarily</string>
|
<string name="manage_child_special_mode_wizard_disable_limits_option">disable limits temporarily</string>
|
||||||
|
|
||||||
<string name="manage_device_activity_level_blocking_title">Activity level blocking</string>
|
<string name="manage_device_activity_level_blocking_title">Activity level blocking</string>
|
||||||
<string name="manage_device_activity_level_blocking_text">
|
<string name="manage_device_activity_level_blocking_text">
|
||||||
|
@ -967,8 +985,8 @@
|
||||||
<string name="manage_disable_time_limits_btn_today">for today</string>
|
<string name="manage_disable_time_limits_btn_today">for today</string>
|
||||||
<string name="manage_disable_time_limits_btn_time">until a time</string>
|
<string name="manage_disable_time_limits_btn_time">until a time</string>
|
||||||
<string name="manage_disable_time_limits_btn_date">until a date</string>
|
<string name="manage_disable_time_limits_btn_date">until a date</string>
|
||||||
<string name="manage_disable_time_limits_btn_no_end_time" tools:ignore="MissingTranslation">no scheduled end time</string>
|
<string name="manage_disable_time_limits_btn_no_end_time">no scheduled end time</string>
|
||||||
<string name="manage_disable_time_limits_btn_until" tools:ignore="MissingTranslation">until %s</string>
|
<string name="manage_disable_time_limits_btn_until">until %s</string>
|
||||||
|
|
||||||
<string name="manage_disable_time_limits_dialog_until">Disable time limits until ...</string>
|
<string name="manage_disable_time_limits_dialog_until">Disable time limits until ...</string>
|
||||||
<string name="manage_disable_time_limits_toast_time_in_past">The selected time is in the past</string>
|
<string name="manage_disable_time_limits_toast_time_in_past">The selected time is in the past</string>
|
||||||
|
@ -1071,7 +1089,7 @@
|
||||||
Apps with category are removed from the previous category when they are added to a new category -
|
Apps with category are removed from the previous category when they are added to a new category -
|
||||||
use parent and child categories to \"add\" an App to multiple categories
|
use parent and child categories to \"add\" an App to multiple categories
|
||||||
</string>
|
</string>
|
||||||
<string name="must_read_blocked_time_areas_obsolete" tools:ignore="MissingTranslation">
|
<string name="must_read_blocked_time_areas_obsolete">
|
||||||
The blocked time areas are old. For most cases, using time limit rules with 0 minutes duration
|
The blocked time areas are old. For most cases, using time limit rules with 0 minutes duration
|
||||||
for a part of a day is a better solution.
|
for a part of a day is a better solution.
|
||||||
</string>
|
</string>
|
||||||
|
@ -1129,9 +1147,9 @@
|
||||||
<string name="overview_device_item_older_version">uses an older TimeLimit version</string>
|
<string name="overview_device_item_older_version">uses an older TimeLimit version</string>
|
||||||
|
|
||||||
<string name="overview_user_item_temporarily_blocked">temporarily blocked</string>
|
<string name="overview_user_item_temporarily_blocked">temporarily blocked</string>
|
||||||
<string name="overview_user_item_temporarily_blocked_until" tools:ignore="MissingTranslation">temporarily blocked until %s</string>
|
<string name="overview_user_item_temporarily_blocked_until">temporarily blocked until %s</string>
|
||||||
<string name="overview_user_item_temporarily_disabled">Time limits temporarily disabled</string>
|
<string name="overview_user_item_temporarily_disabled">Time limits temporarily disabled</string>
|
||||||
<string name="overview_user_item_temporarily_disabled_until" tools:ignore="MissingTranslation">Time limits temporarily disabled until %s</string>
|
<string name="overview_user_item_temporarily_disabled_until">Time limits temporarily disabled until %s</string>
|
||||||
<string name="overview_user_item_role_child">is restricted</string>
|
<string name="overview_user_item_role_child">is restricted</string>
|
||||||
<string name="overview_user_item_role_parent">can change settings</string>
|
<string name="overview_user_item_role_parent">can change settings</string>
|
||||||
|
|
||||||
|
@ -1479,7 +1497,7 @@
|
||||||
<string name="usage_history_time_area">from %s until %s</string>
|
<string name="usage_history_time_area">from %s until %s</string>
|
||||||
<string name="usage_history_item_session_duration_limit">Session duration limit of %s with %s break</string>
|
<string name="usage_history_item_session_duration_limit">Session duration limit of %s with %s break</string>
|
||||||
<string name="usage_history_item_last_usage">Last usage: %s</string>
|
<string name="usage_history_item_last_usage">Last usage: %s</string>
|
||||||
<string name="usage_history_filter_all_categories" tools:ignore="MissingTranslation">All Categories</string>
|
<string name="usage_history_filter_all_categories">All Categories</string>
|
||||||
|
|
||||||
<string name="usage_stats_permission_required_and_missing_title">Usage stats access is required</string>
|
<string name="usage_stats_permission_required_and_missing_title">Usage stats access is required</string>
|
||||||
<string name="usage_stats_permission_required_and_missing_text">TimeLimit needs this permission to work correctly. Select \"permissions\" below to enable it.</string>
|
<string name="usage_stats_permission_required_and_missing_text">TimeLimit needs this permission to work correctly. Select \"permissions\" below to enable it.</string>
|
||||||
|
@ -1532,4 +1550,21 @@
|
||||||
<string name="admin_description_direct">This allows TimeLimit to prevent it from being disabled.</string>
|
<string name="admin_description_direct">This allows TimeLimit to prevent it from being disabled.</string>
|
||||||
|
|
||||||
<string name="admin_disable_warning">Do you want to disable the device admin?</string>
|
<string name="admin_disable_warning">Do you want to disable the device admin?</string>
|
||||||
|
|
||||||
|
<string name="manage_child_tasks">Tasks</string>
|
||||||
|
<string name="manage_child_tasks_add">Add Task</string>
|
||||||
|
<string name="manage_child_tasks_edit">Edit Task</string>
|
||||||
|
<string name="manage_child_tasks_intro">Tasks are things which a child can do to get extra time.
|
||||||
|
A parent must confirm the completion before the extra time is granted.
|
||||||
|
</string>
|
||||||
|
<string name="manage_child_task_last_grant">Last confirmation: %s</string>
|
||||||
|
<string name="manage_child_tasks_select_category">Select a category</string>
|
||||||
|
<string name="manage_child_tasks_title_hint">Task title</string>
|
||||||
|
<string name="manage_child_tasks_toast_saved">Task saved</string>
|
||||||
|
<string name="manage_child_tasks_toast_removed">Task removed</string>
|
||||||
|
|
||||||
|
<string name="task_review_title">Task confirmation</string>
|
||||||
|
<string name="task_review_text">%1$s said that %2$s was finished. Is this correct?</string>
|
||||||
|
<string name="task_review_category">This will grant %1$s extra time for %2$s</string>
|
||||||
|
<string name="task_review_last_grant">This task was confirmed last time at %s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue