second commit

This commit is contained in:
Елена Широкова 2026-05-08 02:31:02 +03:00
parent bdddb15989
commit 42713b3b89
21 changed files with 317 additions and 36 deletions

View File

@ -51,6 +51,7 @@ class SubtitleCardViewTest : BaseViewTest() {
color = PaletteColor(7),
frequency = Frequency(3, 7),
isNumerical = false,
isTimeCost = false,
question = "Did you meditate this morning?",
reminder = Reminder(8, 30, EVERY_DAY),
theme = LightTheme()

View File

@ -51,9 +51,24 @@ class NumberDialog : AppCompatDialogFragment() {
if (!prefs.areQuestionMarksEnabled) view.unknownBtnNumber.visibility = View.GONE
view.numberButtons.visibility = View.VISIBLE
fixDecimalSeparator(view)
val name = requireArguments().getString("habitName")
val unit = requireArguments().getString("unit")
if (name != null) {
view.habitName.text = name
view.habitName.visibility = View.VISIBLE
}
if (unit != null) {
view.unitLabel.text = unit
view.unitLabel.visibility = View.VISIBLE
}
originalNotes = requireArguments().getString("notes")!!
originalValue = requireArguments().getDouble("value")
view.notes.setText(originalNotes)
view.deleteBtn.visibility = if (originalValue > 0 || originalNotes.isNotEmpty()) View.VISIBLE else View.GONE
view.deleteBtn.setTextColor(view.root.sres.getColor(R.attr.contrast60))
view.deleteBtn.typeface = InterfaceUtils.getFontAwesome(requireContext())
view.value.setText(
when {
originalValue < 0.01 -> "0"
@ -70,6 +85,10 @@ class NumberDialog : AppCompatDialogFragment() {
view.saveBtn.setOnClickListener {
save()
}
view.deleteBtn.setOnClickListener {
onToggle(0.0, "")
requireDialog().dismiss()
}
view.skipBtnNumber.setOnClickListener {
view.value.setText(DecimalFormat("#.###").format((Entry.SKIP.toDouble() / 1000)))
save()

View File

@ -44,6 +44,7 @@ import org.isoron.uhabits.activities.common.dialogs.FrequencyPickerDialog
import org.isoron.uhabits.activities.common.dialogs.WeekdayPickerDialog
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateHabitCommand
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
import org.isoron.uhabits.core.commands.EditHabitCommand
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
@ -52,6 +53,7 @@ import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.models.WeekdayList
import org.isoron.platform.time.getToday
import org.isoron.uhabits.databinding.ActivityEditHabitBinding
import org.isoron.uhabits.utils.applyRootViewInsets
import org.isoron.uhabits.utils.applyToolbarInsets
@ -60,17 +62,26 @@ import org.isoron.uhabits.utils.formatTime
import org.isoron.uhabits.utils.toFormattedString
private data class UnitPreset(
val label: String,
val labelRes: Int,
val unit: String,
val minutesPerUnit: Int
)
private val TIME_COST_PRESETS = listOf(
UnitPreset("Банка (500 мл)", "банка", 30),
UnitPreset("Стакан (250 мл)", "стакан", 15),
UnitPreset("Глоток", "глоток", 1),
UnitPreset("Сигарета", "сигарета", 11),
UnitPreset("Затяжка", "затяжка", 1)
UnitPreset(R.string.time_cost_preset_cup_200, "cup_200", 12),
UnitPreset(R.string.time_cost_preset_small_cup_100, "cup_100", 6),
UnitPreset(R.string.time_cost_preset_large_cup_300, "cup_300", 18),
UnitPreset(R.string.time_cost_preset_sip_20, "sip_20", 1),
UnitPreset(R.string.time_cost_preset_shot_50, "shot_50", 3),
UnitPreset(R.string.time_cost_preset_glass_150, "glass_150", 9),
UnitPreset(R.string.time_cost_preset_can_500, "can_500", 30),
UnitPreset(R.string.time_cost_preset_bottle_1_5l, "bottle_1_5l", 90),
UnitPreset(R.string.time_cost_preset_sip_30, "sip_30", 2),
UnitPreset(R.string.time_cost_preset_small_can_250, "can_250", 15),
UnitPreset(R.string.time_cost_preset_medium_can_330, "can_330", 20),
UnitPreset(R.string.time_cost_preset_large_can_500, "can_500_large", 30),
UnitPreset(R.string.time_cost_preset_cigarette_1, "cigarette_1", 11),
UnitPreset(R.string.time_cost_preset_vape_puff_1, "vape_puff_1", 1)
)
fun formatFrequency(freqNum: Int, freqDen: Int, resources: Resources) = when {
@ -168,16 +179,16 @@ class EditHabitActivity : AppCompatActivity() {
}
HabitType.TIME_COST -> {
binding.nameInput.hint = getString(R.string.time_cost_short_example)
binding.questionInput.hint = getString(R.string.time_cost_question_example)
binding.questionOuterBox.visibility = View.GONE
binding.frequencyOuterBox.visibility = View.GONE
binding.targetOuterBox.visibility = View.GONE
binding.targetTypeOuterBox.visibility = View.GONE
binding.targetOuterBox.visibility = View.GONE
val preset = findPresetByUnit(unit)
?: findPresetByMinutes(minutesPerUnit)
?: TIME_COST_PRESETS.first()
minutesPerUnit = preset.minutesPerUnit
unit = preset.unit
binding.unitInput.setText(preset.label, false)
minutesPerUnit = preset.minutesPerUnit
binding.unitInput.setText(getPresetLabel(preset), false)
}
}
@ -303,7 +314,11 @@ class EditHabitActivity : AppCompatActivity() {
}
habit.name = binding.nameInput.text.trim().toString()
habit.question = binding.questionInput.text.trim().toString()
habit.question = if (habitType == HabitType.TIME_COST) {
""
} else {
binding.questionInput.text.trim().toString()
}
habit.description = binding.notesInput.text.trim().toString()
habit.color = color
if (reminderHour >= 0) {
@ -314,13 +329,15 @@ class EditHabitActivity : AppCompatActivity() {
habit.frequency = Frequency(freqNum, freqDen)
if (habitType == HabitType.NUMERICAL) {
habit.targetValue = binding.targetInput.text.toString().toDouble()
habit.targetValue = binding.targetInput.text.toString().toDoubleOrNull() ?: 0.0
habit.targetType = targetType
habit.unit = binding.unitInput.text.trim().toString()
} else if (habitType == HabitType.TIME_COST) {
val preset = selectedPreset() ?: findPresetByUnit(unit) ?: TIME_COST_PRESETS.first()
habit.unit = preset.unit
habit.minutesPerUnit = preset.minutesPerUnit
habit.targetValue = 0.0
habit.targetType = NumericalHabitType.AT_LEAST
}
habit.type = habitType
@ -338,6 +355,22 @@ class EditHabitActivity : AppCompatActivity() {
)
}
component.commandRunner.run(command)
if (habitType == HabitType.TIME_COST) {
val count = 0
val savedHabit = component.habitList.getByUUID(habit.uuid) ?: habit
if (savedHabit.id != null) {
component.commandRunner.run(
CreateRepetitionCommand(
component.habitList,
savedHabit,
getToday(),
count,
""
)
)
}
}
finish()
}
@ -366,13 +399,14 @@ class EditHabitActivity : AppCompatActivity() {
val adapter = ArrayAdapter(
this,
android.R.layout.simple_list_item_1,
TIME_COST_PRESETS.map { it.label }
TIME_COST_PRESETS.map { getPresetLabel(it) }
)
binding.unitInput.setAdapter(adapter)
binding.unitInput.setOnItemClickListener { _, _, position, _ ->
val preset = TIME_COST_PRESETS[position]
minutesPerUnit = preset.minutesPerUnit
binding.unitInput.setText(preset.label, false)
binding.unitInput.setText(getPresetLabel(preset), false)
unit = preset.unit
}
binding.unitInput.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) syncPresetFromText()
@ -395,7 +429,9 @@ class EditHabitActivity : AppCompatActivity() {
private fun selectedPreset(): UnitPreset? {
val value = binding.unitInput.text?.toString()?.trim().orEmpty()
return TIME_COST_PRESETS.firstOrNull { it.label == value }
return TIME_COST_PRESETS.firstOrNull {
getPresetLabel(it) == value || it.unit == value
}
}
private fun findPresetByMinutes(minutes: Int): UnitPreset? {
@ -403,16 +439,23 @@ class EditHabitActivity : AppCompatActivity() {
}
private fun findPresetByUnit(unit: String): UnitPreset? {
return TIME_COST_PRESETS.firstOrNull { it.unit == unit || it.label == unit }
return TIME_COST_PRESETS.firstOrNull {
it.unit == unit || getPresetLabel(it) == unit
}
}
private fun syncPresetFromText() {
selectedPreset()?.let { preset ->
minutesPerUnit = preset.minutesPerUnit
binding.unitInput.setText(preset.label, false)
unit = preset.unit
binding.unitInput.setText(getPresetLabel(preset), false)
}
}
private fun getPresetLabel(preset: UnitPreset): String {
return getString(preset.labelRes)
}
private fun populateReminder() {
if (reminderHour < 0) {
binding.reminderTimePicker.text = getString(R.string.reminder_off)

View File

@ -110,6 +110,11 @@ class ListHabitsMenu(
return true
}
R.id.actionRemoveAll -> {
behavior.onRemoveAll()
return true
}
R.id.actionHideArchived -> {
behavior.onToggleShowArchived()
activity.invalidateOptionsMenu()

View File

@ -20,9 +20,12 @@
package org.isoron.uhabits.activities.habits.list
import android.content.Context
import android.view.Gravity
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.graphics.Typeface
import android.widget.FrameLayout
import android.widget.RelativeLayout
import android.widget.TextView
import me.tatarka.inject.annotations.Inject
import nl.dionsegijn.konfetti.xml.KonfettiView
import org.isoron.uhabits.R
@ -75,6 +78,14 @@ class ListHabitsRootView(
translationZ = 10f
}
val progressBar = TaskProgressBar(context, runner)
val timeLossSummary = TextView(context).apply {
setTextColor(sres.getColor(R.attr.contrast60))
textSize = 16f
setTypeface(typeface, Typeface.BOLD)
gravity = Gravity.CENTER_VERTICAL
setPadding(dp(16f).toInt(), dp(8f).toInt(), dp(16f).toInt(), dp(8f).toInt())
text = context.getString(R.string.time_lost_today_summary, 0, 0, 0)
}
val hintView: HintView
val header = HeaderView(context, preferences, midnightTimer)
@ -88,9 +99,10 @@ class ListHabitsRootView(
addAtTop(konfettiView)
addAtTop(tbar)
addBelow(header, tbar)
addBelow(listView, header, height = MATCH_PARENT)
addBelow(llEmpty, header, height = MATCH_PARENT)
addBelow(progressBar, header) {
addBelow(timeLossSummary, header)
addBelow(listView, timeLossSummary, height = MATCH_PARENT)
addBelow(llEmpty, timeLossSummary, height = MATCH_PARENT)
addBelow(progressBar, timeLossSummary) {
it.topMargin = dp(-6.0f).toInt()
}
addAtBottom(hintView)
@ -124,6 +136,7 @@ class ListHabitsRootView(
super.onAttachedToWindow()
setupControllers()
listAdapter.observable.addListener(this)
updateEmptyView()
}
override fun onDetachedFromWindow() {

View File

@ -24,6 +24,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import me.tatarka.inject.annotations.Inject
import nl.dionsegijn.konfetti.core.Party
@ -172,6 +173,17 @@ class ListHabitsScreen(
dialog.show(activity.supportFragmentManager, "habitType")
}
override fun showRemoveAllDialog() {
val builder = AlertDialog.Builder(activity)
builder.setTitle(R.string.remove_all)
builder.setMessage(R.string.remove_all_message)
builder.setPositiveButton(R.string.yes) { _, _ ->
behavior.value.onRemoveAllConfirmed()
}
builder.setNegativeButton(R.string.no, null)
builder.show()
}
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback, quantity: Int) {
ConfirmDeleteDialog(activity, callback, quantity).dismissCurrentAndShow()
}
@ -272,13 +284,20 @@ class ListHabitsScreen(
override fun showNumberPopup(
value: Double,
notes: String,
habitName: String?,
unit: String?,
color: PaletteColor?,
callback: NumberPickerCallback
) {
val theme = themeSwitcher.currentTheme!!
val fm = (context as AppCompatActivity).supportFragmentManager
val dialog = NumberDialog()
dialog.arguments = Bundle().apply {
putDouble("value", value)
putString("notes", notes)
putString("habitName", habitName)
putString("unit", unit)
putInt("color", theme.color(color ?: PaletteColor(11)).toInt())
}
dialog.onToggle = { v, n -> callback.onNumberPicked(v, n) }
dialog.dismissCurrentAndShow(fm, "numberDialog")

View File

@ -154,6 +154,8 @@ class HabitCardView(
gravity = Gravity.CENTER
}
setThickness(thickness)
isClickable = false
isFocusable = false
}
label = TextView(context).apply {
@ -162,6 +164,8 @@ class HabitCardView(
if (SDK_INT >= Build.VERSION_CODES.Q) {
breakStrategy = BREAK_STRATEGY_BALANCED
}
isClickable = false
isFocusable = false
}
lossText = TextView(context).apply {
@ -176,9 +180,12 @@ class HabitCardView(
layoutParams = LinearLayout.LayoutParams(0, WRAP_CONTENT, 1f)
addView(label)
addView(lossText)
isClickable = false
isFocusable = false
}
checkmarkPanel = checkmarkPanelFactory.create().apply {
isClickable = true
onToggle = { date, value, notes ->
triggerRipple(date)
val location = getAbsoluteButtonLocation(date)
@ -248,6 +255,12 @@ class HabitCardView(
v.background.setHotspot(event.x, event.y)
false
}
setOnClickListener {
val date = getToday()
val location = PointF(it.width / 2f, it.height / 2f)
habit?.let { behavior.onEdit(it, date, location.x, location.y) }
}
}
clipToPadding = false
@ -339,7 +352,7 @@ class HabitCardView(
visibility = if (h.isTimeCost) VISIBLE else GONE
text = if (h.isTimeCost) {
val lostMin = h.getTodayValue() * h.minutesPerUnit
context.getString(R.string.time_lost_today, lostMin)
context.getString(R.string.time_lost_today_card, lostMin)
} else {
""
}

View File

@ -40,6 +40,7 @@ import org.isoron.uhabits.activities.common.dialogs.HistoryEditorDialog
import org.isoron.uhabits.activities.common.dialogs.NumberDialog
import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.DeleteHabitsCommand
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
@ -111,6 +112,12 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
)
view.setListener(presenter)
view.setDeleteListener {
screen.showDeleteConfirmationScreen {
commandRunner.run(DeleteHabitsCommand(appComponent.habitList, listOf(habit)))
screen.close()
}
}
view.applyRootViewInsets()
setContentView(view)
}

View File

@ -31,6 +31,7 @@ import org.isoron.uhabits.utils.setupToolbar
class ShowHabitView(context: Context) : FrameLayout(context) {
private val binding = ShowHabitBinding.inflate(LayoutInflater.from(context))
private var deleteListener: (() -> Unit)? = null
init {
binding.toolbar.applyToolbarInsets()
@ -58,6 +59,7 @@ class ShowHabitView(context: Context) : FrameLayout(context) {
} else {
binding.targetCard.visibility = GONE
}
binding.deleteButton.visibility = VISIBLE
binding.linearLayout.applyBottomInset()
}
@ -65,5 +67,12 @@ class ShowHabitView(context: Context) : FrameLayout(context) {
binding.scoreCard.setListener(presenter.scoreCardPresenter)
binding.historyCard.setListener(presenter.historyCardPresenter)
binding.barCard.setListener(presenter.barCardPresenter)
binding.deleteButton.setOnClickListener {
deleteListener?.invoke()
}
}
fun setDeleteListener(listener: () -> Unit) {
deleteListener = listener
}
}

View File

@ -61,7 +61,13 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
} else {
resources.getString(R.string.reminder_off)
}
binding.targetText.text = "${state.targetValue.toShortString()} ${state.unit}"
binding.targetText.text = if (state.isNumerical) {
"${state.targetValue.toShortString()} ${state.unit}"
} else if (state.isTimeCost) {
getTimeCostUnitLabel(state.unit)
} else {
state.unit
}
binding.questionLabel.visibility = View.VISIBLE
binding.targetIcon.visibility = View.VISIBLE
@ -73,7 +79,6 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
}
} else {
binding.targetIcon.visibility = View.GONE
binding.targetText.visibility = View.GONE
}
if (state.question.isEmpty()) {
binding.questionLabel.visibility = View.GONE
@ -81,4 +86,24 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
postInvalidate()
}
private fun getTimeCostUnitLabel(unit: String): String {
return when (unit) {
"cup_200" -> resources.getString(R.string.time_cost_preset_cup_200)
"cup_100" -> resources.getString(R.string.time_cost_preset_small_cup_100)
"cup_300" -> resources.getString(R.string.time_cost_preset_large_cup_300)
"sip_20" -> resources.getString(R.string.time_cost_preset_sip_20)
"shot_50" -> resources.getString(R.string.time_cost_preset_shot_50)
"glass_150" -> resources.getString(R.string.time_cost_preset_glass_150)
"can_500" -> resources.getString(R.string.time_cost_preset_can_500)
"bottle_1_5l" -> resources.getString(R.string.time_cost_preset_bottle_1_5l)
"sip_30" -> resources.getString(R.string.time_cost_preset_sip_30)
"can_250" -> resources.getString(R.string.time_cost_preset_small_can_250)
"can_330" -> resources.getString(R.string.time_cost_preset_medium_can_330)
"can_500_large" -> resources.getString(R.string.time_cost_preset_large_can_500)
"cigarette_1" -> resources.getString(R.string.time_cost_preset_cigarette_1)
"vape_puff_1" -> resources.getString(R.string.time_cost_preset_vape_puff_1)
else -> unit
}
}
}

View File

@ -126,9 +126,12 @@
</LinearLayout>
<!-- Habit Question -->
<FrameLayout style="@style/FormOuterBox">
<FrameLayout
android:id="@+id/questionOuterBox"
style="@style/FormOuterBox">
<LinearLayout style="@style/FormInnerBox">
<TextView
android:id="@+id/questionLabel"
style="@style/FormLabel"
android:text="@string/question" />
<EditText
@ -187,6 +190,7 @@
android:layout_weight="1">
<LinearLayout style="@style/FormInnerBox">
<TextView
android:id="@+id/targetLabel"
style="@style/FormLabel"
android:text="@string/target" />
<EditText

View File

@ -30,6 +30,32 @@
app:divider="@drawable/checkmark_dialog_divider"
app:showDividers="middle">
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp"
android:gravity="center">
<TextView
android:id="@+id/habitName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?attr/contrast100"
android:visibility="gone" />
<TextView
android:id="@+id/unitLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="?attr/contrast60"
android:visibility="gone" />
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/notes"
android:layout_width="match_parent"
@ -82,6 +108,12 @@
app:divider="@drawable/checkmark_dialog_divider"
app:showDividers="middle">
<TextView
android:id="@+id/deleteBtn"
style="@style/NumericalPopupBtn"
android:text="@string/fa_trash"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/value"
android:layout_width="0dp"
@ -108,4 +140,4 @@
style="@style/CheckmarkPopupBtn"
android:text="@string/fa_question" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>

View File

@ -47,6 +47,16 @@
android:id="@+id/subtitleCard"
style="@style/ShowHabit.Subtitle" />
<com.google.android.material.button.MaterialButton
android:id="@+id/deleteButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="12dp"
android:layout_marginRight="16dp"
android:text="@string/delete" />
<org.isoron.uhabits.activities.habits.show.views.NotesCardView
android:id="@+id/notesCard"
style="@style/Card"

View File

@ -88,6 +88,12 @@
android:title="@string/action_settings"
app:showAsAction="never"/>
<item
android:id="@+id/actionRemoveAll"
android:orderInCategory="100"
android:title="@string/remove_all"
app:showAsAction="never"/>
<item
android:id="@+id/actionFAQ"
android:orderInCategory="100"

View File

@ -189,6 +189,7 @@
<string name="target_type_at_most">Не больше</string>
<string name="example_question_boolean">напр.: Вы упражнялись сегодня?</string>
<string name="question">Вопрос</string>
<string name="quantity">Кол-во</string>
<string name="target">Цель</string>
<string name="yes">Да</string>
<string name="no">Нет</string>
@ -206,6 +207,22 @@
<string name="yes_or_no_example">Например, вы рано проснулись сегодня? Вы занимались спортом? Вы играли в шахматы?</string>
<string name="measurable">Измеримый</string>
<string name="measurable_example">напр.: Сколько км вы пробежали сегодня? Сколько страниц прочитали?</string>
<string name="time_cost">Вредные привычки</string>
<string name="time_cost_example">Отслеживайте вредные привычки</string>
<string name="time_cost_preset_cup_200">Чашка (200 мл)</string>
<string name="time_cost_preset_small_cup_100">Маленькая чашка (100 мл)</string>
<string name="time_cost_preset_large_cup_300">Большая чашка (300 мл)</string>
<string name="time_cost_preset_sip_20">Глоток (20 мл)</string>
<string name="time_cost_preset_shot_50">Рюмка (50 мл)</string>
<string name="time_cost_preset_glass_150">Бокал (150 мл)</string>
<string name="time_cost_preset_can_500">Банка (500 мл)</string>
<string name="time_cost_preset_bottle_1_5l">Бутылка (1,5 л)</string>
<string name="time_cost_preset_sip_30">Глоток (30 мл)</string>
<string name="time_cost_preset_small_can_250">Маленькая банка (250 мл)</string>
<string name="time_cost_preset_medium_can_330">Средняя банка (330 мл)</string>
<string name="time_cost_preset_large_can_500">Большая банка (500 мл)</string>
<string name="time_cost_preset_cigarette_1">Сигарета (1 шт)</string>
<string name="time_cost_preset_vape_puff_1">Затяжка вейп (1 шт)</string>
<string name="x_times_per_week">%d раз в неделю</string>
<string name="x_times_per_month">%d раз в месяц</string>
<string name="x_times_per_y_days">%d раз в %d дней</string>
@ -215,6 +232,10 @@
<string name="measurable_short_example">напр.: Побегать</string>
<string name="measurable_question_example">напр.: Сколько км вы пробежали сегодня?</string>
<string name="measurable_units_example">напр.: км</string>
<string name="time_cost_short_example">напр.: Курить</string>
<string name="time_cost_question_example">напр.: Сколько сигарет сегодня?</string>
<string name="time_lost_today_summary">Сегодня потеряно\n%1$dд%2$dч%3$dм</string>
<string name="time_lost_today_card">Потеряно времени сегодня: %d мин</string>
<string name="every_month">Каждый месяц</string>
<string name="validation_cannot_be_blank">Не может быть пустым</string>
<string name="today">Сегодня</string>

View File

@ -29,6 +29,7 @@
<string translatable="false" name="fa_calendar">&#xf073;</string>
<string translatable="false" name="fa_exclamation_circle">&#xf06a;</string>
<string translatable="false" name="fa_question">&#xf128;</string>
<string translatable="false" name="fa_trash">&#xf1f8;</string>
<string translatable="false" name="fa_umbrella_beach">&#xf5ca;</string>
<!--<string translatable="false" name="fa_glass">&#xf000;</string>-->

View File

@ -181,6 +181,8 @@
<string name="by_color">By color</string>
<string name="by_score">By score</string>
<string name="by_status">By status</string>
<string name="remove_all">Remove all</string>
<string name="remove_all_message">All habits and their history will be permanently deleted. This action cannot be undone.</string>
<string name="export">Export</string>
<string name="long_press_to_edit">Press-and-hold to change the value</string>
<string name="value">Value</string>
@ -191,6 +193,7 @@
<string name="target_type_at_most">At most</string>
<string name="example_question_boolean">e.g. Did you exercise today?</string>
<string name="question">Question</string>
<string name="quantity">Count</string>
<string name="target">Target</string>
<string name="yes">Yes</string>
<string name="no">No</string>
@ -208,8 +211,22 @@
<string name="yes_or_no_example">e.g. Did you wake up early today? Did you exercise? Did you play chess?</string>
<string name="measurable">Measurable</string>
<string name="measurable_example">e.g. How many miles did you run today? How many pages did you read?</string>
<string name="time_cost">Time Cost</string>
<string name="time_cost_example">Track lost time in minutes</string>
<string name="time_cost">Harmful habits</string>
<string name="time_cost_example">Track harmful habits</string>
<string name="time_cost_preset_cup_200">Cup (200 ml)</string>
<string name="time_cost_preset_small_cup_100">Small cup (100 ml)</string>
<string name="time_cost_preset_large_cup_300">Large cup (300 ml)</string>
<string name="time_cost_preset_sip_20">Sip (20 ml)</string>
<string name="time_cost_preset_shot_50">Shot (50 ml)</string>
<string name="time_cost_preset_glass_150">Glass (150 ml)</string>
<string name="time_cost_preset_can_500">Can (500 ml)</string>
<string name="time_cost_preset_bottle_1_5l">Bottle (1.5 l)</string>
<string name="time_cost_preset_sip_30">Sip (30 ml)</string>
<string name="time_cost_preset_small_can_250">Small can (250 ml)</string>
<string name="time_cost_preset_medium_can_330">Medium can (330 ml)</string>
<string name="time_cost_preset_large_can_500">Large can (500 ml)</string>
<string name="time_cost_preset_cigarette_1">Cigarette (1 pc)</string>
<string name="time_cost_preset_vape_puff_1">Vape puff (1 pc)</string>
<string name="x_times_per_week">%d times per week</string>
<string name="x_times_per_month">%d times per month</string>
<string name="x_times_per_y_days">%d times in %d days</string>
@ -221,7 +238,8 @@
<string name="measurable_units_example">e.g. miles</string>
<string name="time_cost_short_example">e.g. Smoke</string>
<string name="time_cost_question_example">e.g. How many cigarettes today?</string>
<string name="time_lost_today">Time lost today: %d min</string>
<string name="time_lost_today_summary">Time lost today\n%1$d%2$dh%3$dm</string>
<string name="time_lost_today_card">Time lost today: %d min</string>
<string name="every_month">Every month</string>
<string name="validation_cannot_be_blank">Cannot be blank</string>
<string name="today">Today</string>

View File

@ -57,7 +57,13 @@ open class ListHabitsBehavior(
val entry = habit.computedEntries.get(date)
if (habit.type == HabitType.TIME_COST) {
val oldValue = entry.value.coerceAtLeast(0).toDouble()
screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String ->
screen.showNumberPopup(
oldValue,
entry.notes,
habit.name,
null,
habit.color
) { newValue: Double, newNotes: String ->
val value = newValue.roundToInt().coerceAtLeast(0)
if (newValue != oldValue && oldValue <= 0.0 && value > 0) {
screen.showConfetti(habit.color, x, y)
@ -66,7 +72,13 @@ open class ListHabitsBehavior(
}
} else if (habit.type == HabitType.NUMERICAL) {
val oldValue = entry.value.toDouble() / 1000
screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String ->
screen.showNumberPopup(
oldValue,
entry.notes,
habit.name,
habit.unit,
habit.color
) { newValue: Double, newNotes: String ->
val value = (newValue * 1000).roundToInt()
if (newValue != oldValue) {
if (
@ -114,6 +126,16 @@ open class ListHabitsBehavior(
taskRunner.execute { habitList.reorder(from, to) }
}
open fun onRemoveAll() {
screen.showRemoveAllDialog()
}
open fun onRemoveAllConfirmed() {
taskRunner.execute {
habitList.removeAll()
}
}
open fun onRepairDB() {
taskRunner.execute(object : Task {
override suspend fun doInBackground() {
@ -178,6 +200,9 @@ open class ListHabitsBehavior(
fun showNumberPopup(
value: Double,
notes: String,
habitName: String? = null,
unit: String? = null,
color: PaletteColor? = null,
callback: NumberPickerCallback
)
fun showCheckmarkPopup(
@ -189,5 +214,6 @@ open class ListHabitsBehavior(
fun showSendBugReportToDeveloperScreen(log: String)
fun showSendFileScreen(filename: String)
fun showConfetti(color: PaletteColor, x: Float, y: Float)
fun showRemoveAllDialog()
}
}

View File

@ -50,6 +50,10 @@ class ListHabitsMenuBehavior(
screen.showSettingsScreen()
}
fun onRemoveAll() {
screen.showRemoveAllDialog()
}
fun onToggleShowArchived() {
showArchived = !showArchived
preferences.showArchived = showArchived
@ -134,6 +138,7 @@ class ListHabitsMenuBehavior(
fun showFAQScreen()
fun showSettingsScreen()
fun showSelectHabitTypeDialog()
fun showRemoveAllDialog()
}
init {

View File

@ -47,6 +47,7 @@ import org.isoron.uhabits.core.ui.views.Theme
data class ShowHabitState(
val title: String = "",
val isNumerical: Boolean = false,
val isTimeCost: Boolean = false,
val color: PaletteColor = PaletteColor(1),
val subtitle: SubtitleCardState,
val overview: OverviewCardState,
@ -95,6 +96,7 @@ class ShowHabitPresenter(
title = habit.name,
color = habit.color,
isNumerical = habit.isNumerical,
isTimeCost = habit.isTimeCost,
theme = theme,
subtitle = SubtitleCardPresenter.buildState(
habit = habit,

View File

@ -30,6 +30,7 @@ data class SubtitleCardState(
val color: PaletteColor,
val frequency: Frequency,
val isNumerical: Boolean,
val isTimeCost: Boolean,
val question: String,
val reminder: Reminder?,
val targetValue: Double = 0.0,
@ -44,12 +45,13 @@ class SubtitleCardPresenter {
habit: Habit,
theme: Theme
): SubtitleCardState = SubtitleCardState(
color = habit.color,
frequency = habit.frequency,
isNumerical = habit.isNumerical,
question = habit.question,
reminder = habit.reminder,
targetValue = habit.targetValue,
color = habit.color,
frequency = habit.frequency,
isNumerical = habit.isNumerical,
isTimeCost = habit.isTimeCost,
question = habit.question,
reminder = habit.reminder,
targetValue = habit.targetValue,
targetType = habit.targetType,
unit = habit.unit,
theme = theme