mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +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 userLimitLoginCategoryDao(): UserLimitLoginCategoryDao
|
||||
fun categoryNetworkId(): CategoryNetworkIdDao
|
||||
fun childTasks(): ChildTaskDao
|
||||
|
||||
fun <T> runInTransaction(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")
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
SessionDuration::class,
|
||||
UserLimitLoginCategory::class,
|
||||
CategoryNetworkId::class
|
||||
], version = 33)
|
||||
CategoryNetworkId::class,
|
||||
ChildTask::class
|
||||
], version = 34)
|
||||
abstract class RoomDatabase: RoomDatabase(), io.timelimit.android.data.Database {
|
||||
companion 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_V31,
|
||||
DatabaseMigrations.MIGRATE_TO_V32,
|
||||
DatabaseMigrations.MIGRATE_TO_V33
|
||||
DatabaseMigrations.MIGRATE_TO_V33,
|
||||
DatabaseMigrations.MIGRATE_TO_V34
|
||||
)
|
||||
.setQueryExecutor(Threads.database)
|
||||
.build()
|
||||
|
|
|
@ -44,6 +44,7 @@ object DatabaseBackupLowlevel {
|
|||
private const val SESSION_DURATION = "sessionDuration"
|
||||
private const val USER_LIMIT_LOGIN_CATEGORY = "userLimitLoginCategory"
|
||||
private const val CATEGORY_NETWORK_ID = "categoryNetworkId"
|
||||
private const val CHILD_TASK = "childTask"
|
||||
|
||||
fun outputAsBackupJson(database: Database, outputStream: OutputStream) {
|
||||
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(USER_LIMIT_LOGIN_CATEGORY) { offset, pageSize -> database.userLimitLoginCategoryDao().getAllowedContactPageSync(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()
|
||||
}
|
||||
|
@ -101,6 +103,7 @@ object DatabaseBackupLowlevel {
|
|||
|
||||
var userLoginLimitCategories = emptyList<UserLimitLoginCategory>()
|
||||
var categoryNetworkId = emptyList<CategoryNetworkId>()
|
||||
var childTasks = emptyList<ChildTask>()
|
||||
|
||||
database.runInTransaction {
|
||||
database.deleteAllData()
|
||||
|
@ -267,6 +270,19 @@ object DatabaseBackupLowlevel {
|
|||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -274,6 +290,7 @@ object DatabaseBackupLowlevel {
|
|||
|
||||
if (userLoginLimitCategories.isNotEmpty()) { database.userLimitLoginCategoryDao().addItemsSync(userLoginLimitCategories) }
|
||||
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")
|
||||
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>>
|
||||
|
||||
@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")
|
||||
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
|
||||
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()
|
||||
|
||||
@Query("SELECT * FROM category LIMIT :pageSize OFFSET :offset")
|
||||
|
@ -114,7 +117,9 @@ data class CategoryWithVersionNumbers(
|
|||
@ColumnInfo(name = "rules_version")
|
||||
val timeLimitRulesVersion: String,
|
||||
@ColumnInfo(name = "usedtimes_version")
|
||||
val usedTimeItemsVersion: String
|
||||
val usedTimeItemsVersion: String,
|
||||
@ColumnInfo(name = "tasks_version")
|
||||
val taskListVersion: String
|
||||
)
|
||||
|
||||
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,
|
||||
@ColumnInfo(name = "usedtimes_version")
|
||||
val usedTimesVersion: String,
|
||||
@ColumnInfo(name = "tasks_version", defaultValue = "")
|
||||
val tasksVersion: String,
|
||||
@ColumnInfo(name = "parent_category_id")
|
||||
val parentCategoryId: String,
|
||||
@ColumnInfo(name = "block_all_notifications")
|
||||
|
@ -95,6 +97,7 @@ data class Category(
|
|||
private const val SORT = "sort"
|
||||
private const val EXTRA_TIME_DAY = "extraTimeDay"
|
||||
private const val DISABLE_LIMIITS_UNTIL = "dlu"
|
||||
private const val TASKS_VERSION = "tv"
|
||||
|
||||
fun parse(reader: JsonReader): Category {
|
||||
var id: String? = null
|
||||
|
@ -117,6 +120,7 @@ data class Category(
|
|||
var sort = 0
|
||||
var extraTimeDay = -1
|
||||
var disableLimitsUntil = 0L
|
||||
var tasksVersion = ""
|
||||
|
||||
reader.beginObject()
|
||||
|
||||
|
@ -141,6 +145,7 @@ data class Category(
|
|||
SORT -> sort = reader.nextInt()
|
||||
EXTRA_TIME_DAY -> extraTimeDay = reader.nextInt()
|
||||
DISABLE_LIMIITS_UNTIL -> disableLimitsUntil = reader.nextLong()
|
||||
TASKS_VERSION -> tasksVersion = reader.nextString()
|
||||
else -> reader.skipValue()
|
||||
}
|
||||
}
|
||||
|
@ -159,6 +164,7 @@ data class Category(
|
|||
assignedAppsVersion = assignedAppsVersion!!,
|
||||
timeLimitRulesVersion = timeLimitRulesVersion!!,
|
||||
usedTimesVersion = usedTimesVersion!!,
|
||||
tasksVersion = tasksVersion,
|
||||
parentCategoryId = parentCategoryId,
|
||||
blockAllNotifications = blockAllNotifications,
|
||||
timeWarnings = timeWarnings,
|
||||
|
@ -214,6 +220,7 @@ data class Category(
|
|||
writer.name(ASSIGNED_APPS_VERSION).value(assignedAppsVersion)
|
||||
writer.name(RULES_VERSION).value(timeLimitRulesVersion)
|
||||
writer.name(USED_TIMES_VERSION).value(usedTimesVersion)
|
||||
writer.name(TASKS_VERSION).value(tasksVersion)
|
||||
writer.name(PARENT_CATEGORY_ID).value(parentCategoryId)
|
||||
writer.name(BlOCK_ALL_NOTIFICATIONS).value(blockAllNotifications)
|
||||
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 TIMELIMIT_RULE_MUSTREAD = 32L
|
||||
const val BLOCKED_TIME_AREAS_OBSOLETE = 64L
|
||||
const val TASKS_INTRODUCTION = 128L
|
||||
}
|
||||
|
||||
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 = "",
|
||||
timeLimitRulesVersion = "",
|
||||
usedTimesVersion = "",
|
||||
tasksVersion = "",
|
||||
parentCategoryId = "",
|
||||
blockAllNotifications = false,
|
||||
timeWarnings = 0,
|
||||
|
@ -201,6 +202,7 @@ class AppSetupLogic(private val appLogic: AppLogic) {
|
|||
assignedAppsVersion = "",
|
||||
timeLimitRulesVersion = "",
|
||||
usedTimesVersion = "",
|
||||
tasksVersion = "",
|
||||
parentCategoryId = "",
|
||||
blockAllNotifications = false,
|
||||
timeWarnings = 0,
|
||||
|
|
|
@ -105,8 +105,21 @@ data class CategoryItselfHandling (
|
|||
else
|
||||
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 okByBlockedTimeAreas = areLimitsTemporarilyDisabled || !categoryRelatedData.category.blockedMinutesInWeek.read(minuteInWeek)
|
||||
val okByBlockedTimeAreas = areLimitsTemporarilyDisabled || (
|
||||
(!categoryRelatedData.category.blockedMinutesInWeek.read(minuteInWeek)) && (!hasBlockedTimeAreaRelatedRule))
|
||||
val dependsOnMaxMinuteOfWeekByBlockedTimeAreas = categoryRelatedData.category.blockedMinutesInWeek.let { blockedTimeAreas ->
|
||||
if (blockedTimeAreas.dataNotToModify[minuteInWeek]) {
|
||||
blockedTimeAreas.dataNotToModify.nextClearBit(minuteInWeek)
|
||||
|
@ -121,27 +134,18 @@ data class CategoryItselfHandling (
|
|||
else
|
||||
dependsOnMaxMinuteOfWeekByBlockedTimeAreas % MinuteOfDay.LENGTH
|
||||
|
||||
val relatedRules = if (areLimitsTemporarilyDisabled)
|
||||
emptyList()
|
||||
else
|
||||
RemainingTime.getRulesRelatedToDay(
|
||||
dayOfWeek = dayOfWeek,
|
||||
minuteOfDay = minuteInWeek % MinuteOfDay.LENGTH,
|
||||
rules = categoryRelatedData.rules
|
||||
)
|
||||
|
||||
val remainingTime = RemainingTime.getRemainingTime(
|
||||
usedTimes = categoryRelatedData.usedTimes,
|
||||
// dependsOnMaxTimeByRules always depends on the day so that this is invalidated correctly
|
||||
extraTime = categoryRelatedData.category.getExtraTime(dayOfEpoch = dayOfEpoch),
|
||||
rules = relatedRules,
|
||||
rules = regularRelatedRules,
|
||||
dayOfWeek = dayOfWeek,
|
||||
minuteOfDay = minuteInWeek % MinuteOfDay.LENGTH,
|
||||
firstDayOfWeekAsEpochDay = firstDayOfWeekAsEpochDay
|
||||
)
|
||||
|
||||
val remainingSessionDuration = RemainingSessionDuration.getRemainingSessionDuration(
|
||||
rules = relatedRules,
|
||||
rules = regularRelatedRules,
|
||||
minuteOfDay = minuteInWeek % MinuteOfDay.LENGTH,
|
||||
dayOfWeek = dayOfWeek,
|
||||
timestamp = timeInMillis,
|
||||
|
@ -149,8 +153,8 @@ data class CategoryItselfHandling (
|
|||
)
|
||||
|
||||
val missingNetworkTimeForRules = categoryRelatedData.rules.isNotEmpty()
|
||||
val okByTimeLimitRules = relatedRules.isEmpty() || (remainingTime != null && remainingTime.hasRemainingTime)
|
||||
val dependsOnMaxTimeByMinuteOfDay = (relatedRules.minBy { it.endMinuteOfDay }?.endMinuteOfDay ?: Int.MAX_VALUE).coerceAtMost(
|
||||
val okByTimeLimitRules = regularRelatedRules.isEmpty() || (remainingTime != null && remainingTime.hasRemainingTime)
|
||||
val dependsOnMaxTimeByMinuteOfDay = (allRelatedRules.minBy { it.endMinuteOfDay }?.endMinuteOfDay ?: Int.MAX_VALUE).coerceAtMost(
|
||||
categoryRelatedData.rules
|
||||
.filter {
|
||||
// related to today
|
||||
|
@ -195,12 +199,12 @@ data class CategoryItselfHandling (
|
|||
val missingNetworkTime = !shouldTrustTimeTemporarily &&
|
||||
(missingNetworkTimeForDisableTempBlocking || missingNetworkTimeForBlockedTimeAreas || missingNetworkTimeForRules)
|
||||
|
||||
val shouldCountTime = relatedRules.isNotEmpty()
|
||||
val shouldCountTime = regularRelatedRules.isNotEmpty()
|
||||
val shouldCountExtraTime = remainingTime?.usingExtraTime == true
|
||||
val sessionDurationSlotsToCount = if (remainingSessionDuration != null && remainingSessionDuration <= 0)
|
||||
emptySet()
|
||||
else
|
||||
relatedRules.filter { it.sessionDurationLimitEnabled }.map {
|
||||
regularRelatedRules.filter { it.sessionDurationLimitEnabled }.map {
|
||||
AddUsedTimeActionItemSessionDurationLimitSlot(
|
||||
startMinuteOfDay = it.startMinuteOfDay,
|
||||
endMinuteOfDay = it.endMinuteOfDay,
|
||||
|
@ -219,7 +223,7 @@ data class CategoryItselfHandling (
|
|||
val maxTimeToAdd = maxTimeToAddByRegularTime.coerceAtMost(maxTimeToAddBySessionDuration)
|
||||
|
||||
val additionalTimeCountingSlots = if (shouldCountTime)
|
||||
relatedRules
|
||||
regularRelatedRules
|
||||
.filterNot { it.appliesToWholeDay }
|
||||
.map { AddUsedTimeActionItemAdditionalCountingSlot(it.startMinuteOfDay, it.endMinuteOfDay) }
|
||||
.toSet()
|
||||
|
|
|
@ -325,6 +325,7 @@ object ApplyServerDataStatus {
|
|||
assignedAppsVersion = "",
|
||||
timeLimitRulesVersion = "",
|
||||
usedTimesVersion = "",
|
||||
tasksVersion = "",
|
||||
parentCategoryId = newCategory.parentCategoryId,
|
||||
timeWarnings = newCategory.timeWarnings,
|
||||
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 ->
|
||||
if (user.limitLoginCategory == null) {
|
||||
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() {
|
||||
companion object {
|
||||
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
|
||||
|
||||
data class UpdateDeviceStatusAction(
|
||||
|
|
|
@ -381,6 +381,15 @@ object LocalDatabaseAppLogicActionDispatcher {
|
|||
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,6 +139,7 @@ object LocalDatabaseParentActionDispatcher {
|
|||
assignedAppsVersion = "",
|
||||
timeLimitRulesVersion = "",
|
||||
usedTimesVersion = "",
|
||||
tasksVersion = "",
|
||||
parentCategoryId = "",
|
||||
blockAllNotifications = false,
|
||||
timeWarnings = 0,
|
||||
|
@ -767,6 +768,64 @@ object LocalDatabaseParentActionDispatcher {
|
|||
|
||||
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 { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ data class ClientDataStatus(
|
|||
private const val CATEGORIES = "categories"
|
||||
private const val USERS = "users"
|
||||
private const val CLIENT_LEVEL = "clientLevel"
|
||||
private const val CLIENT_LEVEL_VALUE = 2
|
||||
private const val CLIENT_LEVEL_VALUE = 3
|
||||
|
||||
val empty = ClientDataStatus(
|
||||
deviceListVersion = "",
|
||||
|
@ -63,7 +63,8 @@ data class ClientDataStatus(
|
|||
baseVersion = it.baseVersion,
|
||||
assignedAppsVersion = it.assignedAppsVersion,
|
||||
timeLimitRulesVersion = it.timeLimitRulesVersion,
|
||||
usedTimeItemsVersion = it.usedTimeItemsVersion
|
||||
usedTimeItemsVersion = it.usedTimeItemsVersion,
|
||||
taskListVersion = it.taskListVersion
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -104,13 +105,15 @@ data class CategoryDataStatus(
|
|||
val baseVersion: String,
|
||||
val assignedAppsVersion: String,
|
||||
val timeLimitRulesVersion: String,
|
||||
val usedTimeItemsVersion: String
|
||||
val usedTimeItemsVersion: String,
|
||||
val taskListVersion: String
|
||||
) {
|
||||
companion object {
|
||||
private const val BASE_VERSION = "base"
|
||||
private const val ASSIGNED_APPS_VERSION = "apps"
|
||||
private const val TIME_LIMIT_RULES_VERSION = "rules"
|
||||
private const val USED_TIME_ITEMS_VERSION = "usedTime"
|
||||
private const val TASK_LIST_VERSION = "tasks"
|
||||
}
|
||||
|
||||
fun serialize(writer: JsonWriter) {
|
||||
|
@ -121,6 +124,8 @@ data class CategoryDataStatus(
|
|||
writer.name(TIME_LIMIT_RULES_VERSION).value(timeLimitRulesVersion)
|
||||
writer.name(USED_TIME_ITEMS_VERSION).value(usedTimeItemsVersion)
|
||||
|
||||
if (taskListVersion.isNotEmpty()) writer.name(TASK_LIST_VERSION).value(taskListVersion)
|
||||
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
|
@ -40,6 +40,7 @@ data class ServerDataStatus(
|
|||
val newCategoryAssignedApps: List<ServerUpdatedCategoryAssignedApps>,
|
||||
val newCategoryUsedTimes: List<ServerUpdatedCategoryUsedTimes>,
|
||||
val newCategoryTimeLimitRules: List<ServerUpdatedTimeLimitRules>,
|
||||
val newCategoryTasks: List<ServerUpdatedCategoryTasks>,
|
||||
val newUserList: ServerUserList?,
|
||||
val fullVersionUntil: Long,
|
||||
val message: String?
|
||||
|
@ -52,6 +53,7 @@ data class ServerDataStatus(
|
|||
private const val NEW_CATEGORY_ASSIGNED_APPS = "categoryApp"
|
||||
private const val NEW_CATEGORY_USED_TIMES = "usedTimes"
|
||||
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 FULL_VERSION_UNTIL = "fullVersion"
|
||||
private const val MESSAGE = "message"
|
||||
|
@ -64,6 +66,7 @@ data class ServerDataStatus(
|
|||
var newCategoryAssignedApps: List<ServerUpdatedCategoryAssignedApps> = Collections.emptyList()
|
||||
var newCategoryUsedTimes: List<ServerUpdatedCategoryUsedTimes> = Collections.emptyList()
|
||||
var newCategoryTimeLimitRules: List<ServerUpdatedTimeLimitRules> = Collections.emptyList()
|
||||
var newCategoryTasks: List<ServerUpdatedCategoryTasks> = emptyList()
|
||||
var newUserList: ServerUserList? = null
|
||||
var fullVersionUntil: Long? = null
|
||||
var message: String? = null
|
||||
|
@ -78,6 +81,7 @@ data class ServerDataStatus(
|
|||
NEW_CATEGORY_ASSIGNED_APPS -> newCategoryAssignedApps = ServerUpdatedCategoryAssignedApps.parseList(reader)
|
||||
NEW_CATEGORY_USED_TIMES -> newCategoryUsedTimes = ServerUpdatedCategoryUsedTimes.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)
|
||||
FULL_VERSION_UNTIL -> fullVersionUntil = reader.nextLong()
|
||||
MESSAGE -> message = reader.nextString()
|
||||
|
@ -94,6 +98,7 @@ data class ServerDataStatus(
|
|||
newCategoryAssignedApps = newCategoryAssignedApps,
|
||||
newCategoryUsedTimes = newCategoryUsedTimes,
|
||||
newCategoryTimeLimitRules = newCategoryTimeLimitRules,
|
||||
newCategoryTasks = newCategoryTasks,
|
||||
newUserList = newUserList,
|
||||
fullVersionUntil = fullVersionUntil!!,
|
||||
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(
|
||||
val deviceId: 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.child.advanced.ManageChildAdvancedFragment
|
||||
import io.timelimit.android.ui.manage.child.apps.ChildAppsFragment
|
||||
import io.timelimit.android.ui.manage.child.tasks.ManageChildTasksFragment
|
||||
|
||||
abstract class ChildFragmentWrapper: SingleFragmentWrapper() {
|
||||
abstract val childId: String
|
||||
|
@ -64,4 +65,13 @@ class ChildUsageHistoryFragmentWrapper: ChildFragmentWrapper(), FragmentWithCust
|
|||
|
||||
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)}" } }
|
||||
}
|
||||
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val adapter = LockActivityAdapter(supportFragmentManager, this)
|
||||
|
||||
setContentView(R.layout.lock_activity)
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
|
@ -97,7 +99,7 @@ class LockActivity : AppCompatActivity(), ActivityViewModelHolder {
|
|||
|
||||
model.init(blockedPackageName, blockedActivityName)
|
||||
|
||||
pager.adapter = LockActivityAdapter(supportFragmentManager, this)
|
||||
pager.adapter = adapter
|
||||
|
||||
model.content.observe(this) {
|
||||
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) {
|
||||
super.onPageSelected(position)
|
||||
|
||||
showAuth.value = position > 0
|
||||
showAuth.value = position == 1
|
||||
}
|
||||
})
|
||||
|
||||
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() {
|
||||
|
|
|
@ -21,19 +21,24 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import io.timelimit.android.R
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
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) {
|
||||
0 -> LockReasonFragment()
|
||||
1 -> LockActionFragment()
|
||||
2 -> LockTaskFragment()
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence? = context.getString(when (position) {
|
||||
0 -> R.string.lock_tab_reason
|
||||
1 -> R.string.lock_tab_action
|
||||
2 -> R.string.lock_tab_task
|
||||
else -> throw IllegalArgumentException()
|
||||
})
|
||||
}
|
|
@ -171,6 +171,18 @@ class LockModel(application: Application): AndroidViewModel(application) {
|
|||
|
||||
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() {
|
||||
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 com.google.android.material.snackbar.Snackbar
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.data.extensions.mapToTimezone
|
||||
import io.timelimit.android.databinding.FragmentCategorySettingsBinding
|
||||
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.networks.ManageCategoryNetworksView
|
||||
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
||||
import io.timelimit.android.ui.view.SelectTimeSpanViewListener
|
||||
import io.timelimit.android.ui.util.bind
|
||||
|
||||
class CategorySettingsFragment : Fragment() {
|
||||
companion object {
|
||||
|
@ -210,16 +209,8 @@ class CategorySettingsFragment : Fragment() {
|
|||
binding.extraTimeSelection.enablePickerMode(it)
|
||||
})
|
||||
|
||||
binding.extraTimeSelection.listener = object: SelectTimeSpanViewListener {
|
||||
override fun onTimeSpanChanged(newTimeInMillis: Long) {
|
||||
updateEditExtraTimeConfirmButtonVisibility()
|
||||
}
|
||||
|
||||
override fun setEnablePickerMode(enable: Boolean) {
|
||||
Threads.database.execute {
|
||||
appLogic.database.config().setEnableAlternativeDurationSelectionSync(enable)
|
||||
}
|
||||
}
|
||||
binding.extraTimeSelection.bind(appLogic.database, viewLifecycleOwner) {
|
||||
updateEditExtraTimeConfirmButtonVisibility()
|
||||
}
|
||||
|
||||
binding.switchLimitExtraTimeToToday.setOnCheckedChangeListener { _, _ ->
|
||||
|
|
|
@ -123,10 +123,10 @@ class EditTimeLimitRuleDialogFragment : BottomSheetDialogFragment(), DurationPic
|
|||
}
|
||||
)
|
||||
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())
|
||||
view.timeSpan.maxDays = Math.max(0, affectedDays - 1) // max prevents crash
|
||||
view.timeSpan.timeInMillis = newRule.maximumTimeInMillis.toLong()
|
||||
view.affectsMultipleDays = affectedDays >= 2
|
||||
|
||||
view.applyToWholeDay = newRule.appliesToWholeDay
|
||||
|
|
|
@ -89,6 +89,14 @@ class ManageChildFragment : ChildFragmentWrapper(), FragmentWithCustomTitle {
|
|||
|
||||
true
|
||||
}
|
||||
R.id.menu_manage_child_tasks -> {
|
||||
navigation.safeNavigate(
|
||||
ManageChildFragmentDirections.actionManageChildFragmentToManageChildTasksFragment(childId = childId),
|
||||
R.id.manageChildFragment
|
||||
)
|
||||
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import io.timelimit.android.R
|
|||
import io.timelimit.android.data.model.Category
|
||||
import io.timelimit.android.databinding.AddItemViewBinding
|
||||
import io.timelimit.android.databinding.CategoryRichCardBinding
|
||||
import io.timelimit.android.databinding.IntroCardBinding
|
||||
import io.timelimit.android.ui.util.DateUtil
|
||||
import io.timelimit.android.util.TimeTextUtil
|
||||
import kotlin.properties.Delegates
|
||||
|
@ -91,8 +92,10 @@ class Adapter: RecyclerView.Adapter<ViewHolder>() {
|
|||
|
||||
TYPE_INTRO ->
|
||||
IntroViewHolder(
|
||||
LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.category_list_intro, parent, false)
|
||||
IntroCardBinding.inflate(LayoutInflater.from(parent.context), parent, false).also {
|
||||
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 ->
|
||||
|
|
|
@ -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.category.create.CreateCategoryDialogFragment
|
||||
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() {
|
||||
companion object {
|
||||
|
@ -57,7 +57,7 @@ class ManageChildCategoriesFragment : Fragment() {
|
|||
private val model: ManageChildCategoriesModel by viewModels()
|
||||
|
||||
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?) {
|
||||
|
|
|
@ -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.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.CoroutineFragment
|
||||
import io.timelimit.android.data.model.Device
|
||||
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.data.model.*
|
||||
import io.timelimit.android.livedata.waitForNonNullValue
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
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.getActivityViewModel
|
||||
import kotlinx.android.synthetic.main.fragment_overview.*
|
||||
|
@ -41,11 +38,9 @@ import kotlinx.coroutines.launch
|
|||
|
||||
class OverviewFragment : CoroutineFragment() {
|
||||
private val handlers: OverviewFragmentParentHandlers by lazy { parentFragment as OverviewFragmentParentHandlers }
|
||||
private val logic: AppLogic by lazy { DefaultAppLogic.with(context!!) }
|
||||
private val auth: ActivityViewModel by lazy { getActivityViewModel(activity!!) }
|
||||
private val model: OverviewFragmentModel by lazy {
|
||||
ViewModelProviders.of(this).get(OverviewFragmentModel::class.java)
|
||||
}
|
||||
private val logic: AppLogic by lazy { DefaultAppLogic.with(requireContext()) }
|
||||
private val auth: ActivityViewModel by lazy { getActivityViewModel(requireActivity()) }
|
||||
private val model: OverviewFragmentModel by viewModels()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_overview, container, false)
|
||||
|
@ -57,7 +52,7 @@ class OverviewFragment : CoroutineFragment() {
|
|||
val adapter = OverviewFragmentAdapter()
|
||||
|
||||
recycler.adapter = adapter
|
||||
recycler.layoutManager = LinearLayoutManager(context!!)
|
||||
recycler.layoutManager = LinearLayoutManager(requireContext())
|
||||
|
||||
adapter.handlers = object: OverviewFragmentHandlers {
|
||||
override fun onAddUserClicked() {
|
||||
|
@ -106,9 +101,33 @@ class OverviewFragment : CoroutineFragment() {
|
|||
override fun onSetDeviceListVisibility(level: DeviceListItemVisibility) {
|
||||
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(
|
||||
object: ItemTouchHelper.Callback() {
|
||||
|
|
|
@ -21,10 +21,13 @@ import android.view.ViewGroup
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.data.model.ChildTask
|
||||
import io.timelimit.android.data.model.Device
|
||||
import io.timelimit.android.data.model.User
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.databinding.*
|
||||
import io.timelimit.android.ui.util.DateUtil
|
||||
import io.timelimit.android.util.TimeTextUtil
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class OverviewFragmentAdapter : RecyclerView.Adapter<OverviewFragmentViewHolder>() {
|
||||
|
@ -45,6 +48,7 @@ class OverviewFragmentAdapter : RecyclerView.Adapter<OverviewFragmentViewHolder>
|
|||
return when (item) {
|
||||
is OverviewFragmentItemDevice -> "device ${item.device.id}".hashCode().toLong()
|
||||
is OverviewFragmentItemUser -> "user ${item.user.id}".hashCode().toLong()
|
||||
is TaskReviewOverviewItem -> "task ${item.task.taskId}".hashCode().toLong()
|
||||
else -> item.hashCode().toLong()
|
||||
}
|
||||
}
|
||||
|
@ -70,6 +74,7 @@ class OverviewFragmentAdapter : RecyclerView.Adapter<OverviewFragmentViewHolder>
|
|||
is OverviewFragmentHeaderFinishSetup -> OverviewFragmentViewType.FinishSetup
|
||||
is OverviewFragmentItemMessage -> OverviewFragmentViewType.ServerMessage
|
||||
is ShowMoreOverviewFragmentItem -> OverviewFragmentViewType.ShowMoreButton
|
||||
is TaskReviewOverviewItem -> OverviewFragmentViewType.TaskReview
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
OverviewFragmentViewType.TaskReview.ordinal -> TaskReviewHolder(
|
||||
FragmentOverviewTaskReviewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
)
|
||||
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
|
||||
|
@ -241,6 +250,23 @@ class OverviewFragmentAdapter : RecyclerView.Adapter<OverviewFragmentViewHolder>
|
|||
}
|
||||
}.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 { }
|
||||
}
|
||||
}
|
||||
|
@ -254,7 +280,8 @@ enum class OverviewFragmentViewType {
|
|||
Introduction,
|
||||
FinishSetup,
|
||||
ServerMessage,
|
||||
ShowMoreButton
|
||||
ShowMoreButton,
|
||||
TaskReview
|
||||
}
|
||||
|
||||
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 ServerMessageViewHolder(val binding: FragmentOverviewServerMessageBinding): OverviewFragmentViewHolder(binding.root)
|
||||
class ShowMoreViewHolder(view: View): OverviewFragmentViewHolder(view)
|
||||
class TaskReviewHolder(val binding: FragmentOverviewTaskReviewBinding): OverviewFragmentViewHolder(binding.root)
|
||||
|
||||
interface OverviewFragmentHandlers {
|
||||
fun onAddUserClicked()
|
||||
|
@ -276,4 +304,7 @@ interface OverviewFragmentHandlers {
|
|||
fun onFinishSetupClicked()
|
||||
fun onShowAllUsersClicked()
|
||||
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
|
||||
|
||||
import io.timelimit.android.data.model.ChildTask
|
||||
import io.timelimit.android.data.model.Device
|
||||
import io.timelimit.android.data.model.User
|
||||
import io.timelimit.android.data.model.UserType
|
||||
|
@ -39,4 +40,5 @@ data class OverviewFragmentItemMessage(val message: String): OverviewFragmentIte
|
|||
sealed class ShowMoreOverviewFragmentItem: OverviewFragmentItem() {
|
||||
object ShowAllUsers: ShowMoreOverviewFragmentItem()
|
||||
data class ShowMoreDevices(val level: DeviceListItemVisibility): ShowMoreOverviewFragmentItem()
|
||||
}
|
||||
}
|
||||
data class TaskReviewOverviewItem(val task: ChildTask, val childTitle: String, val categoryTitle: String): 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 ->
|
||||
deviceEntries.switchMap { deviceEntries ->
|
||||
userEntries.switchMap { userEntries ->
|
||||
itemVisibility.map { itemVisibility ->
|
||||
mutableListOf<OverviewFragmentItem>().apply {
|
||||
addAll(introEntries)
|
||||
pendingTaskItemLive.switchMap { pendingTaskItem ->
|
||||
itemVisibility.map { itemVisibility ->
|
||||
mutableListOf<OverviewFragmentItem>().apply {
|
||||
addAll(introEntries)
|
||||
|
||||
add(OverviewFragmentHeaderDevices)
|
||||
val shownDevices = when (itemVisibility.devices) {
|
||||
DeviceListItemVisibility.BareMinimum -> deviceEntries.filter { it.isCurrentDevice || it.isImportant }
|
||||
DeviceListItemVisibility.AllChildDevices -> deviceEntries.filter { it.isCurrentDevice || it.isImportant || it.deviceUser?.type == UserType.Child }
|
||||
DeviceListItemVisibility.AllDevices -> deviceEntries
|
||||
}
|
||||
addAll(shownDevices)
|
||||
if (shownDevices.size == deviceEntries.size) {
|
||||
add(OverviewFragmentActionAddDevice)
|
||||
} else {
|
||||
add(ShowMoreOverviewFragmentItem.ShowMoreDevices(when (itemVisibility.devices) {
|
||||
DeviceListItemVisibility.BareMinimum -> if (deviceEntries.find { it.deviceUser?.type == UserType.Child } != null)
|
||||
DeviceListItemVisibility.AllChildDevices else DeviceListItemVisibility.AllDevices
|
||||
DeviceListItemVisibility.AllChildDevices -> DeviceListItemVisibility.AllDevices
|
||||
DeviceListItemVisibility.AllDevices -> DeviceListItemVisibility.AllDevices
|
||||
}))
|
||||
}
|
||||
if (pendingTaskItem != null) add(pendingTaskItem)
|
||||
|
||||
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>>
|
||||
add(OverviewFragmentHeaderDevices)
|
||||
val shownDevices = when (itemVisibility.devices) {
|
||||
DeviceListItemVisibility.BareMinimum -> deviceEntries.filter { it.isCurrentDevice || it.isImportant }
|
||||
DeviceListItemVisibility.AllChildDevices -> deviceEntries.filter { it.isCurrentDevice || it.isImportant || it.deviceUser?.type == UserType.Child }
|
||||
DeviceListItemVisibility.AllDevices -> deviceEntries
|
||||
}
|
||||
addAll(shownDevices)
|
||||
if (shownDevices.size == deviceEntries.size) {
|
||||
add(OverviewFragmentActionAddDevice)
|
||||
} else {
|
||||
add(ShowMoreOverviewFragmentItem.ShowMoreDevices(when (itemVisibility.devices) {
|
||||
DeviceListItemVisibility.BareMinimum -> 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
|
||||
* 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.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.SeekBar
|
||||
import android.widget.*
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.databinding.ViewSelectTimeSpanBinding
|
||||
import io.timelimit.android.util.TimeTextUtil
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class SelectTimeSpanView(context: Context, attributeSet: AttributeSet? = null): FrameLayout(context, attributeSet) {
|
||||
private val binding = ViewSelectTimeSpanBinding.inflate(LayoutInflater.from(context), this, false)
|
||||
|
||||
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 timeInMillis: Long by Delegates.observable(0L) { _, _, _ ->
|
||||
bindTime()
|
||||
var timeInMillis: Long by Delegates.observable(0L) { _, oldValue, newValue ->
|
||||
if (oldValue != newValue) { bindTime() }
|
||||
|
||||
listener?.onTimeSpanChanged(timeInMillis)
|
||||
}
|
||||
|
||||
var maxDays: Int by Delegates.observable(0) { _, _, _ ->
|
||||
binding.maxDays = maxDays
|
||||
|
||||
binding.dayPicker.maxValue = maxDays
|
||||
binding.dayPickerContainer.visibility = if (maxDays > 0) View.VISIBLE else View.GONE
|
||||
}
|
||||
var maxDays: Int by Delegates.observable(0) { _, _, newValue -> bindMaxDays(newValue) }
|
||||
|
||||
init {
|
||||
val attributes = context.obtainStyledAttributes(attributeSet, R.styleable.SelectTimeSpanView)
|
||||
|
@ -56,77 +67,76 @@ class SelectTimeSpanView(context: Context, attributeSet: AttributeSet? = null):
|
|||
attributes.recycle()
|
||||
|
||||
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() {
|
||||
val totalMinutes = (timeInMillis / (1000 * 60)).toInt()
|
||||
val totalHours = totalMinutes / 60
|
||||
val totalDays = totalHours / 24
|
||||
val minutes = totalMinutes % 60
|
||||
val hours = totalHours % 24
|
||||
val duration = Duration.decode(timeInMillis)
|
||||
|
||||
binding.days = totalDays
|
||||
binding.minutes = minutes
|
||||
binding.hours = hours
|
||||
daysText.text = TimeTextUtil.days(duration.days, context!!)
|
||||
minutesText.text = TimeTextUtil.minutes(duration.minutes, context!!)
|
||||
hoursText.text = TimeTextUtil.hours(duration.hours, context!!)
|
||||
|
||||
binding.daysText = TimeTextUtil.days(totalDays, context!!)
|
||||
binding.minutesText = TimeTextUtil.minutes(minutes, context!!)
|
||||
binding.hoursText = TimeTextUtil.hours(hours, context!!)
|
||||
minutePicker.value = duration.minutes
|
||||
minuteSeekbar.progress = duration.minutes
|
||||
|
||||
binding.minutePicker.value = binding.minutes ?: 0
|
||||
binding.hourPicker.value = binding.hours ?: 0
|
||||
binding.dayPicker.value = binding.days ?: 0
|
||||
}
|
||||
hourPicker.value = duration.hours
|
||||
hourSeekbar.progress = duration.hours
|
||||
|
||||
private fun readStatusFromBinding() {
|
||||
val days = binding.days!!.toLong()
|
||||
val hours = binding.hours!!.toLong()
|
||||
val minutes = binding.minutes!!.toLong()
|
||||
|
||||
timeInMillis = (((days * 24) + hours) * 60 + minutes) * 1000 * 60
|
||||
dayPicker.value = duration.days
|
||||
daySeekbar.progress = duration.days
|
||||
}
|
||||
|
||||
fun clearNumberPickerFocus() {
|
||||
binding.minutePicker.clearFocus()
|
||||
binding.hourPicker.clearFocus()
|
||||
binding.dayPicker.clearFocus()
|
||||
minutePicker.clearFocus()
|
||||
hourPicker.clearFocus()
|
||||
dayPicker.clearFocus()
|
||||
}
|
||||
|
||||
fun enablePickerMode(enable: Boolean) {
|
||||
binding.seekbarContainer.visibility = if (enable) View.GONE else View.VISIBLE
|
||||
binding.pickerContainer.visibility = if (enable) View.VISIBLE else View.GONE
|
||||
seekbarContainer.visibility = if (enable) View.GONE else View.VISIBLE
|
||||
pickerContainer.visibility = if (enable) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
init {
|
||||
binding.minutePicker.minValue = 0
|
||||
binding.minutePicker.maxValue = 59
|
||||
minutePicker.minValue = 0
|
||||
minutePicker.maxValue = 59
|
||||
|
||||
binding.hourPicker.minValue = 0
|
||||
binding.hourPicker.maxValue = 23
|
||||
hourPicker.minValue = 0
|
||||
hourPicker.maxValue = 23
|
||||
|
||||
binding.dayPicker.minValue = 0
|
||||
binding.dayPicker.maxValue = 1
|
||||
binding.dayPickerContainer.visibility = View.GONE
|
||||
dayPicker.minValue = 0
|
||||
dayPicker.maxValue = 1
|
||||
dayPickerContainer.visibility = View.GONE
|
||||
|
||||
binding.minutePicker.setOnValueChangedListener { _, _, newValue ->
|
||||
binding.minutes = newValue
|
||||
readStatusFromBinding()
|
||||
minutePicker.setOnValueChangedListener { _, _, newValue ->
|
||||
timeInMillis = Duration.decode(timeInMillis).copy(minutes = newValue).timeInMillis
|
||||
}
|
||||
|
||||
binding.hourPicker.setOnValueChangedListener { _, _, newValue ->
|
||||
binding.hours = newValue
|
||||
readStatusFromBinding()
|
||||
hourPicker.setOnValueChangedListener { _, _, newValue ->
|
||||
timeInMillis = Duration.decode(timeInMillis).copy(hours = newValue).timeInMillis
|
||||
}
|
||||
|
||||
binding.dayPicker.setOnValueChangedListener { _, _, newValue ->
|
||||
binding.days = newValue
|
||||
readStatusFromBinding()
|
||||
dayPicker.setOnValueChangedListener { _, _, newValue ->
|
||||
timeInMillis = Duration.decode(timeInMillis).copy(days = newValue).timeInMillis
|
||||
}
|
||||
|
||||
binding.daysSeek.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
|
||||
daySeekbar.setOnSeekBarChangeListener(object: SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
binding.days = progress
|
||||
readStatusFromBinding()
|
||||
timeInMillis = Duration.decode(timeInMillis).copy(days = progress).timeInMillis
|
||||
}
|
||||
|
||||
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) {
|
||||
binding.hours = progress
|
||||
readStatusFromBinding()
|
||||
timeInMillis = Duration.decode(timeInMillis).copy(hours = progress).timeInMillis
|
||||
}
|
||||
|
||||
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) {
|
||||
binding.minutes = progress
|
||||
readStatusFromBinding()
|
||||
timeInMillis = Duration.decode(timeInMillis).copy(minutes = progress).timeInMillis
|
||||
}
|
||||
|
||||
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) }
|
||||
binding.switchToSeekbarButton.setOnClickListener { listener?.setEnablePickerMode(false) }
|
||||
switchToPickerButton.setOnClickListener { listener?.setEnablePickerMode(true) }
|
||||
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
|
||||
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="minutes"
|
||||
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
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/seekbar_container"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/seekbar_container"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
<SeekBar
|
||||
android:id="@+id/minutes_seek"
|
||||
android:max="59"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/minutes_seek"
|
||||
android:progress="@{safeUnbox(minutes)}"
|
||||
android:max="59"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
<TextView
|
||||
android:id="@+id/minutes_text"
|
||||
tools:text="5 Minuten"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
tools:text="5 Minuten"
|
||||
android:text="@{minutesText}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
<SeekBar
|
||||
android:id="@+id/hours_seek"
|
||||
android:max="23"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/hours_seek"
|
||||
android:progress="@{safeUnbox(hours)}"
|
||||
android:max="23"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
<TextView
|
||||
tools:text="1 Stunde"
|
||||
android:id="@+id/hours_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
tools:text="1 Stunde"
|
||||
android:text="@{hoursText}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
<SeekBar
|
||||
android:id="@+id/days_seek"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<SeekBar
|
||||
android:visibility="@{safeUnbox(maxDays) > 0 ? View.VISIBLE : View.GONE}"
|
||||
android:id="@+id/days_seek"
|
||||
android:progress="@{safeUnbox(days)}"
|
||||
android:max="@{safeUnbox(maxDays)}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
<TextView
|
||||
tools:text="2 Tage"
|
||||
android:id="@+id/days_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
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>
|
||||
|
||||
</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
|
||||
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" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/picker_container"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<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
|
||||
android:id="@+id/picker_container"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/day_picker_container"
|
||||
android:orientation="vertical"
|
||||
<NumberPicker
|
||||
android:id="@+id/hour_picker"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="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
|
||||
android:orientation="vertical"
|
||||
<TextView
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:text="@string/select_time_span_view_hours"
|
||||
android:layout_width="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" />
|
||||
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>
|
||||
|
||||
</layout>
|
||||
</LinearLayout>
|
||||
|
|
|
@ -19,6 +19,13 @@
|
|||
<item
|
||||
app:showAsAction="ifRoom"
|
||||
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:title="@string/contacts_title_long"
|
||||
android:id="@+id/menu_manage_child_phone" />
|
||||
|
|
|
@ -107,8 +107,7 @@
|
|||
<fragment
|
||||
android:id="@+id/manageChildFragment"
|
||||
android:name="io.timelimit.android.ui.manage.child.ManageChildFragment"
|
||||
android:label="fragment_manage_child"
|
||||
tools:layout="@layout/fragment_manage_child" >
|
||||
android:label="fragment_manage_child">
|
||||
<action
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
|
@ -150,12 +149,18 @@
|
|||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_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
|
||||
android:id="@+id/manageCategoryFragment"
|
||||
android:name="io.timelimit.android.ui.manage.category.ManageCategoryFragment"
|
||||
android:label="fragment_manage_category"
|
||||
tools:layout="@layout/fragment_manage_category" >
|
||||
android:label="fragment_manage_category">
|
||||
<argument
|
||||
android:name="childId"
|
||||
app:argType="string" />
|
||||
|
@ -571,4 +576,12 @@
|
|||
android:name="categoryId"
|
||||
app:argType="string" />
|
||||
</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>
|
||||
|
|
|
@ -26,6 +26,9 @@
|
|||
<string name="generic_manage_card">Verwaltung</string>
|
||||
<string name="generic_enable">Aktivieren</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_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_action">Aktionen</string>
|
||||
<string name="lock_tab_task">Aufgaben</string>
|
||||
<string name="lock_header_blocked">Gesperrt!</string>
|
||||
|
||||
<string name="lock_header_what">Was wurde gesperrt?</string>
|
||||
|
@ -619,6 +623,18 @@
|
|||
</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_password_hint">Passwort</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_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>
|
||||
|
|
|
@ -13,7 +13,10 @@
|
|||
You should have received a copy of the GNU General Public License
|
||||
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_cancel">Cancel</string>
|
||||
<string name="generic_help">Help</string>
|
||||
|
@ -25,7 +28,10 @@
|
|||
<string name="generic_go">Go</string>
|
||||
<string name="generic_manage_card">Manage</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_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="blocked_time_areas" tools:ignore="MissingTranslation">
|
||||
<string name="blocked_time_areas">
|
||||
Blocked time areas
|
||||
</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.
|
||||
During a blocked time area, no Apps of the category can be used.
|
||||
You can define the time area to the minute.
|
||||
</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.
|
||||
</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.
|
||||
You can jump to a day using the day dropdown.
|
||||
</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.
|
||||
Then select the end time.
|
||||
Now, the area colors will be inverted.
|
||||
</string>
|
||||
|
||||
<string name="blocked_time_areas_snackbar_modified" tools:ignore="MissingTranslation">
|
||||
<string name="blocked_time_areas_snackbar_modified">
|
||||
The blocked time areas were changed
|
||||
</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_from" tools:ignore="MissingTranslation">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_other_days">Copy settings from one day to other days</string>
|
||||
<string name="blocked_time_areas_copy_from">Copy from</string>
|
||||
<string name="blocked_time_areas_copy_to">Copy from %s to</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
|
||||
</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_empty">The new name must not be empty</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_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_hmw" tools:ignore="MissingTranslation">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_hmw">Hide manipulation warning in the category list</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>
|
||||
|
||||
|
@ -574,8 +580,9 @@
|
|||
and it is not one - using an older version is enough to circumvent this.
|
||||
</string>
|
||||
|
||||
<string name="lock_tab_reason" tools:ignore="MissingTranslation">Reason</string>
|
||||
<string name="lock_tab_action" tools:ignore="MissingTranslation">Actions</string>
|
||||
<string name="lock_tab_reason">Reason</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_what">What was blocked?</string>
|
||||
|
@ -673,6 +680,17 @@
|
|||
</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_password_hint">Password</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="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>
|
||||
|
||||
|
@ -767,10 +785,10 @@
|
|||
|
||||
<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_option" tools:ignore="MissingTranslation">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_option" tools:ignore="MissingTranslation">disable limits temporarily</string>
|
||||
<string name="manage_child_special_mode_wizard_block_title">Block %s temporarily</string>
|
||||
<string name="manage_child_special_mode_wizard_block_option">block temporarily</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">disable limits temporarily</string>
|
||||
|
||||
<string name="manage_device_activity_level_blocking_title">Activity level blocking</string>
|
||||
<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_time">until a time</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_until" tools:ignore="MissingTranslation">until %s</string>
|
||||
<string name="manage_disable_time_limits_btn_no_end_time">no scheduled end time</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_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 -
|
||||
use parent and child categories to \"add\" an App to multiple categories
|
||||
</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
|
||||
for a part of a day is a better solution.
|
||||
</string>
|
||||
|
@ -1129,9 +1147,9 @@
|
|||
<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_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_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_parent">can change settings</string>
|
||||
|
||||
|
@ -1479,7 +1497,7 @@
|
|||
<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_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_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_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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue