Compare commits
18 Commits
v2.1.0
...
hotfix/2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
8b2adbf301
|
|||
|
88cc3a2a12
|
|||
|
26526a71a9
|
|||
|
11eb3713e5
|
|||
|
1df9cc7664
|
|||
|
b76da35752
|
|||
|
abead88ceb
|
|||
|
908eb4ac99
|
|||
|
71a05d598a
|
|||
|
2131fb3a3d
|
|||
|
1470dcd560
|
|||
|
471f977209
|
|||
|
2ba5f5fb98
|
|||
|
4de67bd27a
|
|||
|
0bb82a48a5
|
|||
|
d5a5273607
|
|||
|
40a4d254f5
|
|||
|
177d01edd9
|
2
.github/workflows/main.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
run: ./build.sh build
|
run: ./build.sh build
|
||||||
|
|
||||||
- name: Run Android tests
|
- name: Run Android tests
|
||||||
run: ./build.sh android-tests-parallel 23 24 25 26 27 28 30 31
|
run: ./build.sh android-tests-parallel 24 25 26 28 30 31
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
21
CHANGELOG.md
@@ -1,6 +1,25 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [2.1.0] -- Unreleased
|
## [2.1.2] -- 2023-05-26
|
||||||
|
### Fixed
|
||||||
|
- Fix bug that caused widget to enter checkmark on wrong date (@iSoron, #1541)
|
||||||
|
- Fix widget corners on Android 12 (@iSoron)
|
||||||
|
- Fix bug that caused notes to be lost when editing a checkmark (@iSoron, #1566)
|
||||||
|
- Prevent soft keyboard from covering entry popup (@iSoron)
|
||||||
|
- Accept comma (instead of dot) in certain locales (@iSoron)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Remove update delay after entering a checkmark (@iSoron)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Remove stack widgets (@iSoron)
|
||||||
|
|
||||||
|
|
||||||
|
## [2.1.1] -- 2022-09-24
|
||||||
|
### Fixed
|
||||||
|
- Fix Tasker plugin (@iSoron, #1503)
|
||||||
|
|
||||||
|
## [2.1.0] -- 2022-09-10
|
||||||
### Added
|
### Added
|
||||||
- Allow user to add notes to specific dates (@vbh, #1103)
|
- Allow user to add notes to specific dates (@vbh, #1103)
|
||||||
- Allow user to track "at most" numerical habits (@KristianTashkov, #1101)
|
- Allow user to track "at most" numerical habits (@KristianTashkov, #1101)
|
||||||
|
|||||||
16
build.sh
@@ -217,20 +217,28 @@ android_test_parallel() {
|
|||||||
(
|
(
|
||||||
LOG=build/android-test-$API.log
|
LOG=build/android-test-$API.log
|
||||||
log_info "API $API: Running tests..."
|
log_info "API $API: Running tests..."
|
||||||
if android_test $API 1>$LOG 2>&1; then
|
android_test $API 1>$LOG 2>&1
|
||||||
|
ret_code=$?
|
||||||
|
if [ $ret_code = 0 ]; then
|
||||||
log_info "API $API: Passed"
|
log_info "API $API: Passed"
|
||||||
else
|
else
|
||||||
log_error "API $API: Failed"
|
log_error "API $API: Failed"
|
||||||
fi
|
fi
|
||||||
pkill -9 -f ${AVD_PREFIX}${API}
|
pkill -9 -f ${AVD_PREFIX}${API}
|
||||||
|
exit $ret_code
|
||||||
)&
|
)&
|
||||||
PIDS+=" $!"
|
PIDS+=" $!"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Check exit codes
|
# Check exit codes
|
||||||
RET_CODE=0
|
success=0
|
||||||
for pid in $PIDS; do
|
for pid in $PIDS; do
|
||||||
wait $pid || RET_CODE=1
|
wait $pid
|
||||||
|
ret_code=$?
|
||||||
|
if [ $ret_code != 0 ]; then
|
||||||
|
success=1
|
||||||
|
fi
|
||||||
|
echo pid=$pid ret_code=$ret_code success=$success
|
||||||
done
|
done
|
||||||
|
|
||||||
# Print all logs
|
# Print all logs
|
||||||
@@ -240,7 +248,7 @@ android_test_parallel() {
|
|||||||
echo "::endgroup::"
|
echo "::endgroup::"
|
||||||
done
|
done
|
||||||
|
|
||||||
return $RET_CODE
|
return $success
|
||||||
}
|
}
|
||||||
|
|
||||||
android_build() {
|
android_build() {
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ android {
|
|||||||
compileSdk = 31
|
compileSdk = 31
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionCode = 20100
|
versionCode = 20102
|
||||||
versionName = "2.1.0"
|
versionName = "2.1.2"
|
||||||
minSdk = 23
|
minSdk = 23
|
||||||
targetSdk = 31
|
targetSdk = 31
|
||||||
applicationId = "org.isoron.uhabits"
|
applicationId = "org.isoron.uhabits"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
@@ -44,7 +44,7 @@ class EntryButtonViewTest : BaseViewTest() {
|
|||||||
view = component.getEntryButtonViewFactory().create().apply {
|
view = component.getEntryButtonViewFactory().create().apply {
|
||||||
value = Entry.NO
|
value = Entry.NO
|
||||||
color = PaletteUtils.getAndroidTestColor(5)
|
color = PaletteUtils.getAndroidTestColor(5)
|
||||||
onToggle = { _, _, _ -> toggled = true }
|
onToggle = { _, _ -> toggled = true }
|
||||||
onEdit = { edited = true }
|
onEdit = { edited = true }
|
||||||
}
|
}
|
||||||
measureView(view, dpToPixels(48), dpToPixels(48))
|
measureView(view, dpToPixels(48), dpToPixels(48))
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class EntryPanelViewTest : BaseViewTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun testToggle() {
|
fun testToggle() {
|
||||||
val timestamps = mutableListOf<Timestamp>()
|
val timestamps = mutableListOf<Timestamp>()
|
||||||
view.onToggle = { t, _, _, _ -> timestamps.add(t) }
|
view.onToggle = { t, _, _ -> timestamps.add(t) }
|
||||||
view.buttons[0].performLongClick()
|
view.buttons[0].performLongClick()
|
||||||
view.buttons[2].performLongClick()
|
view.buttons[2].performLongClick()
|
||||||
view.buttons[3].performLongClick()
|
view.buttons[3].performLongClick()
|
||||||
@@ -88,7 +88,7 @@ class EntryPanelViewTest : BaseViewTest() {
|
|||||||
fun testToggle_withOffset() {
|
fun testToggle_withOffset() {
|
||||||
val timestamps = mutableListOf<Timestamp>()
|
val timestamps = mutableListOf<Timestamp>()
|
||||||
view.dataOffset = 3
|
view.dataOffset = 3
|
||||||
view.onToggle = { t, _, _, _ -> timestamps += t }
|
view.onToggle = { t, _, _ -> timestamps += t }
|
||||||
view.buttons[0].performLongClick()
|
view.buttons[0].performLongClick()
|
||||||
view.buttons[2].performLongClick()
|
view.buttons[2].performLongClick()
|
||||||
view.buttons[3].performLongClick()
|
view.buttons[3].performLongClick()
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class CheckmarkWidgetViewTest : BaseViewTest() {
|
|||||||
@Before
|
@Before
|
||||||
override fun setUp() {
|
override fun setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
|
similarityCutoff = 0.00025
|
||||||
setTheme(R.style.WidgetTheme)
|
setTheme(R.style.WidgetTheme)
|
||||||
val habit = fixtures.createShortHabit()
|
val habit = fixtures.createShortHabit()
|
||||||
val computedEntries = habit.computedEntries
|
val computedEntries = habit.computedEntries
|
||||||
|
|||||||
@@ -270,7 +270,7 @@
|
|||||||
<!-- Locale/Tasker -->
|
<!-- Locale/Tasker -->
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".automation.FireSettingReceiver"
|
android:name=".automation.FireSettingReceiver"
|
||||||
android:exported="false">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|||||||
@@ -20,109 +20,60 @@
|
|||||||
package org.isoron.uhabits.activities.common.dialogs
|
package org.isoron.uhabits.activities.common.dialogs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.Context
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.View.GONE
|
import android.view.View.GONE
|
||||||
import android.view.View.VISIBLE
|
import android.view.View.VISIBLE
|
||||||
|
import androidx.appcompat.app.AppCompatDialogFragment
|
||||||
|
import org.isoron.uhabits.HabitsApplication
|
||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.core.models.Entry.Companion.NO
|
import org.isoron.uhabits.core.models.Entry.Companion.NO
|
||||||
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
|
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
|
||||||
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
|
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
|
||||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
|
|
||||||
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
|
||||||
import org.isoron.uhabits.core.preferences.Preferences
|
|
||||||
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
||||||
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
|
import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome
|
||||||
import org.isoron.uhabits.utils.dimBehind
|
|
||||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
|
||||||
import org.isoron.uhabits.utils.dp
|
|
||||||
import org.isoron.uhabits.utils.sres
|
import org.isoron.uhabits.utils.sres
|
||||||
|
|
||||||
const val POPUP_WIDTH = 4 * 48f + 16f
|
class CheckmarkDialog : AppCompatDialogFragment() {
|
||||||
const val POPUP_HEIGHT = 48f * 2.5f + 8f
|
|
||||||
|
|
||||||
class CheckmarkPopup(
|
|
||||||
private val context: Context,
|
|
||||||
private val color: Int,
|
|
||||||
private var notes: String,
|
|
||||||
private var value: Int,
|
|
||||||
private val prefs: Preferences,
|
|
||||||
private val anchor: View,
|
|
||||||
) {
|
|
||||||
var onToggle: (Int, String) -> Unit = { _, _ -> }
|
var onToggle: (Int, String) -> Unit = { _, _ -> }
|
||||||
private lateinit var dialog: Dialog
|
|
||||||
|
|
||||||
private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
// Required for round corners
|
val appComponent = (requireActivity().application as HabitsApplication).component
|
||||||
container.clipToOutline = true
|
val prefs = appComponent.preferences
|
||||||
}
|
val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
|
||||||
|
|
||||||
init {
|
|
||||||
view.booleanButtons.visibility = VISIBLE
|
|
||||||
initColors()
|
|
||||||
initTypefaces()
|
|
||||||
hideDisabledButtons()
|
|
||||||
populate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initColors() {
|
|
||||||
arrayOf(view.yesBtn, view.skipBtn).forEach {
|
arrayOf(view.yesBtn, view.skipBtn).forEach {
|
||||||
it.setTextColor(color)
|
it.setTextColor(requireArguments().getInt("color"))
|
||||||
}
|
}
|
||||||
arrayOf(view.noBtn, view.unknownBtn).forEach {
|
arrayOf(view.noBtn, view.unknownBtn).forEach {
|
||||||
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
|
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun initTypefaces() {
|
|
||||||
arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach {
|
arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach {
|
||||||
it.typeface = getFontAwesome(context)
|
it.typeface = getFontAwesome(requireContext())
|
||||||
}
|
}
|
||||||
}
|
view.notes.setText(requireArguments().getString("notes")!!)
|
||||||
|
|
||||||
private fun hideDisabledButtons() {
|
|
||||||
if (!prefs.isSkipEnabled) view.skipBtn.visibility = GONE
|
if (!prefs.isSkipEnabled) view.skipBtn.visibility = GONE
|
||||||
if (!prefs.areQuestionMarksEnabled) view.unknownBtn.visibility = GONE
|
if (!prefs.areQuestionMarksEnabled) view.unknownBtn.visibility = GONE
|
||||||
}
|
view.booleanButtons.visibility = VISIBLE
|
||||||
|
val dialog = Dialog(requireContext())
|
||||||
private fun populate() {
|
|
||||||
val selectedBtn = when (value) {
|
|
||||||
YES_MANUAL -> view.yesBtn
|
|
||||||
YES_AUTO -> view.noBtn
|
|
||||||
NO -> view.noBtn
|
|
||||||
UNKNOWN -> if (prefs.areQuestionMarksEnabled) view.unknownBtn else view.noBtn
|
|
||||||
SKIP -> if (prefs.isSkipEnabled) view.skipBtn else view.noBtn
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
view.notes.setText(notes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun show() {
|
|
||||||
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
|
|
||||||
dialog.setContentView(view.root)
|
dialog.setContentView(view.root)
|
||||||
dialog.window?.apply {
|
dialog.window?.apply {
|
||||||
setLayout(
|
|
||||||
view.root.dp(POPUP_WIDTH).toInt(),
|
|
||||||
view.root.dp(POPUP_HEIGHT).toInt()
|
|
||||||
)
|
|
||||||
setBackgroundDrawableResource(android.R.color.transparent)
|
setBackgroundDrawableResource(android.R.color.transparent)
|
||||||
}
|
}
|
||||||
fun onClick(v: Int) {
|
fun onClick(v: Int) {
|
||||||
this.value = v
|
val notes = view.notes.text.toString().trim()
|
||||||
save()
|
onToggle(v, notes)
|
||||||
|
requireDialog().dismiss()
|
||||||
}
|
}
|
||||||
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }
|
view.yesBtn.setOnClickListener { onClick(YES_MANUAL) }
|
||||||
view.noBtn.setOnClickListener { onClick(NO) }
|
view.noBtn.setOnClickListener { onClick(NO) }
|
||||||
view.skipBtn.setOnClickListener { onClick(SKIP) }
|
view.skipBtn.setOnClickListener { onClick(SKIP) }
|
||||||
view.unknownBtn.setOnClickListener { onClick(UNKNOWN) }
|
view.unknownBtn.setOnClickListener { onClick(UNKNOWN) }
|
||||||
dialog.setCanceledOnTouchOutside(true)
|
view.notes.setOnEditorActionListener { v, actionId, event ->
|
||||||
dialog.dimBehind()
|
onClick(requireArguments().getInt("value"))
|
||||||
dialog.dismissCurrentAndShow()
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun save() {
|
return dialog
|
||||||
onToggle(value, view.notes.text.toString().trim())
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package org.isoron.uhabits.activities.common.dialogs
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.method.DigitsKeyListener
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AppCompatDialogFragment
|
||||||
|
import org.isoron.uhabits.HabitsApplication
|
||||||
|
import org.isoron.uhabits.R
|
||||||
|
import org.isoron.uhabits.core.models.Entry
|
||||||
|
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
||||||
|
import org.isoron.uhabits.utils.InterfaceUtils
|
||||||
|
import org.isoron.uhabits.utils.requestFocusWithKeyboard
|
||||||
|
import org.isoron.uhabits.utils.sres
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import java.text.DecimalFormatSymbols
|
||||||
|
import java.text.NumberFormat
|
||||||
|
import java.text.ParseException
|
||||||
|
|
||||||
|
class NumberDialog : AppCompatDialogFragment() {
|
||||||
|
|
||||||
|
var onToggle: (Double, String) -> Unit = { _, _ -> }
|
||||||
|
var onDismiss: () -> Unit = {}
|
||||||
|
|
||||||
|
private var originalNotes: String = ""
|
||||||
|
private var originalValue: Double = 0.0
|
||||||
|
private lateinit var view: CheckmarkPopupBinding
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val appComponent = (requireActivity().application as HabitsApplication).component
|
||||||
|
val prefs = appComponent.preferences
|
||||||
|
view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context))
|
||||||
|
arrayOf(view.yesBtn, view.skipBtn).forEach {
|
||||||
|
it.setTextColor(requireArguments().getInt("color"))
|
||||||
|
}
|
||||||
|
arrayOf(view.noBtn, view.unknownBtn).forEach {
|
||||||
|
it.setTextColor(view.root.sres.getColor(R.attr.contrast60))
|
||||||
|
}
|
||||||
|
arrayOf(view.yesBtn, view.noBtn, view.skipBtn, view.unknownBtn).forEach {
|
||||||
|
it.typeface = InterfaceUtils.getFontAwesome(requireContext())
|
||||||
|
}
|
||||||
|
if (!prefs.isSkipEnabled) view.skipBtnNumber.visibility = View.GONE
|
||||||
|
view.numberButtons.visibility = View.VISIBLE
|
||||||
|
fixDecimalSeparator(view)
|
||||||
|
originalNotes = requireArguments().getString("notes")!!
|
||||||
|
originalValue = requireArguments().getDouble("value")
|
||||||
|
view.notes.setText(originalNotes)
|
||||||
|
view.value.setText(
|
||||||
|
when {
|
||||||
|
originalValue < 0.01 -> "0"
|
||||||
|
else -> DecimalFormat("#.##").format(originalValue)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
view.value.setOnKeyListener { _, keyCode, event ->
|
||||||
|
if (event.action == MotionEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||||
|
save()
|
||||||
|
return@setOnKeyListener true
|
||||||
|
}
|
||||||
|
return@setOnKeyListener false
|
||||||
|
}
|
||||||
|
view.saveBtn.setOnClickListener {
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
view.skipBtnNumber.setOnClickListener {
|
||||||
|
view.value.setText((Entry.SKIP.toDouble() / 1000).toString())
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
view.notes.setOnEditorActionListener { v, actionId, event ->
|
||||||
|
save()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
view.value.requestFocusWithKeyboard()
|
||||||
|
val dialog = Dialog(requireContext())
|
||||||
|
dialog.setContentView(view.root)
|
||||||
|
dialog.window?.apply {
|
||||||
|
setBackgroundDrawableResource(android.R.color.transparent)
|
||||||
|
}
|
||||||
|
dialog.setOnDismissListener { onDismiss() }
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fixDecimalSeparator(view: CheckmarkPopupBinding) {
|
||||||
|
// https://stackoverflow.com/a/34256139
|
||||||
|
val separator = DecimalFormatSymbols.getInstance().decimalSeparator
|
||||||
|
view.value.keyListener = DigitsKeyListener.getInstance("0123456789$separator")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save() {
|
||||||
|
var value = originalValue
|
||||||
|
try {
|
||||||
|
val numberFormat = NumberFormat.getInstance()
|
||||||
|
val valueStr = view.value.text.toString()
|
||||||
|
value = numberFormat.parse(valueStr)!!.toDouble()
|
||||||
|
} catch (e: ParseException) {
|
||||||
|
// NOP
|
||||||
|
}
|
||||||
|
val notes = view.notes.text.toString()
|
||||||
|
onToggle(value, notes)
|
||||||
|
requireDialog().dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker 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, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker 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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.uhabits.activities.common.dialogs
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.KeyEvent.KEYCODE_ENTER
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MotionEvent.ACTION_DOWN
|
|
||||||
import android.view.View
|
|
||||||
import android.view.View.GONE
|
|
||||||
import android.view.View.VISIBLE
|
|
||||||
import org.isoron.uhabits.core.models.Entry
|
|
||||||
import org.isoron.uhabits.core.preferences.Preferences
|
|
||||||
import org.isoron.uhabits.databinding.CheckmarkPopupBinding
|
|
||||||
import org.isoron.uhabits.utils.dimBehind
|
|
||||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
|
||||||
import org.isoron.uhabits.utils.dp
|
|
||||||
import org.isoron.uhabits.utils.requestFocusWithKeyboard
|
|
||||||
import java.text.DecimalFormat
|
|
||||||
|
|
||||||
class NumberPopup(
|
|
||||||
private val context: Context,
|
|
||||||
private var notes: String,
|
|
||||||
private var value: Double,
|
|
||||||
private val prefs: Preferences,
|
|
||||||
private val anchor: View,
|
|
||||||
) {
|
|
||||||
var onToggle: (Double, String) -> Unit = { _, _ -> }
|
|
||||||
var onDismiss: () -> Unit = {}
|
|
||||||
private val originalValue = value
|
|
||||||
private lateinit var dialog: Dialog
|
|
||||||
|
|
||||||
private val view = CheckmarkPopupBinding.inflate(LayoutInflater.from(context)).apply {
|
|
||||||
// Required for round corners
|
|
||||||
container.clipToOutline = true
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
view.numberButtons.visibility = VISIBLE
|
|
||||||
hideDisabledButtons()
|
|
||||||
populate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hideDisabledButtons() {
|
|
||||||
if (!prefs.isSkipEnabled) view.skipBtnNumber.visibility = GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun populate() {
|
|
||||||
view.notes.setText(notes)
|
|
||||||
view.value.setText(
|
|
||||||
when {
|
|
||||||
value < 0.01 -> "0"
|
|
||||||
else -> DecimalFormat("#.##").format(value)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun show() {
|
|
||||||
dialog = Dialog(context, android.R.style.Theme_NoTitleBar)
|
|
||||||
dialog.setContentView(view.root)
|
|
||||||
dialog.window?.apply {
|
|
||||||
setLayout(
|
|
||||||
view.root.dp(POPUP_WIDTH).toInt(),
|
|
||||||
view.root.dp(POPUP_HEIGHT).toInt()
|
|
||||||
)
|
|
||||||
setBackgroundDrawableResource(android.R.color.transparent)
|
|
||||||
}
|
|
||||||
dialog.setOnDismissListener {
|
|
||||||
onDismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
view.value.setOnKeyListener { _, keyCode, event ->
|
|
||||||
if (event.action == ACTION_DOWN && keyCode == KEYCODE_ENTER) {
|
|
||||||
save()
|
|
||||||
return@setOnKeyListener true
|
|
||||||
}
|
|
||||||
return@setOnKeyListener false
|
|
||||||
}
|
|
||||||
view.saveBtn.setOnClickListener {
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
view.skipBtnNumber.setOnClickListener {
|
|
||||||
view.value.setText((Entry.SKIP.toDouble() / 1000).toString())
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
view.value.requestFocusWithKeyboard()
|
|
||||||
dialog.setCanceledOnTouchOutside(true)
|
|
||||||
dialog.dimBehind()
|
|
||||||
dialog.dismissCurrentAndShow()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun save() {
|
|
||||||
val value = view.value.text.toString().toDoubleOrNull() ?: originalValue
|
|
||||||
val notes = view.notes.text.toString()
|
|
||||||
onToggle(value, notes)
|
|
||||||
dialog.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -40,6 +40,7 @@ import org.isoron.uhabits.inject.ActivityContextModule
|
|||||||
import org.isoron.uhabits.inject.DaggerHabitsActivityComponent
|
import org.isoron.uhabits.inject.DaggerHabitsActivityComponent
|
||||||
import org.isoron.uhabits.inject.HabitsActivityComponent
|
import org.isoron.uhabits.inject.HabitsActivityComponent
|
||||||
import org.isoron.uhabits.inject.HabitsApplicationComponent
|
import org.isoron.uhabits.inject.HabitsApplicationComponent
|
||||||
|
import org.isoron.uhabits.utils.dismissCurrentDialog
|
||||||
import org.isoron.uhabits.utils.restartWithFade
|
import org.isoron.uhabits.utils.restartWithFade
|
||||||
|
|
||||||
class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
||||||
@@ -91,6 +92,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
|||||||
midnightTimer.onPause()
|
midnightTimer.onPause()
|
||||||
screen.onDetached()
|
screen.onDetached()
|
||||||
adapter.cancelRefresh()
|
adapter.cancelRefresh()
|
||||||
|
dismissCurrentDialog()
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +101,7 @@ class ListHabitsActivity : AppCompatActivity(), Preferences.Listener {
|
|||||||
screen.onAttached()
|
screen.onAttached()
|
||||||
rootView.postInvalidate()
|
rootView.postInvalidate()
|
||||||
midnightTimer.onResume()
|
midnightTimer.onResume()
|
||||||
|
appComponent.reminderScheduler.scheduleAll()
|
||||||
taskRunner.run {
|
taskRunner.run {
|
||||||
try {
|
try {
|
||||||
AutoBackup(this@ListHabitsActivity).run()
|
AutoBackup(this@ListHabitsActivity).run()
|
||||||
|
|||||||
@@ -22,14 +22,15 @@ package org.isoron.uhabits.activities.habits.list
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import org.isoron.platform.gui.toInt
|
import org.isoron.platform.gui.toInt
|
||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup
|
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
|
||||||
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
|
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
|
||||||
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
||||||
import org.isoron.uhabits.activities.common.dialogs.NumberPopup
|
import org.isoron.uhabits.activities.common.dialogs.NumberDialog
|
||||||
import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
|
import org.isoron.uhabits.activities.habits.edit.HabitTypeDialog
|
||||||
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
|
import org.isoron.uhabits.activities.habits.list.views.HabitCardListAdapter
|
||||||
import org.isoron.uhabits.core.commands.ArchiveHabitsCommand
|
import org.isoron.uhabits.core.commands.ArchiveHabitsCommand
|
||||||
@@ -233,17 +234,14 @@ class ListHabitsScreen
|
|||||||
notes: String,
|
notes: String,
|
||||||
callback: ListHabitsBehavior.NumberPickerCallback
|
callback: ListHabitsBehavior.NumberPickerCallback
|
||||||
) {
|
) {
|
||||||
val view = rootView.get()
|
val fm = (context as AppCompatActivity).supportFragmentManager
|
||||||
NumberPopup(
|
val dialog = NumberDialog()
|
||||||
context = context,
|
dialog.arguments = Bundle().apply {
|
||||||
prefs = preferences,
|
putDouble("value", value)
|
||||||
anchor = view,
|
putString("notes", notes)
|
||||||
notes = notes,
|
|
||||||
value = value,
|
|
||||||
).apply {
|
|
||||||
onToggle = { value, notes -> callback.onNumberPicked(value, notes) }
|
|
||||||
show()
|
|
||||||
}
|
}
|
||||||
|
dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) }
|
||||||
|
dialog.dismissCurrentAndShow(fm, "numberDialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showCheckmarkPopup(
|
override fun showCheckmarkPopup(
|
||||||
@@ -252,18 +250,16 @@ class ListHabitsScreen
|
|||||||
color: PaletteColor,
|
color: PaletteColor,
|
||||||
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
||||||
) {
|
) {
|
||||||
val view = rootView.get()
|
val theme = rootView.get().currentTheme()
|
||||||
CheckmarkPopup(
|
val fm = (context as AppCompatActivity).supportFragmentManager
|
||||||
context = context,
|
val dialog = CheckmarkDialog()
|
||||||
prefs = preferences,
|
dialog.arguments = Bundle().apply {
|
||||||
anchor = view,
|
putInt("color", theme.color(color).toInt())
|
||||||
color = view.currentTheme().color(color).toInt(),
|
putInt("value", selectedValue)
|
||||||
notes = notes,
|
putString("notes", notes)
|
||||||
value = selectedValue,
|
|
||||||
).apply {
|
|
||||||
onToggle = { value, notes -> callback.onNotesSaved(value, notes) }
|
|
||||||
show()
|
|
||||||
}
|
}
|
||||||
|
dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) }
|
||||||
|
dialog.dismissCurrentAndShow(fm, "checkmarkDialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getExecuteString(command: Command): String? {
|
private fun getExecuteString(command: Command): String? {
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ import org.isoron.uhabits.utils.sres
|
|||||||
import org.isoron.uhabits.utils.toMeasureSpec
|
import org.isoron.uhabits.utils.toMeasureSpec
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
const val TOGGLE_DELAY_MILLIS = 2000L
|
|
||||||
|
|
||||||
class CheckmarkButtonViewFactory
|
class CheckmarkButtonViewFactory
|
||||||
@Inject constructor(
|
@Inject constructor(
|
||||||
@ActivityContext val context: Context,
|
@ActivityContext val context: Context,
|
||||||
@@ -79,7 +77,7 @@ class CheckmarkButtonView(
|
|||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
var onToggle: (Int, String, Long) -> Unit = { _, _, _ -> }
|
var onToggle: (Int, String) -> Unit = { _, _ -> }
|
||||||
|
|
||||||
var onEdit: () -> Unit = { }
|
var onEdit: () -> Unit = { }
|
||||||
|
|
||||||
@@ -90,25 +88,25 @@ class CheckmarkButtonView(
|
|||||||
setOnLongClickListener(this)
|
setOnLongClickListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun performToggle(delay: Long) {
|
fun performToggle() {
|
||||||
value = Entry.nextToggleValue(
|
value = Entry.nextToggleValue(
|
||||||
value = value,
|
value = value,
|
||||||
isSkipEnabled = preferences.isSkipEnabled,
|
isSkipEnabled = preferences.isSkipEnabled,
|
||||||
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
|
areQuestionMarksEnabled = preferences.areQuestionMarksEnabled
|
||||||
)
|
)
|
||||||
onToggle(value, notes, delay)
|
onToggle(value, notes)
|
||||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
override fun onClick(v: View) {
|
||||||
if (preferences.isShortToggleEnabled) performToggle(TOGGLE_DELAY_MILLIS)
|
if (preferences.isShortToggleEnabled) performToggle()
|
||||||
else onEdit()
|
else onEdit()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLongClick(v: View): Boolean {
|
override fun onLongClick(v: View): Boolean {
|
||||||
if (preferences.isShortToggleEnabled) onEdit()
|
if (preferences.isShortToggleEnabled) onEdit()
|
||||||
else performToggle(TOGGLE_DELAY_MILLIS)
|
else performToggle()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class CheckmarkPanelView(
|
|||||||
setupButtons()
|
setupButtons()
|
||||||
}
|
}
|
||||||
|
|
||||||
var onToggle: (Timestamp, Int, String, Long) -> Unit = { _, _, _, _ -> }
|
var onToggle: (Timestamp, Int, String) -> Unit = { _, _, _ -> }
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
setupButtons()
|
setupButtons()
|
||||||
@@ -89,7 +89,7 @@ class CheckmarkPanelView(
|
|||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
button.color = color
|
button.color = color
|
||||||
button.onToggle = { value, notes, delay -> onToggle(timestamp, value, notes, delay) }
|
button.onToggle = { value, notes -> onToggle(timestamp, value, notes) }
|
||||||
button.onEdit = { onEdit(timestamp) }
|
button.onEdit = { onEdit(timestamp) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,13 +57,6 @@ class HabitCardViewFactory
|
|||||||
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
|
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class DelayedToggle(
|
|
||||||
var habit: Habit,
|
|
||||||
var timestamp: Timestamp,
|
|
||||||
var value: Int,
|
|
||||||
var notes: String
|
|
||||||
)
|
|
||||||
|
|
||||||
class HabitCardView(
|
class HabitCardView(
|
||||||
@ActivityContext context: Context,
|
@ActivityContext context: Context,
|
||||||
checkmarkPanelFactory: CheckmarkPanelViewFactory,
|
checkmarkPanelFactory: CheckmarkPanelViewFactory,
|
||||||
@@ -136,7 +129,6 @@ class HabitCardView(
|
|||||||
private var scoreRing: RingView
|
private var scoreRing: RingView
|
||||||
|
|
||||||
private var currentToggleTaskId = 0
|
private var currentToggleTaskId = 0
|
||||||
private var queuedToggles = mutableListOf<DelayedToggle>()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scoreRing = RingView(context).apply {
|
scoreRing = RingView(context).apply {
|
||||||
@@ -160,12 +152,9 @@ class HabitCardView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
||||||
onToggle = { timestamp, value, notes, delay ->
|
onToggle = { timestamp, value, notes ->
|
||||||
if (delay > 0) triggerRipple(timestamp)
|
triggerRipple(timestamp)
|
||||||
habit?.let {
|
habit?.let { behavior.onToggle(it, timestamp, value, notes) }
|
||||||
val taskId = queueToggle(it, timestamp, value, notes);
|
|
||||||
{ runPendingToggles(taskId) }.delay(delay)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
onEdit = { timestamp ->
|
onEdit = { timestamp ->
|
||||||
triggerRipple(timestamp)
|
triggerRipple(timestamp)
|
||||||
@@ -205,25 +194,6 @@ class HabitCardView(
|
|||||||
addView(innerFrame)
|
addView(innerFrame)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun runPendingToggles(id: Int) {
|
|
||||||
if (currentToggleTaskId != id) return
|
|
||||||
for ((h, t, v, n) in queuedToggles) behavior.onToggle(h, t, v, n)
|
|
||||||
queuedToggles.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun queueToggle(
|
|
||||||
it: Habit,
|
|
||||||
timestamp: Timestamp,
|
|
||||||
value: Int,
|
|
||||||
notes: String,
|
|
||||||
): Int {
|
|
||||||
currentToggleTaskId += 1
|
|
||||||
queuedToggles.add(DelayedToggle(it, timestamp, value, notes))
|
|
||||||
return currentToggleTaskId
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onModelChange() {
|
override fun onModelChange() {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
habit?.let { copyAttributesFrom(it) }
|
habit?.let { copyAttributesFrom(it) }
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ import org.isoron.uhabits.HabitsApplication
|
|||||||
import org.isoron.uhabits.R
|
import org.isoron.uhabits.R
|
||||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||||
import org.isoron.uhabits.activities.HabitsDirFinder
|
import org.isoron.uhabits.activities.HabitsDirFinder
|
||||||
import org.isoron.uhabits.activities.common.dialogs.CheckmarkPopup
|
import org.isoron.uhabits.activities.common.dialogs.CheckmarkDialog
|
||||||
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
|
||||||
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
|
import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
|
||||||
import org.isoron.uhabits.activities.common.dialogs.NumberPopup
|
import org.isoron.uhabits.activities.common.dialogs.NumberDialog
|
||||||
import org.isoron.uhabits.core.commands.Command
|
import org.isoron.uhabits.core.commands.Command
|
||||||
import org.isoron.uhabits.core.commands.CommandRunner
|
import org.isoron.uhabits.core.commands.CommandRunner
|
||||||
import org.isoron.uhabits.core.models.Habit
|
import org.isoron.uhabits.core.models.Habit
|
||||||
@@ -51,6 +51,7 @@ import org.isoron.uhabits.core.ui.views.OnDateClickedListener
|
|||||||
import org.isoron.uhabits.intents.IntentFactory
|
import org.isoron.uhabits.intents.IntentFactory
|
||||||
import org.isoron.uhabits.utils.currentTheme
|
import org.isoron.uhabits.utils.currentTheme
|
||||||
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
import org.isoron.uhabits.utils.dismissCurrentAndShow
|
||||||
|
import org.isoron.uhabits.utils.dismissCurrentDialog
|
||||||
import org.isoron.uhabits.utils.showMessage
|
import org.isoron.uhabits.utils.showMessage
|
||||||
import org.isoron.uhabits.utils.showSendFileScreen
|
import org.isoron.uhabits.utils.showSendFileScreen
|
||||||
import org.isoron.uhabits.widgets.WidgetUpdater
|
import org.isoron.uhabits.widgets.WidgetUpdater
|
||||||
@@ -129,6 +130,7 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
dismissCurrentDialog()
|
||||||
commandRunner.removeListener(this)
|
commandRunner.removeListener(this)
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
@@ -170,41 +172,32 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
|
|||||||
override fun showNumberPopup(
|
override fun showNumberPopup(
|
||||||
value: Double,
|
value: Double,
|
||||||
notes: String,
|
notes: String,
|
||||||
preferences: Preferences,
|
|
||||||
callback: ListHabitsBehavior.NumberPickerCallback
|
callback: ListHabitsBehavior.NumberPickerCallback
|
||||||
) {
|
) {
|
||||||
val anchor = getPopupAnchor() ?: return
|
val dialog = NumberDialog()
|
||||||
NumberPopup(
|
dialog.arguments = Bundle().apply {
|
||||||
context = this@ShowHabitActivity,
|
putDouble("value", value)
|
||||||
prefs = preferences,
|
putString("notes", notes)
|
||||||
notes = notes,
|
|
||||||
anchor = anchor,
|
|
||||||
value = value,
|
|
||||||
).apply {
|
|
||||||
onToggle = { v, n -> callback.onNumberPicked(v, n) }
|
|
||||||
show()
|
|
||||||
}
|
}
|
||||||
|
dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) }
|
||||||
|
dialog.dismissCurrentAndShow(supportFragmentManager, "numberDialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showCheckmarkPopup(
|
override fun showCheckmarkPopup(
|
||||||
selectedValue: Int,
|
selectedValue: Int,
|
||||||
notes: String,
|
notes: String,
|
||||||
preferences: Preferences,
|
|
||||||
color: PaletteColor,
|
color: PaletteColor,
|
||||||
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
callback: ListHabitsBehavior.CheckMarkDialogCallback
|
||||||
) {
|
) {
|
||||||
val anchor = getPopupAnchor() ?: return
|
val theme = view.currentTheme()
|
||||||
CheckmarkPopup(
|
val dialog = CheckmarkDialog()
|
||||||
context = this@ShowHabitActivity,
|
dialog.arguments = Bundle().apply {
|
||||||
prefs = preferences,
|
putInt("color", theme.color(color).toInt())
|
||||||
notes = notes,
|
putInt("value", selectedValue)
|
||||||
color = view.currentTheme().color(color).toInt(),
|
putString("notes", notes)
|
||||||
anchor = anchor,
|
|
||||||
value = selectedValue,
|
|
||||||
).apply {
|
|
||||||
onToggle = { v, n -> callback.onNotesSaved(v, n) }
|
|
||||||
show()
|
|
||||||
}
|
}
|
||||||
|
dialog.onToggle = { v, n -> callback.onNotesSaved(v, n) }
|
||||||
|
dialog.dismissCurrentAndShow(supportFragmentManager, "checkmarkDialog")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPopupAnchor(): View? {
|
private fun getPopupAnchor(): View? {
|
||||||
|
|||||||
@@ -6,16 +6,24 @@ import androidx.fragment.app.FragmentManager
|
|||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
var currentDialog: WeakReference<Dialog> = WeakReference(null)
|
var currentDialog: WeakReference<Dialog> = WeakReference(null)
|
||||||
|
var currentDialogFragment: WeakReference<DialogFragment> = WeakReference(null)
|
||||||
|
|
||||||
|
fun dismissCurrentDialog() {
|
||||||
|
currentDialog.get()?.dismiss()
|
||||||
|
currentDialog = WeakReference(null)
|
||||||
|
currentDialogFragment.get()?.dismiss()
|
||||||
|
currentDialogFragment = WeakReference(null)
|
||||||
|
}
|
||||||
|
|
||||||
fun Dialog.dismissCurrentAndShow() {
|
fun Dialog.dismissCurrentAndShow() {
|
||||||
currentDialog.get()?.dismiss()
|
dismissCurrentDialog()
|
||||||
currentDialog = WeakReference(this)
|
currentDialog = WeakReference(this)
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun DialogFragment.dismissCurrentAndShow(fragmentManager: FragmentManager, tag: String) {
|
fun DialogFragment.dismissCurrentAndShow(fragmentManager: FragmentManager, tag: String) {
|
||||||
currentDialog.get()?.dismiss()
|
dismissCurrentDialog()
|
||||||
|
currentDialogFragment = WeakReference(this)
|
||||||
show(fragmentManager, tag)
|
show(fragmentManager, tag)
|
||||||
fragmentManager.executePendingTransactions()
|
fragmentManager.executePendingTransactions()
|
||||||
currentDialog = WeakReference(this.dialog)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ open class CheckmarkWidget(
|
|||||||
|
|
||||||
override fun getOnClickPendingIntent(context: Context): PendingIntent? {
|
override fun getOnClickPendingIntent(context: Context): PendingIntent? {
|
||||||
return if (habit.isNumerical) {
|
return if (habit.isNumerical) {
|
||||||
pendingIntentFactory.showNumberPicker(habit, DateUtils.getToday())
|
pendingIntentFactory.showNumberPicker(habit, DateUtils.getTodayWithOffset())
|
||||||
} else {
|
} else {
|
||||||
pendingIntentFactory.toggleCheckmark(habit, null)
|
pendingIntentFactory.toggleCheckmark(habit, null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import org.isoron.uhabits.R
|
|||||||
import org.isoron.uhabits.core.models.Habit
|
import org.isoron.uhabits.core.models.Habit
|
||||||
import org.isoron.uhabits.core.models.HabitNotFoundException
|
import org.isoron.uhabits.core.models.HabitNotFoundException
|
||||||
import org.isoron.uhabits.core.preferences.Preferences
|
import org.isoron.uhabits.core.preferences.Preferences
|
||||||
import org.isoron.uhabits.core.utils.DateUtils.Companion.getToday
|
import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset
|
||||||
import org.isoron.uhabits.intents.IntentFactory
|
import org.isoron.uhabits.intents.IntentFactory
|
||||||
import org.isoron.uhabits.intents.PendingIntentFactory
|
import org.isoron.uhabits.intents.PendingIntentFactory
|
||||||
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
|
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
|
||||||
@@ -101,7 +101,7 @@ internal class StackRemoteViewsFactory(private val context: Context, intent: Int
|
|||||||
val landscapeViews = widget.landscapeRemoteViews
|
val landscapeViews = widget.landscapeRemoteViews
|
||||||
val portraitViews = widget.portraitRemoteViews
|
val portraitViews = widget.portraitRemoteViews
|
||||||
val factory = PendingIntentFactory(context, IntentFactory())
|
val factory = PendingIntentFactory(context, IntentFactory())
|
||||||
val intent = StackWidgetType.getIntentFillIn(factory, widgetType, h, habits, getToday())
|
val intent = StackWidgetType.getIntentFillIn(factory, widgetType, h, habits, getTodayWithOffset())
|
||||||
landscapeViews.setOnClickFillInIntent(R.id.button, intent)
|
landscapeViews.setOnClickFillInIntent(R.id.button, intent)
|
||||||
portraitViews.setOnClickFillInIntent(R.id.button, intent)
|
portraitViews.setOnClickFillInIntent(R.id.button, intent)
|
||||||
val remoteViews = RemoteViews(landscapeViews, portraitViews)
|
val remoteViews = RemoteViews(landscapeViews, portraitViews)
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
|
|||||||
import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID
|
import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.AbsListView.CHOICE_MODE_MULTIPLE
|
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ListView
|
import android.widget.ListView
|
||||||
@@ -34,7 +33,6 @@ import org.isoron.uhabits.R
|
|||||||
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
import org.isoron.uhabits.activities.AndroidThemeSwitcher
|
||||||
import org.isoron.uhabits.core.preferences.WidgetPreferences
|
import org.isoron.uhabits.core.preferences.WidgetPreferences
|
||||||
import org.isoron.uhabits.widgets.WidgetUpdater
|
import org.isoron.uhabits.widgets.WidgetUpdater
|
||||||
import java.util.ArrayList
|
|
||||||
|
|
||||||
class BooleanHabitPickerDialog : HabitPickerDialog() {
|
class BooleanHabitPickerDialog : HabitPickerDialog() {
|
||||||
override fun shouldHideNumerical() = true
|
override fun shouldHideNumerical() = true
|
||||||
@@ -88,20 +86,12 @@ open class HabitPickerDialog : Activity() {
|
|||||||
with(listView) {
|
with(listView) {
|
||||||
adapter = ArrayAdapter(
|
adapter = ArrayAdapter(
|
||||||
context,
|
context,
|
||||||
android.R.layout.simple_list_item_multiple_choice,
|
android.R.layout.simple_list_item_1,
|
||||||
habitNames
|
habitNames
|
||||||
)
|
)
|
||||||
choiceMode = CHOICE_MODE_MULTIPLE
|
setOnItemClickListener { parent, view, position, id ->
|
||||||
itemsCanFocus = false
|
confirm(mutableListOf(habitIds[position]))
|
||||||
}
|
|
||||||
saveButton.setOnClickListener {
|
|
||||||
val selectedIds = mutableListOf<Long>()
|
|
||||||
for (i in 0..listView.count) {
|
|
||||||
if (listView.isItemChecked(i)) {
|
|
||||||
selectedIds.add(habitIds[i])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
confirm(selectedIds)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,23 +66,21 @@ class CheckmarkWidgetView : HabitWidgetView {
|
|||||||
val res = StyledResources(context)
|
val res = StyledResources(context)
|
||||||
val bgColor: Int
|
val bgColor: Int
|
||||||
val fgColor: Int
|
val fgColor: Int
|
||||||
|
setShadowAlpha(0x4f)
|
||||||
when (entryState) {
|
when (entryState) {
|
||||||
YES_MANUAL, SKIP -> {
|
YES_MANUAL, SKIP -> {
|
||||||
bgColor = activeColor
|
bgColor = activeColor
|
||||||
fgColor = res.getColor(R.attr.contrast0)
|
fgColor = res.getColor(R.attr.contrast0)
|
||||||
setShadowAlpha(0x4f)
|
|
||||||
backgroundPaint!!.color = bgColor
|
backgroundPaint!!.color = bgColor
|
||||||
frame!!.setBackgroundDrawable(background)
|
frame!!.setBackgroundDrawable(background)
|
||||||
}
|
}
|
||||||
YES_AUTO, NO, UNKNOWN -> {
|
YES_AUTO, NO, UNKNOWN -> {
|
||||||
bgColor = res.getColor(R.attr.cardBgColor)
|
bgColor = res.getColor(R.attr.cardBgColor)
|
||||||
fgColor = res.getColor(R.attr.contrast60)
|
fgColor = res.getColor(R.attr.contrast60)
|
||||||
setShadowAlpha(0x00)
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
bgColor = res.getColor(R.attr.cardBgColor)
|
bgColor = res.getColor(R.attr.cardBgColor)
|
||||||
fgColor = res.getColor(R.attr.contrast60)
|
fgColor = res.getColor(R.attr.contrast60)
|
||||||
setShadowAlpha(0x00)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ring.setPercentage(percentage)
|
ring.setPercentage(percentage)
|
||||||
@@ -126,7 +124,7 @@ class CheckmarkWidgetView : HabitWidgetView {
|
|||||||
} else {
|
} else {
|
||||||
width = min(width, height)
|
width = min(width, height)
|
||||||
}
|
}
|
||||||
val textSize = min(0.2f * width, getDimension(context, R.dimen.smallerTextSize))
|
val textSize = min(0.175f * width, getDimension(context, R.dimen.smallTextSize))
|
||||||
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
|
||||||
if (isNumerical) {
|
if (isNumerical) {
|
||||||
ring.setTextSize(textSize * 0.9f)
|
ring.setTextSize(textSize * 0.9f)
|
||||||
@@ -141,7 +139,8 @@ class CheckmarkWidgetView : HabitWidgetView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun init() {
|
private fun init() {
|
||||||
val appComponent: HabitsApplicationComponent = (context.applicationContext as HabitsApplication).component
|
val appComponent: HabitsApplicationComponent =
|
||||||
|
(context.applicationContext as HabitsApplication).component
|
||||||
preferences = appComponent.preferences
|
preferences = appComponent.preferences
|
||||||
ring = findViewById<View>(R.id.scoreRing) as RingView
|
ring = findViewById<View>(R.id.scoreRing) as RingView
|
||||||
label = findViewById<View>(R.id.label) as TextView
|
label = findViewById<View>(R.id.label) as TextView
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ abstract class HabitWidgetView : FrameLayout {
|
|||||||
val shadowRadius = dpToPixels(context, 2f).toInt()
|
val shadowRadius = dpToPixels(context, 2f).toInt()
|
||||||
val shadowOffset = dpToPixels(context, 1f).toInt()
|
val shadowOffset = dpToPixels(context, 1f).toInt()
|
||||||
val shadowColor = Color.argb(shadowAlpha, 0, 0, 0)
|
val shadowColor = Color.argb(shadowAlpha, 0, 0, 0)
|
||||||
val cornerRadius = dpToPixels(context, 5f)
|
val cornerRadius = dpToPixels(context, 12f)
|
||||||
val radii = FloatArray(8)
|
val radii = FloatArray(8)
|
||||||
Arrays.fill(radii, cornerRadius)
|
Arrays.fill(radii, cornerRadius)
|
||||||
val shape = RoundRectShape(radii, null, null)
|
val shape = RoundRectShape(radii, null, null)
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
2.1.1:
|
||||||
|
* Fix Tasker plugin
|
||||||
|
|
||||||
2.1:
|
2.1:
|
||||||
* Add notes to specific dates
|
* Add notes to specific dates
|
||||||
* Track at-most measurable habits
|
* Track at-most measurable habits
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<item android:id="@android:id/mask">
|
<item android:id="@android:id/mask">
|
||||||
<shape android:shape="rectangle">
|
<shape android:shape="rectangle">
|
||||||
<corners android:radius="5dp"/>
|
<corners android:radius="12dp"/>
|
||||||
<solid android:color="?android:colorPrimary"/>
|
<solid android:color="?android:colorPrimary"/>
|
||||||
</shape>
|
</shape>
|
||||||
<color android:color="@color/white"/>
|
<color android:color="@color/white"/>
|
||||||
|
|||||||
@@ -21,8 +21,10 @@
|
|||||||
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:id="@+id/container"
|
android:id="@+id/container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight="128dp"
|
||||||
|
android:minWidth="208dp"
|
||||||
app:divider="@drawable/checkmark_dialog_divider"
|
app:divider="@drawable/checkmark_dialog_divider"
|
||||||
app:showDividers="middle"
|
app:showDividers="middle"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
@@ -34,7 +36,7 @@
|
|||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:inputType="textCapSentences|textMultiLine"
|
android:inputType="textCapSentences"
|
||||||
android:textSize="@dimen/smallTextSize"
|
android:textSize="@dimen/smallTextSize"
|
||||||
android:padding="4dp"
|
android:padding="4dp"
|
||||||
android:background="@color/transparent"
|
android:background="@color/transparent"
|
||||||
|
|||||||
@@ -30,10 +30,4 @@
|
|||||||
android:layout_weight="1">
|
android:layout_weight="1">
|
||||||
</ListView>
|
</ListView>
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/buttonSave"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:text="@string/save"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
android:paddingTop="4dp"
|
android:paddingTop="4dp"
|
||||||
android:paddingLeft="8dp"
|
android:paddingLeft="8dp"
|
||||||
android:paddingRight="8dp"
|
android:paddingRight="8dp"
|
||||||
android:paddingBottom="4dp"
|
android:paddingBottom="8dp"
|
||||||
tools:ignore="UselessParent">
|
tools:ignore="UselessParent">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ class HabitsCSVExporter(
|
|||||||
|
|
||||||
val timeframe = getTimeframe()
|
val timeframe = getTimeframe()
|
||||||
val oldest = timeframe[0]
|
val oldest = timeframe[0]
|
||||||
val newest = DateUtils.getToday()
|
val newest = DateUtils.getTodayWithOffset()
|
||||||
val checkmarks: MutableList<ArrayList<Entry>> = ArrayList()
|
val checkmarks: MutableList<ArrayList<Entry>> = ArrayList()
|
||||||
val scores: MutableList<ArrayList<Score>> = ArrayList()
|
val scores: MutableList<ArrayList<Score>> = ArrayList()
|
||||||
for (habit in selectedHabits) {
|
for (habit in selectedHabits) {
|
||||||
|
|||||||
@@ -204,9 +204,16 @@ open class EntryList {
|
|||||||
// Copy original entries
|
// Copy original entries
|
||||||
original.forEach { entry ->
|
original.forEach { entry ->
|
||||||
val offset = entry.timestamp.daysUntil(to)
|
val offset = entry.timestamp.daysUntil(to)
|
||||||
if (result[offset].value == UNKNOWN || entry.value == SKIP || entry.value == YES_MANUAL) {
|
val value = if (
|
||||||
result[offset] = entry
|
result[offset].value == UNKNOWN ||
|
||||||
|
entry.value == SKIP ||
|
||||||
|
entry.value == YES_MANUAL
|
||||||
|
) {
|
||||||
|
entry.value
|
||||||
|
} else {
|
||||||
|
YES_AUTO
|
||||||
}
|
}
|
||||||
|
result[offset] = Entry(entry.timestamp, value, entry.notes)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class BarCardPresenter(
|
|||||||
} else {
|
} else {
|
||||||
boolBucketSizes[boolSpinnerPosition]
|
boolBucketSizes[boolSpinnerPosition]
|
||||||
}
|
}
|
||||||
val today = DateUtils.getToday()
|
val today = DateUtils.getTodayWithOffset()
|
||||||
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
|
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
|
||||||
val entries = habit.computedEntries.getByInterval(oldest, today).groupedSum(
|
val entries = habit.computedEntries.getByInterval(oldest, today).groupedSum(
|
||||||
truncateField = ScoreCardPresenter.getTruncateField(bucketSize),
|
truncateField = ScoreCardPresenter.getTruncateField(bucketSize),
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ class HistoryCardPresenter(
|
|||||||
screen.showCheckmarkPopup(
|
screen.showCheckmarkPopup(
|
||||||
entry.value,
|
entry.value,
|
||||||
entry.notes,
|
entry.notes,
|
||||||
preferences,
|
|
||||||
habit.color,
|
habit.color,
|
||||||
) { newValue, newNotes ->
|
) { newValue, newNotes ->
|
||||||
commandRunner.run(
|
commandRunner.run(
|
||||||
@@ -130,7 +129,6 @@ class HistoryCardPresenter(
|
|||||||
screen.showNumberPopup(
|
screen.showNumberPopup(
|
||||||
value = oldValue / 1000.0,
|
value = oldValue / 1000.0,
|
||||||
notes = entry.notes,
|
notes = entry.notes,
|
||||||
preferences = preferences,
|
|
||||||
) { newValue: Double, newNotes: String ->
|
) { newValue: Double, newNotes: String ->
|
||||||
val thousands = (newValue * 1000).roundToInt()
|
val thousands = (newValue * 1000).roundToInt()
|
||||||
commandRunner.run(
|
commandRunner.run(
|
||||||
@@ -203,13 +201,11 @@ class HistoryCardPresenter(
|
|||||||
fun showNumberPopup(
|
fun showNumberPopup(
|
||||||
value: Double,
|
value: Double,
|
||||||
notes: String,
|
notes: String,
|
||||||
preferences: Preferences,
|
|
||||||
callback: ListHabitsBehavior.NumberPickerCallback,
|
callback: ListHabitsBehavior.NumberPickerCallback,
|
||||||
)
|
)
|
||||||
fun showCheckmarkPopup(
|
fun showCheckmarkPopup(
|
||||||
selectedValue: Int,
|
selectedValue: Int,
|
||||||
notes: String,
|
notes: String,
|
||||||
preferences: Preferences,
|
|
||||||
color: PaletteColor,
|
color: PaletteColor,
|
||||||
callback: ListHabitsBehavior.CheckMarkDialogCallback,
|
callback: ListHabitsBehavior.CheckMarkDialogCallback,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ class EntryListTest {
|
|||||||
fun testAddFromInterval() {
|
fun testAddFromInterval() {
|
||||||
val entries = listOf(
|
val entries = listOf(
|
||||||
Entry(day(1), YES_MANUAL),
|
Entry(day(1), YES_MANUAL),
|
||||||
Entry(day(2), NO),
|
Entry(day(2), NO, "Test"),
|
||||||
Entry(day(4), NO),
|
Entry(day(4), NO),
|
||||||
Entry(day(5), YES_MANUAL),
|
Entry(day(5), YES_MANUAL),
|
||||||
Entry(day(10), YES_MANUAL),
|
Entry(day(10), YES_MANUAL),
|
||||||
@@ -230,7 +230,7 @@ class EntryListTest {
|
|||||||
)
|
)
|
||||||
val expected = listOf(
|
val expected = listOf(
|
||||||
Entry(day(1), YES_MANUAL),
|
Entry(day(1), YES_MANUAL),
|
||||||
Entry(day(2), YES_AUTO),
|
Entry(day(2), YES_AUTO, "Test"),
|
||||||
Entry(day(3), UNKNOWN),
|
Entry(day(3), UNKNOWN),
|
||||||
Entry(day(4), YES_AUTO),
|
Entry(day(4), YES_AUTO),
|
||||||
Entry(day(5), YES_MANUAL),
|
Entry(day(5), YES_MANUAL),
|
||||||
|
|||||||