mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Allow user to skip days without breaking streak
Co-authored-by: Alinson S. Xavier <git@axavier.org>
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
@@ -93,8 +93,8 @@ class CheckmarkPanelViewTest : BaseViewTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testToggle() {
|
fun testToggle() {
|
||||||
var timestamps = mutableListOf<Timestamp>()
|
val timestamps = mutableListOf<Timestamp>()
|
||||||
view.onToggle = { timestamps.add(it) }
|
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()
|
||||||
@@ -105,7 +105,7 @@ class CheckmarkPanelViewTest : BaseViewTest() {
|
|||||||
fun testToggle_withOffset() {
|
fun testToggle_withOffset() {
|
||||||
val timestamps = mutableListOf<Timestamp>()
|
val timestamps = mutableListOf<Timestamp>()
|
||||||
view.dataOffset = 3
|
view.dataOffset = 3
|
||||||
view.onToggle = { timestamps += it }
|
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()
|
||||||
|
|||||||
@@ -70,7 +70,10 @@ public class CheckmarkWidgetTest extends BaseViewTest
|
|||||||
// possible to capture intents sent to BroadcastReceivers.
|
// possible to capture intents sent to BroadcastReceivers.
|
||||||
button.performClick();
|
button.performClick();
|
||||||
sleep(1000);
|
sleep(1000);
|
||||||
|
assertThat(checkmarks.getTodayValue(), equalTo(SKIPPED));
|
||||||
|
|
||||||
|
button.performClick();
|
||||||
|
sleep(1000);
|
||||||
assertThat(checkmarks.getTodayValue(), equalTo(UNCHECKED));
|
assertThat(checkmarks.getTodayValue(), equalTo(UNCHECKED));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -147,11 +147,10 @@ public class HistoryChart extends ScrollableChart
|
|||||||
int offset = timestamp.daysUntil(today);
|
int offset = timestamp.daysUntil(today);
|
||||||
if (offset < checkmarks.length)
|
if (offset < checkmarks.length)
|
||||||
{
|
{
|
||||||
boolean isChecked = checkmarks[offset] == CHECKED_EXPLICITLY;
|
checkmarks[offset] = Repetition.nextToggleValue(checkmarks[offset]);
|
||||||
checkmarks[offset] = (isChecked ? UNCHECKED : CHECKED_EXPLICITLY);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.onToggleCheckmark(timestamp);
|
controller.onToggleCheckmark(timestamp, checkmarks[offset]);
|
||||||
postInvalidate();
|
postInvalidate();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -362,21 +361,42 @@ public class HistoryChart extends ScrollableChart
|
|||||||
GregorianCalendar date,
|
GregorianCalendar date,
|
||||||
int checkmarkOffset)
|
int checkmarkOffset)
|
||||||
{
|
{
|
||||||
|
int checkmark = 0;
|
||||||
if (checkmarkOffset >= checkmarks.length) pSquareBg.setColor(colors[0]);
|
if (checkmarkOffset >= checkmarks.length) pSquareBg.setColor(colors[0]);
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
int checkmark = checkmarks[checkmarkOffset];
|
checkmark = checkmarks[checkmarkOffset];
|
||||||
if(checkmark == 0) pSquareBg.setColor(colors[0]);
|
if(checkmark == 0)
|
||||||
|
pSquareBg.setColor(colors[0]);
|
||||||
else if(checkmark < target)
|
else if(checkmark < target)
|
||||||
{
|
pSquareBg.setColor(colors[1]);
|
||||||
pSquareBg.setColor(isNumerical ? textColor : colors[1]);
|
else
|
||||||
}
|
pSquareBg.setColor(colors[2]);
|
||||||
else pSquareBg.setColor(colors[2]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pSquareFg.setColor(reverseTextColor);
|
pSquareFg.setColor(reverseTextColor);
|
||||||
|
pSquareFg.setStrokeWidth(columnWidth * 0.025f);
|
||||||
|
|
||||||
float round = dpToPixels(getContext(), 2);
|
float round = dpToPixels(getContext(), 2);
|
||||||
canvas.drawRoundRect(location, round, round, pSquareBg);
|
canvas.drawRoundRect(location, round, round, pSquareBg);
|
||||||
|
|
||||||
|
if (!isNumerical && checkmark == SKIPPED)
|
||||||
|
{
|
||||||
|
canvas.save();
|
||||||
|
canvas.clipRect(location);
|
||||||
|
float offset = - columnWidth;
|
||||||
|
for (int k = 0; k < 10; k++)
|
||||||
|
{
|
||||||
|
offset += columnWidth / 5;
|
||||||
|
canvas.drawLine(location.left + offset,
|
||||||
|
location.bottom,
|
||||||
|
location.right + offset,
|
||||||
|
location.top,
|
||||||
|
pSquareFg);
|
||||||
|
}
|
||||||
|
canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
String text = Integer.toString(date.get(Calendar.DAY_OF_MONTH));
|
String text = Integer.toString(date.get(Calendar.DAY_OF_MONTH));
|
||||||
canvas.drawText(text, location.centerX(),
|
canvas.drawText(text, location.centerX(),
|
||||||
location.centerY() + squareTextOffset, pSquareFg);
|
location.centerY() + squareTextOffset, pSquareFg);
|
||||||
@@ -492,6 +512,6 @@ public class HistoryChart extends ScrollableChart
|
|||||||
|
|
||||||
public interface Controller
|
public interface Controller
|
||||||
{
|
{
|
||||||
default void onToggleCheckmark(Timestamp timestamp) {}
|
default void onToggleCheckmark(Timestamp timestamp, int value) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import android.view.View.MeasureSpec.*
|
|||||||
import com.google.auto.factory.*
|
import com.google.auto.factory.*
|
||||||
import org.isoron.androidbase.activities.*
|
import org.isoron.androidbase.activities.*
|
||||||
import org.isoron.uhabits.*
|
import org.isoron.uhabits.*
|
||||||
|
import org.isoron.uhabits.core.models.*
|
||||||
import org.isoron.uhabits.core.models.Checkmark.*
|
import org.isoron.uhabits.core.models.Checkmark.*
|
||||||
import org.isoron.uhabits.core.preferences.*
|
import org.isoron.uhabits.core.preferences.*
|
||||||
import org.isoron.uhabits.utils.*
|
import org.isoron.uhabits.utils.*
|
||||||
@@ -51,7 +52,7 @@ class CheckmarkButtonView(
|
|||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
var onToggle: () -> Unit = {}
|
var onToggle: (Int) -> Unit = {}
|
||||||
private var drawer = Drawer()
|
private var drawer = Drawer()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -61,11 +62,8 @@ class CheckmarkButtonView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun performToggle() {
|
fun performToggle() {
|
||||||
onToggle()
|
value = Repetition.nextToggleValue(value)
|
||||||
value = when (value) {
|
onToggle(value)
|
||||||
CHECKED_EXPLICITLY -> UNCHECKED
|
|
||||||
else -> CHECKED_EXPLICITLY
|
|
||||||
}
|
|
||||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
@@ -106,9 +104,11 @@ class CheckmarkButtonView(
|
|||||||
fun draw(canvas: Canvas) {
|
fun draw(canvas: Canvas) {
|
||||||
paint.color = when (value) {
|
paint.color = when (value) {
|
||||||
CHECKED_EXPLICITLY -> color
|
CHECKED_EXPLICITLY -> color
|
||||||
|
SKIPPED -> color
|
||||||
else -> lowContrastColor
|
else -> lowContrastColor
|
||||||
}
|
}
|
||||||
val id = when (value) {
|
val id = when (value) {
|
||||||
|
SKIPPED -> R.string.fa_skipped
|
||||||
UNCHECKED -> R.string.fa_times
|
UNCHECKED -> R.string.fa_times
|
||||||
else -> R.string.fa_check
|
else -> R.string.fa_check
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class CheckmarkPanelView(
|
|||||||
setupButtons()
|
setupButtons()
|
||||||
}
|
}
|
||||||
|
|
||||||
var onToggle: (Timestamp) -> Unit = {}
|
var onToggle: (Timestamp, Int) -> Unit = {_, _ ->}
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
setupButtons()
|
setupButtons()
|
||||||
@@ -65,7 +65,7 @@ class CheckmarkPanelView(
|
|||||||
else -> UNCHECKED
|
else -> UNCHECKED
|
||||||
}
|
}
|
||||||
button.color = color
|
button.color = color
|
||||||
button.onToggle = { onToggle(timestamp) }
|
button.onToggle = { value -> onToggle(timestamp, value) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,9 +121,9 @@ class HabitCardView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
checkmarkPanel = checkmarkPanelFactory.create().apply {
|
||||||
onToggle = { timestamp ->
|
onToggle = { timestamp, value ->
|
||||||
triggerRipple(timestamp)
|
triggerRipple(timestamp)
|
||||||
habit?.let { behavior.onToggle(it, timestamp) }
|
habit?.let { behavior.onToggle(it, timestamp, value) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,9 +81,9 @@ public class ShowHabitScreen extends BaseScreen
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onToggleCheckmark(Timestamp timestamp)
|
public void onToggleCheckmark(Timestamp timestamp, int value)
|
||||||
{
|
{
|
||||||
behavior.get().onToggleCheckmark(timestamp);
|
behavior.get().onToggleCheckmark(timestamp, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class NumericalCheckmarkWidgetActivity : Activity(), ListHabitsBehavior.NumberPi
|
|||||||
val component = app.component
|
val component = app.component
|
||||||
val parser = app.component.intentParser
|
val parser = app.component.intentParser
|
||||||
data = parser.parseCheckmarkIntent(intent)
|
data = parser.parseCheckmarkIntent(intent)
|
||||||
behavior = WidgetBehavior(component.habitList, component.commandRunner, component.notificationTray)
|
behavior = WidgetBehavior(component.commandRunner, component.notificationTray)
|
||||||
widgetUpdater = component.widgetUpdater
|
widgetUpdater = component.widgetUpdater
|
||||||
showNumberSelector(this)
|
showNumberSelector(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ public class CheckmarkWidgetView extends HabitWidgetView {
|
|||||||
|
|
||||||
switch (checkmarkState) {
|
switch (checkmarkState) {
|
||||||
case Checkmark.CHECKED_EXPLICITLY:
|
case Checkmark.CHECKED_EXPLICITLY:
|
||||||
|
case Checkmark.SKIPPED:
|
||||||
bgColor = activeColor;
|
bgColor = activeColor;
|
||||||
fgColor = res.getColor(R.attr.highContrastReverseTextColor);
|
fgColor = res.getColor(R.attr.highContrastReverseTextColor);
|
||||||
setShadowAlpha(0x4f);
|
setShadowAlpha(0x4f);
|
||||||
@@ -117,7 +118,8 @@ public class CheckmarkWidgetView extends HabitWidgetView {
|
|||||||
case Checkmark.CHECKED_EXPLICITLY:
|
case Checkmark.CHECKED_EXPLICITLY:
|
||||||
case Checkmark.CHECKED_IMPLICITLY:
|
case Checkmark.CHECKED_IMPLICITLY:
|
||||||
return getResources().getString(R.string.fa_check);
|
return getResources().getString(R.string.fa_check);
|
||||||
|
case Checkmark.SKIPPED:
|
||||||
|
return getResources().getString(R.string.fa_skipped);
|
||||||
case Checkmark.UNCHECKED:
|
case Checkmark.UNCHECKED:
|
||||||
default:
|
default:
|
||||||
return getResources().getString(R.string.fa_times);
|
return getResources().getString(R.string.fa_times);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
<string translatable="false" name="fa_arrow_circle_up"></string>
|
<string translatable="false" name="fa_arrow_circle_up"></string>
|
||||||
<string translatable="false" name="fa_check"></string>
|
<string translatable="false" name="fa_check"></string>
|
||||||
<string translatable="false" name="fa_times"></string>
|
<string translatable="false" name="fa_times"></string>
|
||||||
|
<string translatable="false" name="fa_skipped"></string>
|
||||||
<string translatable="false" name="fa_bell_o"></string>
|
<string translatable="false" name="fa_bell_o"></string>
|
||||||
<string translatable="false" name="fa_calendar"></string>
|
<string translatable="false" name="fa_calendar"></string>
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ public class WidgetControllerTest extends BaseAndroidJVMTest
|
|||||||
commandRunner = mock(CommandRunner.class);
|
commandRunner = mock(CommandRunner.class);
|
||||||
notificationTray = mock(NotificationTray.class);
|
notificationTray = mock(NotificationTray.class);
|
||||||
controller =
|
controller =
|
||||||
new WidgetBehavior(habitList, commandRunner, notificationTray);
|
new WidgetBehavior(commandRunner, notificationTray);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -72,10 +72,6 @@ public class CommandParser
|
|||||||
.fromJson(json, EditHabitCommand.Record.class)
|
.fromJson(json, EditHabitCommand.Record.class)
|
||||||
.toCommand(modelFactory, habitList);
|
.toCommand(modelFactory, habitList);
|
||||||
|
|
||||||
if (event.equals("Toggle")) return gson
|
|
||||||
.fromJson(json, ToggleRepetitionCommand.Record.class)
|
|
||||||
.toCommand(habitList);
|
|
||||||
|
|
||||||
if (event.equals("Unarchive")) return gson
|
if (event.equals("Unarchive")) return gson
|
||||||
.fromJson(json, UnarchiveHabitsCommand.Record.class)
|
.fromJson(json, UnarchiveHabitsCommand.Record.class)
|
||||||
.toCommand(habitList);
|
.toCommand(habitList);
|
||||||
|
|||||||
@@ -58,8 +58,11 @@ public class CreateRepetitionCommand extends Command
|
|||||||
previousRep = reps.getByTimestamp(timestamp);
|
previousRep = reps.getByTimestamp(timestamp);
|
||||||
if (previousRep != null) reps.remove(previousRep);
|
if (previousRep != null) reps.remove(previousRep);
|
||||||
|
|
||||||
newRep = new Repetition(timestamp, value);
|
if (value > 0)
|
||||||
reps.add(newRep);
|
{
|
||||||
|
newRep = new Repetition(timestamp, value);
|
||||||
|
reps.add(newRep);
|
||||||
|
}
|
||||||
|
|
||||||
habit.invalidateNewerThan(timestamp);
|
habit.invalidateNewerThan(timestamp);
|
||||||
}
|
}
|
||||||
@@ -80,9 +83,7 @@ public class CreateRepetitionCommand extends Command
|
|||||||
@Override
|
@Override
|
||||||
public void undo()
|
public void undo()
|
||||||
{
|
{
|
||||||
if(newRep == null) throw new IllegalStateException();
|
if(newRep != null) habit.getRepetitions().remove(newRep);
|
||||||
habit.getRepetitions().remove(newRep);
|
|
||||||
|
|
||||||
if (previousRep != null) habit.getRepetitions().add(previousRep);
|
if (previousRep != null) habit.getRepetitions().add(previousRep);
|
||||||
habit.invalidateNewerThan(timestamp);
|
habit.invalidateNewerThan(timestamp);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
|
||||||
*
|
|
||||||
* 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.core.commands;
|
|
||||||
|
|
||||||
import androidx.annotation.*;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Command to toggle a repetition.
|
|
||||||
*/
|
|
||||||
public class ToggleRepetitionCommand extends Command
|
|
||||||
{
|
|
||||||
@NonNull
|
|
||||||
private HabitList list;
|
|
||||||
|
|
||||||
final Timestamp timestamp;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
final Habit habit;
|
|
||||||
|
|
||||||
public ToggleRepetitionCommand(@NonNull HabitList list,
|
|
||||||
@NonNull Habit habit,
|
|
||||||
Timestamp timestamp)
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
this.list = list;
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
this.habit = habit;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void execute()
|
|
||||||
{
|
|
||||||
habit.getRepetitions().toggle(timestamp);
|
|
||||||
list.update(habit);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public Habit getHabit()
|
|
||||||
{
|
|
||||||
return habit;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public Record toRecord()
|
|
||||||
{
|
|
||||||
return new Record(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void undo()
|
|
||||||
{
|
|
||||||
execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Record
|
|
||||||
{
|
|
||||||
@NonNull
|
|
||||||
public String id;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public String event = "Toggle";
|
|
||||||
|
|
||||||
public long habit;
|
|
||||||
|
|
||||||
public long repTimestamp;
|
|
||||||
|
|
||||||
public Record(@NonNull ToggleRepetitionCommand command)
|
|
||||||
{
|
|
||||||
id = command.getId();
|
|
||||||
Long habitId = command.habit.getId();
|
|
||||||
if (habitId == null) throw new RuntimeException("Habit not saved");
|
|
||||||
|
|
||||||
this.repTimestamp = command.timestamp.getUnixTime();
|
|
||||||
this.habit = habitId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ToggleRepetitionCommand toCommand(@NonNull HabitList habitList)
|
|
||||||
{
|
|
||||||
Habit h = habitList.getById(habit);
|
|
||||||
if (h == null) throw new HabitNotFoundException();
|
|
||||||
|
|
||||||
ToggleRepetitionCommand command;
|
|
||||||
command = new ToggleRepetitionCommand(
|
|
||||||
habitList, h, new Timestamp(repTimestamp));
|
|
||||||
command.setId(id);
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,6 +37,11 @@ import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
|
|||||||
@ThreadSafe
|
@ThreadSafe
|
||||||
public final class Checkmark
|
public final class Checkmark
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Indicates that there was an explicit skip at the timestamp.
|
||||||
|
*/
|
||||||
|
public static final int SKIPPED = 3;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that there was a repetition at the timestamp.
|
* Indicates that there was a repetition at the timestamp.
|
||||||
*/
|
*/
|
||||||
@@ -59,8 +64,8 @@ public final class Checkmark
|
|||||||
/**
|
/**
|
||||||
* The value of the checkmark.
|
* The value of the checkmark.
|
||||||
* <p>
|
* <p>
|
||||||
* For boolean habits, this equals either UNCHECKED, CHECKED_EXPLICITLY,
|
* For boolean habits, this equals either UNCHECKED, CHECKED_EXPLICITLY, CHECKED_IMPLICITLY
|
||||||
* or CHECKED_IMPLICITLY.
|
* or SKIPPED.
|
||||||
* <p>
|
* <p>
|
||||||
* For numerical habits, this number is stored in thousandths. That
|
* For numerical habits, this number is stored in thousandths. That
|
||||||
* is, if the user enters value 1.50 on the app, it is stored as 1500.
|
* is, if the user enters value 1.50 on the app, it is stored as 1500.
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ public abstract class CheckmarkList
|
|||||||
{
|
{
|
||||||
Timestamp date = rep.getTimestamp();
|
Timestamp date = rep.getTimestamp();
|
||||||
int offset = date.daysUntil(today);
|
int offset = date.daysUntil(today);
|
||||||
checkmarks.set(offset, new Checkmark(date, CHECKED_EXPLICITLY));
|
checkmarks.set(offset, new Checkmark(date, rep.getValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return checkmarks;
|
return checkmarks;
|
||||||
|
|||||||
@@ -27,10 +27,11 @@ import java.util.Calendar;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
|
|
||||||
|
import static org.isoron.uhabits.core.models.Checkmark.*;
|
||||||
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
|
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a record that the user has performed a certain habit at a certain
|
* Represents a record that the user has performed or skipped a certain habit at a certain
|
||||||
* date.
|
* date.
|
||||||
*/
|
*/
|
||||||
public final class Repetition
|
public final class Repetition
|
||||||
@@ -41,9 +42,9 @@ public final class Repetition
|
|||||||
/**
|
/**
|
||||||
* The value of the repetition.
|
* The value of the repetition.
|
||||||
*
|
*
|
||||||
* For boolean habits, this always equals Checkmark.CHECKED_EXPLICITLY.
|
* For boolean habits, this equals CHECKED_EXPLICITLY if performed or SKIPPED if skipped.
|
||||||
* For numerical habits, this number is stored in thousandths. That
|
* For numerical habits, this number is stored in thousandths. That is, if the user enters
|
||||||
* is, if the user enters value 1.50 on the app, it is here stored as 1500.
|
* value 1.50 on the app, it is here stored as 1500.
|
||||||
*/
|
*/
|
||||||
private final int value;
|
private final int value;
|
||||||
|
|
||||||
@@ -61,6 +62,21 @@ public final class Repetition
|
|||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int nextToggleValue(int value)
|
||||||
|
{
|
||||||
|
switch(value) {
|
||||||
|
case UNCHECKED:
|
||||||
|
case CHECKED_IMPLICITLY:
|
||||||
|
return CHECKED_EXPLICITLY;
|
||||||
|
case CHECKED_EXPLICITLY:
|
||||||
|
return SKIPPED;
|
||||||
|
default:
|
||||||
|
case SKIPPED:
|
||||||
|
return UNCHECKED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o)
|
public boolean equals(Object o)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -119,15 +119,15 @@ public abstract class RepetitionList
|
|||||||
public abstract Repetition getNewest();
|
public abstract Repetition getNewest();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the total number of repetitions for each month, from the first
|
* Returns the total number of successful repetitions for each month, from the first
|
||||||
* repetition until today, grouped by day of week.
|
* repetition until today, grouped by day of week.
|
||||||
* <p>
|
* <p>
|
||||||
* The repetitions are returned in a HashMap. The key is the timestamp for
|
* The repetitions are returned in a HashMap. The key is the timestamp for
|
||||||
* the first day of the month, at midnight (00:00). The value is an integer
|
* the first day of the month, at midnight (00:00). The value is an integer
|
||||||
* array with 7 entries. The first entry contains the total number of
|
* array with 7 entries. The first entry contains the total number of
|
||||||
* repetitions during the specified month that occurred on a Saturday. The
|
* successful repetitions during the specified month that occurred on a Saturday. The
|
||||||
* second entry corresponds to Sunday, and so on. If there are no
|
* second entry corresponds to Sunday, and so on. If there are no
|
||||||
* repetitions during a certain month, the value is null.
|
* successful repetitions during a certain month, the value is null.
|
||||||
*
|
*
|
||||||
* @return total number of repetitions by month versus day of week
|
* @return total number of repetitions by month versus day of week
|
||||||
*/
|
*/
|
||||||
@@ -140,6 +140,9 @@ public abstract class RepetitionList
|
|||||||
|
|
||||||
for (Repetition r : reps)
|
for (Repetition r : reps)
|
||||||
{
|
{
|
||||||
|
if (!habit.isNumerical() && r.getValue() != Checkmark.CHECKED_EXPLICITLY)
|
||||||
|
continue;
|
||||||
|
|
||||||
Calendar date = r.getTimestamp().toCalendar();
|
Calendar date = r.getTimestamp().toCalendar();
|
||||||
int weekday = r.getTimestamp().getWeekday();
|
int weekday = r.getTimestamp().getWeekday();
|
||||||
date.set(Calendar.DAY_OF_MONTH, 1);
|
date.set(Calendar.DAY_OF_MONTH, 1);
|
||||||
@@ -202,12 +205,6 @@ public abstract class RepetitionList
|
|||||||
return rep;
|
return rep;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the number of all repetitions
|
|
||||||
*
|
|
||||||
* @return number of all repetitions
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public abstract long getTotalCount();
|
public abstract long getTotalCount();
|
||||||
|
|
||||||
public void toggle(Timestamp timestamp, int value)
|
public void toggle(Timestamp timestamp, int value)
|
||||||
|
|||||||
@@ -275,17 +275,16 @@ public abstract class ScoreList implements Iterable<Score>
|
|||||||
for (int i = 0; i < checkmarkValues.length; i++)
|
for (int i = 0; i < checkmarkValues.length; i++)
|
||||||
{
|
{
|
||||||
double value = checkmarkValues[checkmarkValues.length - i - 1];
|
double value = checkmarkValues[checkmarkValues.length - i - 1];
|
||||||
|
if (!habit.isNumerical() || value != Checkmark.SKIPPED)
|
||||||
if (habit.isNumerical())
|
|
||||||
{
|
{
|
||||||
value /= 1000;
|
if (habit.isNumerical())
|
||||||
value /= habit.getTargetValue();
|
{
|
||||||
|
value /= 1000;
|
||||||
|
value /= habit.getTargetValue();
|
||||||
|
}
|
||||||
value = Math.min(1, value);
|
value = Math.min(1, value);
|
||||||
|
previousValue = Score.compute(freq, previousValue, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!habit.isNumerical() && value > 0) value = 1;
|
|
||||||
|
|
||||||
previousValue = Score.compute(freq, previousValue, value);
|
|
||||||
scores.add(new Score(from.plus(i), previousValue));
|
scores.add(new Score(from.plus(i), previousValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,11 @@ public class MemoryRepetitionList extends RepetitionList
|
|||||||
@Override
|
@Override
|
||||||
public long getTotalCount()
|
public long getTotalCount()
|
||||||
{
|
{
|
||||||
return list.size();
|
int count = 0;
|
||||||
|
for (Repetition rep : list)
|
||||||
|
if (rep.getValue() == Checkmark.CHECKED_EXPLICITLY)
|
||||||
|
count++;
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class ReminderScheduler implements CommandRunner.Listener
|
|||||||
public synchronized void onCommandExecuted(@NonNull Command command,
|
public synchronized void onCommandExecuted(@NonNull Command command,
|
||||||
@Nullable Long refreshKey)
|
@Nullable Long refreshKey)
|
||||||
{
|
{
|
||||||
if (command instanceof ToggleRepetitionCommand) return;
|
if (command instanceof CreateRepetitionCommand) return;
|
||||||
if (command instanceof ChangeHabitColorCommand) return;
|
if (command instanceof ChangeHabitColorCommand) return;
|
||||||
scheduleAll();
|
scheduleAll();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,17 +76,11 @@ public class NotificationTray
|
|||||||
public void onCommandExecuted(@NonNull Command command,
|
public void onCommandExecuted(@NonNull Command command,
|
||||||
@Nullable Long refreshKey)
|
@Nullable Long refreshKey)
|
||||||
{
|
{
|
||||||
if (command instanceof ToggleRepetitionCommand)
|
if (command instanceof CreateRepetitionCommand)
|
||||||
{
|
{
|
||||||
ToggleRepetitionCommand toggleCmd =
|
CreateRepetitionCommand createCmd = (CreateRepetitionCommand) command;
|
||||||
(ToggleRepetitionCommand) command;
|
Habit habit = createCmd.getHabit();
|
||||||
|
cancel(habit);
|
||||||
Habit habit = toggleCmd.getHabit();
|
|
||||||
taskRunner.execute(() ->
|
|
||||||
{
|
|
||||||
if (habit.getCheckmarks().getTodayValue() !=
|
|
||||||
Checkmark.UNCHECKED) cancel(habit);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command instanceof DeleteHabitsCommand)
|
if (command instanceof DeleteHabitsCommand)
|
||||||
|
|||||||
@@ -149,11 +149,11 @@ public class ListHabitsBehavior
|
|||||||
if (prefs.isFirstRun()) onFirstRun();
|
if (prefs.isFirstRun()) onFirstRun();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onToggle(@NonNull Habit habit, Timestamp timestamp)
|
public void onToggle(@NonNull Habit habit, Timestamp timestamp, int value)
|
||||||
{
|
{
|
||||||
commandRunner.execute(
|
commandRunner.execute(
|
||||||
new ToggleRepetitionCommand(habitList, habit, timestamp),
|
new CreateRepetitionCommand(habit, timestamp, value),
|
||||||
habit.getId());
|
habit.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Message
|
public enum Message
|
||||||
|
|||||||
@@ -56,10 +56,10 @@ public class ShowHabitBehavior
|
|||||||
screen.showEditHistoryScreen();
|
screen.showEditHistoryScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onToggleCheckmark(Timestamp timestamp)
|
public void onToggleCheckmark(Timestamp timestamp, int value)
|
||||||
{
|
{
|
||||||
commandRunner.execute(
|
commandRunner.execute(
|
||||||
new ToggleRepetitionCommand(habitList, habit, timestamp), null);
|
new CreateRepetitionCommand(habit, timestamp, value), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface Screen
|
public interface Screen
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ public class ShowHabitMenuBehavior
|
|||||||
if (i % 7 == 0) strength = max(0, min(100, strength + 10 * random.nextGaussian()));
|
if (i % 7 == 0) strength = max(0, min(100, strength + 10 * random.nextGaussian()));
|
||||||
if (random.nextInt(100) > strength) continue;
|
if (random.nextInt(100) > strength) continue;
|
||||||
|
|
||||||
int value = 1;
|
int value = Checkmark.CHECKED_EXPLICITLY;
|
||||||
if (habit.isNumerical())
|
if (habit.isNumerical())
|
||||||
value = (int) (1000 + 250 * random.nextGaussian() * strength / 100) * 1000;
|
value = (int) (1000 + 250 * random.nextGaussian() * strength / 100) * 1000;
|
||||||
|
|
||||||
|
|||||||
@@ -29,19 +29,15 @@ import javax.inject.*;
|
|||||||
|
|
||||||
public class WidgetBehavior
|
public class WidgetBehavior
|
||||||
{
|
{
|
||||||
private HabitList habitList;
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private final CommandRunner commandRunner;
|
private final CommandRunner commandRunner;
|
||||||
|
|
||||||
private NotificationTray notificationTray;
|
private NotificationTray notificationTray;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public WidgetBehavior(@NonNull HabitList habitList,
|
public WidgetBehavior(@NonNull CommandRunner commandRunner,
|
||||||
@NonNull CommandRunner commandRunner,
|
|
||||||
@NonNull NotificationTray notificationTray)
|
@NonNull NotificationTray notificationTray)
|
||||||
{
|
{
|
||||||
this.habitList = habitList;
|
|
||||||
this.commandRunner = commandRunner;
|
this.commandRunner = commandRunner;
|
||||||
this.notificationTray = notificationTray;
|
this.notificationTray = notificationTray;
|
||||||
}
|
}
|
||||||
@@ -51,7 +47,7 @@ public class WidgetBehavior
|
|||||||
notificationTray.cancel(habit);
|
notificationTray.cancel(habit);
|
||||||
Repetition rep = habit.getRepetitions().getByTimestamp(timestamp);
|
Repetition rep = habit.getRepetitions().getByTimestamp(timestamp);
|
||||||
if (rep != null) return;
|
if (rep != null) return;
|
||||||
performToggle(habit, timestamp);
|
performToggle(habit, timestamp, Checkmark.CHECKED_EXPLICITLY);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onRemoveRepetition(@NonNull Habit habit, Timestamp timestamp)
|
public void onRemoveRepetition(@NonNull Habit habit, Timestamp timestamp)
|
||||||
@@ -59,18 +55,20 @@ public class WidgetBehavior
|
|||||||
notificationTray.cancel(habit);
|
notificationTray.cancel(habit);
|
||||||
Repetition rep = habit.getRepetitions().getByTimestamp(timestamp);
|
Repetition rep = habit.getRepetitions().getByTimestamp(timestamp);
|
||||||
if (rep == null) return;
|
if (rep == null) return;
|
||||||
performToggle(habit, timestamp);
|
performToggle(habit, timestamp, Checkmark.UNCHECKED);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onToggleRepetition(@NonNull Habit habit, Timestamp timestamp)
|
public void onToggleRepetition(@NonNull Habit habit, Timestamp timestamp)
|
||||||
{
|
{
|
||||||
performToggle(habit, timestamp);
|
Repetition previous = habit.getRepetitions().getByTimestamp(timestamp);
|
||||||
|
if(previous == null) performToggle(habit, timestamp, Checkmark.CHECKED_EXPLICITLY);
|
||||||
|
else performToggle(habit, timestamp, Repetition.nextToggleValue(previous.getValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void performToggle(@NonNull Habit habit, Timestamp timestamp)
|
private void performToggle(@NonNull Habit habit, Timestamp timestamp, int value)
|
||||||
{
|
{
|
||||||
commandRunner.execute(
|
commandRunner.execute(
|
||||||
new ToggleRepetitionCommand(habitList, habit, timestamp),
|
new CreateRepetitionCommand(habit, timestamp, value),
|
||||||
habit.getId());
|
habit.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,20 +136,6 @@ public class CommandParserTest extends BaseUnitTest
|
|||||||
.getData()));
|
.getData()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testDecodeToggleCommand() throws JSONException
|
|
||||||
{
|
|
||||||
ToggleRepetitionCommand original, decoded;
|
|
||||||
original = new ToggleRepetitionCommand(habitList, habit,
|
|
||||||
Timestamp.ZERO.plus(100));
|
|
||||||
decoded = (ToggleRepetitionCommand) parser.parse(original.toJson());
|
|
||||||
|
|
||||||
MatcherAssert.assertThat(decoded.getId(), equalTo(original.getId()));
|
|
||||||
MatcherAssert.assertThat(decoded.timestamp, equalTo(original
|
|
||||||
.timestamp));
|
|
||||||
MatcherAssert.assertThat(decoded.habit, equalTo(original.habit));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testDecodeUnarchiveCommand() throws JSONException
|
public void testDecodeUnarchiveCommand() throws JSONException
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
|
|
||||||
*
|
|
||||||
* 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.core.commands;
|
|
||||||
|
|
||||||
import org.isoron.uhabits.core.*;
|
|
||||||
import org.isoron.uhabits.core.models.*;
|
|
||||||
import org.isoron.uhabits.core.utils.*;
|
|
||||||
import org.junit.*;
|
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
|
||||||
import static org.hamcrest.Matchers.*;
|
|
||||||
import static org.junit.Assert.*;
|
|
||||||
|
|
||||||
public class ToggleRepetitionCommandTest extends BaseUnitTest
|
|
||||||
{
|
|
||||||
|
|
||||||
private ToggleRepetitionCommand command;
|
|
||||||
private Habit habit;
|
|
||||||
private Timestamp today;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Before
|
|
||||||
public void setUp() throws Exception
|
|
||||||
{
|
|
||||||
super.setUp();
|
|
||||||
|
|
||||||
habit = fixtures.createShortHabit();
|
|
||||||
habitList.add(habit);
|
|
||||||
|
|
||||||
today = DateUtils.getToday();
|
|
||||||
command = new ToggleRepetitionCommand(habitList, habit, today);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testExecuteUndoRedo()
|
|
||||||
{
|
|
||||||
assertTrue(habit.getRepetitions().containsTimestamp(today));
|
|
||||||
|
|
||||||
command.execute();
|
|
||||||
assertFalse(habit.getRepetitions().containsTimestamp(today));
|
|
||||||
|
|
||||||
command.undo();
|
|
||||||
assertTrue(habit.getRepetitions().containsTimestamp(today));
|
|
||||||
|
|
||||||
command.execute();
|
|
||||||
assertFalse(habit.getRepetitions().containsTimestamp(today));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRecord()
|
|
||||||
{
|
|
||||||
ToggleRepetitionCommand.Record rec = command.toRecord();
|
|
||||||
ToggleRepetitionCommand other = rec.toCommand(habitList);
|
|
||||||
|
|
||||||
assertThat(command.getId(), equalTo(other.getId()));
|
|
||||||
assertThat(command.timestamp, equalTo(other.timestamp));
|
|
||||||
assertThat(command.habit, equalTo(other.habit));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -84,9 +84,7 @@ public class HabitCardListCacheTest extends BaseUnitTest
|
|||||||
{
|
{
|
||||||
Habit h2 = habitList.getByPosition(2);
|
Habit h2 = habitList.getByPosition(2);
|
||||||
Timestamp today = DateUtils.getToday();
|
Timestamp today = DateUtils.getToday();
|
||||||
commandRunner.execute(new ToggleRepetitionCommand(habitList, h2, today),
|
commandRunner.execute(new CreateRepetitionCommand(h2, today, Checkmark.UNCHECKED), h2.getId());
|
||||||
h2.getId());
|
|
||||||
|
|
||||||
verify(listener).onItemChanged(2);
|
verify(listener).onItemChanged(2);
|
||||||
verify(listener).onRefreshFinished();
|
verify(listener).onRefreshFinished();
|
||||||
verifyNoMoreInteractions(listener);
|
verifyNoMoreInteractions(listener);
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ public class ListHabitsBehaviorTest extends BaseUnitTest
|
|||||||
public void testOnToggle()
|
public void testOnToggle()
|
||||||
{
|
{
|
||||||
assertTrue(habit1.isCompletedToday());
|
assertTrue(habit1.isCompletedToday());
|
||||||
behavior.onToggle(habit1, DateUtils.getToday());
|
behavior.onToggle(habit1, DateUtils.getToday(), Checkmark.UNCHECKED);
|
||||||
assertFalse(habit1.isCompletedToday());
|
assertFalse(habit1.isCompletedToday());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user