Allow selecting date and time for special mode end times

This commit is contained in:
Jonas Lochmann 2023-07-31 02:00:00 +02:00
parent a367aeec27
commit 85b83d4fb9
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
6 changed files with 108 additions and 59 deletions

View file

@ -0,0 +1,21 @@
/*
* TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.extensions
import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId
fun LocalDateTime.toInstant(zone: ZoneId) = toInstant(zone.rules.getOffset(this))

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -30,12 +30,14 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.databinding.SpecialModeDialogBinding import io.timelimit.android.databinding.SpecialModeDialogBinding
import io.timelimit.android.extensions.showSafe import io.timelimit.android.extensions.showSafe
import io.timelimit.android.extensions.toInstant
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
import io.timelimit.android.ui.main.getActivityViewModel import io.timelimit.android.ui.main.getActivityViewModel
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
import org.threeten.bp.Instant import org.threeten.bp.Instant
import org.threeten.bp.LocalDate import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime import org.threeten.bp.LocalDateTime
import org.threeten.bp.LocalTime
import org.threeten.bp.ZoneId import org.threeten.bp.ZoneId
class SetCategorySpecialModeFragment: DialogFragment() { class SetCategorySpecialModeFragment: DialogFragment() {
@ -48,8 +50,8 @@ class SetCategorySpecialModeFragment: DialogFragment() {
private const val PAGE_TYPE = 0 private const val PAGE_TYPE = 0
private const val PAGE_SUGGESTION = 1 private const val PAGE_SUGGESTION = 1
private const val PAGE_CLOCK = 2 private const val PAGE_CALENDAR = 2
private const val PAGE_CALENDAR = 3 private const val PAGE_CLOCK = 3
fun newInstance(childId: String, categoryId: String, mode: SpecialModeDialogMode) = SetCategorySpecialModeFragment().apply { fun newInstance(childId: String, categoryId: String, mode: SpecialModeDialogMode) = SetCategorySpecialModeFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
@ -166,24 +168,22 @@ class SetCategorySpecialModeFragment: DialogFragment() {
} }
run { run {
fun readClockTime(timeZone: String, now: Long) = binding.timePicker.let { fun readClockTime(timeZone: String, localDate: LocalDate?, now: Long) = binding.timePicker.let {
LocalDateTime.ofInstant( val zoneId = ZoneId.of(timeZone)
Instant.ofEpochMilli(now),
ZoneId.of(timeZone) LocalDateTime.of(
) localDate ?: LocalDateTime.ofInstant(Instant.ofEpochMilli(now), zoneId).toLocalDate(),
.toLocalDate() LocalTime.of(it.currentHour, it.currentMinute)
.atStartOfDay(ZoneId.of(timeZone)) ).toInstant(zoneId).toEpochMilli()
.plusHours(it.currentHour.toLong())
.plusMinutes(it.currentMinute.toLong())
.toEpochSecond() * 1000
} }
fun update() { fun update() {
val content = model.content.value val content = model.content.value
val screen = content?.screen
val minTime = model.minTimestamp.value val minTime = model.minTimestamp.value
if (content?.screen is SetCategorySpecialModeModel.Screen.WithType.ClockScreen && minTime != null) { if (screen is SetCategorySpecialModeModel.Screen.WithType.ClockScreen && minTime != null) {
val currentSelectedTime = readClockTime(content.childTimezone, model.now()) val currentSelectedTime = readClockTime(content.childTimezone, screen.date, model.now())
val isEnabled = currentSelectedTime > minTime val isEnabled = currentSelectedTime > minTime
binding.confirmTimePickerButton.isEnabled = isEnabled binding.confirmTimePickerButton.isEnabled = isEnabled
@ -203,10 +203,8 @@ class SetCategorySpecialModeFragment: DialogFragment() {
} }
run { run {
fun readCalendarTime(timeZone: String) = binding.datePicker.let { fun readCalendarDate() = binding.datePicker.let {
LocalDate.of(it.year, it.month + 1, it.dayOfMonth) LocalDate.of(it.year, it.month + 1, it.dayOfMonth)
.atStartOfDay(ZoneId.of(timeZone))
.toEpochSecond() * 1000
} }
fun update() { fun update() {
@ -214,19 +212,23 @@ class SetCategorySpecialModeFragment: DialogFragment() {
val minTime = model.minTimestamp.value val minTime = model.minTimestamp.value
if (content?.screen is SetCategorySpecialModeModel.Screen.WithType.CalendarScreen && minTime != null) { if (content?.screen is SetCategorySpecialModeModel.Screen.WithType.CalendarScreen && minTime != null) {
val currentSelectedTime = readCalendarTime(content.childTimezone) val zoneId = ZoneId.of(content.childTimezone)
val isEnabled = currentSelectedTime > minTime val currentSelectedDate = readCalendarDate()
val currentStartOfDayTime = LocalDateTime.of(currentSelectedDate, LocalTime.MIN).toInstant(zoneId).toEpochMilli()
val currentEndOfDayTime = LocalDateTime.of(currentSelectedDate, LocalTime.of(23, 59)).toInstant(zoneId).toEpochMilli()
binding.confirmDatePickerButton.isEnabled = isEnabled val isConfirmEnabled = currentStartOfDayTime > minTime
val isClockEnabled = currentEndOfDayTime > minTime
if (isEnabled) { binding.confirmDatePickerButton.isEnabled = isConfirmEnabled
binding.confirmDatePickerButton.setOnClickListener { binding.timeOfDayDatePickerButton.isEnabled = isClockEnabled
val currentSelectedTimeNew = readCalendarTime(content.childTimezone)
if (currentSelectedTimeNew > minTime) { if (isConfirmEnabled) binding.confirmDatePickerButton.setOnClickListener {
model.applySelection(timeInMillis = currentSelectedTimeNew, auth = auth) model.applySelection(timeInMillis = currentStartOfDayTime, auth = auth)
} else update() }
}
if (isClockEnabled) binding.timeOfDayDatePickerButton.setOnClickListener {
model.openClockScreen(currentSelectedDate)
} }
} else binding.confirmDatePickerButton.isEnabled = false } else binding.confirmDatePickerButton.isEnabled = false
} }

View file

@ -26,6 +26,7 @@ import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.sync.actions.UpdateCategoryDisableLimitsAction import io.timelimit.android.sync.actions.UpdateCategoryDisableLimitsAction
import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction import io.timelimit.android.sync.actions.UpdateCategoryTemporarilyBlockedAction
import io.timelimit.android.ui.main.ActivityViewModel import io.timelimit.android.ui.main.ActivityViewModel
import org.threeten.bp.LocalDate
class SetCategorySpecialModeModel(application: Application): AndroidViewModel(application) { class SetCategorySpecialModeModel(application: Application): AndroidViewModel(application) {
private val logic = DefaultAppLogic.with(application) private val logic = DefaultAppLogic.with(application)
@ -113,7 +114,7 @@ class SetCategorySpecialModeModel(application: Application): AndroidViewModel(ap
options = options options = options
) )
} }
DurationSelection.Clock -> Screen.WithType.ClockScreen(type = type) is DurationSelection.Clock -> Screen.WithType.ClockScreen(type = type, date = durationSelection.date)
DurationSelection.Calendar -> Screen.WithType.CalendarScreen(type = type) DurationSelection.Calendar -> Screen.WithType.CalendarScreen(type = type)
} }
@ -127,11 +128,15 @@ class SetCategorySpecialModeModel(application: Application): AndroidViewModel(ap
} }
fun selectType(type: Type) { typeLive.value = type } fun selectType(type: Type) { typeLive.value = type }
fun openClockScreen() { durationSelectionLive.value = DurationSelection.Clock } fun openClockScreen(date: LocalDate? = null) { durationSelectionLive.value = DurationSelection.Clock(date) }
fun openCalendarScreen() { durationSelectionLive.value = DurationSelection.Calendar } fun openCalendarScreen() { durationSelectionLive.value = DurationSelection.Calendar }
fun goBack(): Boolean = if (durationSelectionLive.value != DurationSelection.SuggestionList) { fun goBack(): Boolean = if (durationSelectionLive.value != DurationSelection.SuggestionList) {
durationSelectionLive.value = DurationSelection.SuggestionList val v = durationSelectionLive.value
durationSelectionLive.value =
if (v is DurationSelection.Clock && v.date != null) DurationSelection.Calendar
else DurationSelection.SuggestionList
true true
} else if ( } else if (
@ -240,10 +245,10 @@ class SetCategorySpecialModeModel(application: Application): AndroidViewModel(ap
DisableLimits DisableLimits
} }
internal enum class DurationSelection { internal sealed class DurationSelection {
SuggestionList, object SuggestionList: DurationSelection()
Clock, data class Clock(val date: LocalDate?): DurationSelection()
Calendar object Calendar: DurationSelection()
} }
data class Content( data class Content(
@ -259,8 +264,8 @@ class SetCategorySpecialModeModel(application: Application): AndroidViewModel(ap
sealed class WithType: Screen() { sealed class WithType: Screen() {
abstract val type: Type abstract val type: Type
data class ClockScreen(override val type: Type, ): WithType() data class ClockScreen(override val type: Type, val date: LocalDate?): WithType()
data class CalendarScreen(override val type: Type, ): WithType() data class CalendarScreen(override val type: Type): WithType()
data class SuggestionList( data class SuggestionList(
override val type: Type, override val type: Type,

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- <!--
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
the Free Software Foundation version 3 of the License. the Free Software Foundation version 3 of the License.
@ -99,6 +99,46 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<DatePicker
android:layout_gravity="center_horizontal"
android:id="@+id/date_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
style="?borderlessButtonStyle"
android:layout_margin="8dp"
android:text="@string/manage_child_special_mode_wizard_time_of_day"
android:id="@+id/time_of_day_date_picker_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<View
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="1dp" />
<Button
android:layout_margin="8dp"
android:text="@string/generic_ok"
android:id="@+id/confirm_date_picker_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -119,27 +159,6 @@
</LinearLayout> </LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<DatePicker
android:layout_gravity="center_horizontal"
android:id="@+id/date_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:layout_margin="8dp"
android:layout_gravity="end"
android:text="@string/generic_ok"
android:id="@+id/confirm_date_picker_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</io.timelimit.android.ui.view.SafeViewFlipper> </io.timelimit.android.ui.view.SafeViewFlipper>
</LinearLayout> </LinearLayout>
</layout> </layout>

View file

@ -829,6 +829,7 @@
<string name="manage_child_special_mode_wizard_block_option">vorübergehend sperren</string> <string name="manage_child_special_mode_wizard_block_option">vorübergehend sperren</string>
<string name="manage_child_special_mode_wizard_disable_limits_title">Begrenzungen von %s deaktivieren</string> <string name="manage_child_special_mode_wizard_disable_limits_title">Begrenzungen von %s deaktivieren</string>
<string name="manage_child_special_mode_wizard_disable_limits_option">Begrenzungen vorübergehend deaktivieren</string> <string name="manage_child_special_mode_wizard_disable_limits_option">Begrenzungen vorübergehend deaktivieren</string>
<string name="manage_child_special_mode_wizard_time_of_day">Uhrzeit wählen</string>
<string name="manage_child_confirm_enable_limits_again_title">Begrenzungen aktivieren</string> <string name="manage_child_confirm_enable_limits_again_title">Begrenzungen aktivieren</string>
<string name="manage_child_confirm_enable_limits_again_text">Sollen die Begrenzungen für die Kategorie %s wirklich wieder aktiviert werden?</string> <string name="manage_child_confirm_enable_limits_again_text">Sollen die Begrenzungen für die Kategorie %s wirklich wieder aktiviert werden?</string>

View file

@ -883,6 +883,7 @@
<string name="manage_child_special_mode_wizard_block_option">block temporarily</string> <string name="manage_child_special_mode_wizard_block_option">block temporarily</string>
<string name="manage_child_special_mode_wizard_disable_limits_title">Disable limits for %s</string> <string name="manage_child_special_mode_wizard_disable_limits_title">Disable limits for %s</string>
<string name="manage_child_special_mode_wizard_disable_limits_option">disable limits temporarily</string> <string name="manage_child_special_mode_wizard_disable_limits_option">disable limits temporarily</string>
<string name="manage_child_special_mode_wizard_time_of_day">Select time</string>
<string name="manage_child_confirm_enable_limits_again_title">Enable limitations</string> <string name="manage_child_confirm_enable_limits_again_title">Enable limitations</string>
<string name="manage_child_confirm_enable_limits_again_text">Would you like to enable the limitations for the category %s?</string> <string name="manage_child_confirm_enable_limits_again_text">Would you like to enable the limitations for the category %s?</string>