Merge remote-tracking branch 'upstream/dev' into dev

pull/699/head
MarKco 5 years ago
commit 1db37a64c2

@ -78,8 +78,6 @@ android {
} }
dependencies { dependencies {
androidTestAnnotationProcessor "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
androidTestCompileOnly "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$ESPRESSO_VERSION" androidTestImplementation "androidx.test.espresso:espresso-contrib:$ESPRESSO_VERSION"
androidTestImplementation "androidx.test.espresso:espresso-core:$ESPRESSO_VERSION" androidTestImplementation "androidx.test.espresso:espresso-core:$ESPRESSO_VERSION"
androidTestImplementation "com.google.dagger:dagger:$DAGGER_VERSION" androidTestImplementation "com.google.dagger:dagger:$DAGGER_VERSION"
@ -93,8 +91,6 @@ dependencies {
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
androidTestImplementation 'androidx.test:rules:1.3.0' androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation project(":uhabits-core") androidTestImplementation project(":uhabits-core")
annotationProcessor "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
compileOnly "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
compileOnly "javax.annotation:jsr250-api:1.0" compileOnly "javax.annotation:jsr250-api:1.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
implementation "com.github.paolorotolo:appintro:3.4.0" implementation "com.github.paolorotolo:appintro:3.4.0"

@ -22,6 +22,7 @@ package org.isoron.uhabits.activities.common.views;
import androidx.test.ext.junit.runners.*; import androidx.test.ext.junit.runners.*;
import androidx.test.filters.*; import androidx.test.filters.*;
import org.apache.commons.lang3.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.ui.callbacks.*; import org.isoron.uhabits.core.ui.callbacks.*;
@ -56,9 +57,16 @@ public class HistoryChartTest extends BaseViewTest
habit = fixtures.createLongHabit(); habit = fixtures.createLongHabit();
today = new Timestamp(DateUtils.getStartOfToday()); today = new Timestamp(DateUtils.getStartOfToday());
Integer[] entries = habit
.getComputedEntries()
.getByInterval(today.minus(300), today)
.stream()
.map(Entry::getValue)
.toArray(Integer[]::new);
chart = new HistoryChart(targetContext); chart = new HistoryChart(targetContext);
chart.setSkipEnabled(true); chart.setSkipEnabled(true);
chart.setEntries(habit.getComputedEntries().getAllValues()); chart.setEntries(ArrayUtils.toPrimitive(entries));
chart.setColor(PaletteUtilsKt.toFixedAndroidColor(habit.getColor())); chart.setColor(PaletteUtilsKt.toFixedAndroidColor(habit.getColor()));
measureView(chart, dpToPixels(400), dpToPixels(200)); measureView(chart, dpToPixels(400), dpToPixels(200));

@ -23,8 +23,8 @@ import androidx.test.ext.junit.runners.*;
import androidx.test.filters.*; import androidx.test.filters.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.activities.habits.show.views.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.ui.screens.habits.show.views.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;
import org.junit.*; import org.junit.*;
import org.junit.runner.*; import org.junit.runner.*;
@ -49,8 +49,8 @@ public class ScoreChartTest extends BaseViewTest
fixtures.purgeHabits(habitList); fixtures.purgeHabits(habitList);
habit = fixtures.createLongHabit(); habit = fixtures.createLongHabit();
presenter = new ScoreCardPresenter(habit, prefs.getFirstWeekday()); presenter = new ScoreCardPresenter();
ScoreCardViewModel model = presenter.present(0); ScoreCardViewModel model = presenter.present(habit, prefs.getFirstWeekday(), 0);
view = new ScoreChart(targetContext); view = new ScoreChart(targetContext);
view.setScores(model.getScores()); view.setScores(model.getScores());
@ -84,7 +84,7 @@ public class ScoreChartTest extends BaseViewTest
@Test @Test
public void testRender_withMonthlyBucket() throws Throwable public void testRender_withMonthlyBucket() throws Throwable
{ {
ScoreCardViewModel model = presenter.present(2); ScoreCardViewModel model = presenter.present(habit, prefs.getFirstWeekday(), 2);
view.setScores(model.getScores()); view.setScores(model.getScores());
view.setBucketSize(model.getBucketSize()); view.setBucketSize(model.getBucketSize());
view.invalidate(); view.invalidate();
@ -102,7 +102,7 @@ public class ScoreChartTest extends BaseViewTest
@Test @Test
public void testRender_withYearlyBucket() throws Throwable public void testRender_withYearlyBucket() throws Throwable
{ {
ScoreCardViewModel model = presenter.present(4); ScoreCardViewModel model = presenter.present(habit, prefs.getFirstWeekday(), 4);
view.setScores(model.getScores()); view.setScores(model.getScores());
view.setBucketSize(model.getBucketSize()); view.setBucketSize(model.getBucketSize());
view.invalidate(); view.invalidate();

@ -25,6 +25,7 @@ import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R 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.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -37,6 +38,7 @@ class HabitCardViewTest : BaseViewTest() {
private lateinit var view: HabitCardView private lateinit var view: HabitCardView
private lateinit var habit1: Habit private lateinit var habit1: Habit
private lateinit var habit2: Habit private lateinit var habit2: Habit
private lateinit var today: Timestamp
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
@ -44,11 +46,16 @@ class HabitCardViewTest : BaseViewTest() {
habit1 = fixtures.createLongHabit() habit1 = fixtures.createLongHabit()
habit2 = fixtures.createLongNumericalHabit() habit2 = fixtures.createLongNumericalHabit()
val today = DateUtils.getTodayWithOffset() today = DateUtils.getTodayWithOffset()
val entries = habit1
.computedEntries
.getByInterval(today.minus(300), today)
.map { it.value }.toIntArray()
view = component.getHabitCardViewFactory().create().apply { view = component.getHabitCardViewFactory().create().apply {
habit = habit1 habit = habit1
values = habit1.computedEntries.getAllValues() values = entries
score = habit1.scores.get(today).value score = habit1.scores.get(today).value
isSelected = false isSelected = false
buttonCount = 5 buttonCount = 5
@ -73,9 +80,14 @@ class HabitCardViewTest : BaseViewTest() {
@Test @Test
fun testRender_numerical() { fun testRender_numerical() {
val entries = habit2
.computedEntries
.getByInterval(today.minus(300), today)
.map { it.value }.toIntArray()
view.apply { view.apply {
habit = habit2 habit = habit2
values = habit2.computedEntries.getAllValues() values = entries
} }
assertRenders(view, "$PATH/render_numerical.png") assertRenders(view, "$PATH/render_numerical.png")
} }

@ -24,15 +24,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardPresenter
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@MediumTest @MediumTest
class FrequencyCardTest : BaseViewTest() { class FrequencyCardViewTest : BaseViewTest() {
val PATH = "habits/show/FrequencyCard/" val PATH = "habits/show/FrequencyCard/"
private lateinit var view: FrequencyCard private lateinit var view: FrequencyCardView
@Before @Before
override fun setUp() { override fun setUp() {
@ -41,8 +42,8 @@ class FrequencyCardTest : BaseViewTest() {
view = LayoutInflater view = LayoutInflater
.from(targetContext) .from(targetContext)
.inflate(R.layout.show_habit, null) .inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.frequencyCard) as FrequencyCard .findViewById<View>(R.id.frequencyCard) as FrequencyCardView
view.update(FrequencyCardPresenter(habit, 0).present()) view.update(FrequencyCardPresenter().present(habit = habit, firstWeekday = 0))
measureView(view, 800f, 600f) measureView(view, 800f, 600f)
} }

@ -24,14 +24,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@MediumTest @MediumTest
class HistoryCardTest : BaseViewTest() { class HistoryCardViewTest : BaseViewTest() {
private lateinit var view: HistoryCard private lateinit var view: HistoryCardView
val PATH = "habits/show/HistoryCard/" val PATH = "habits/show/HistoryCard/"
@Before @Before
@ -41,13 +42,13 @@ class HistoryCardTest : BaseViewTest() {
view = LayoutInflater view = LayoutInflater
.from(targetContext) .from(targetContext)
.inflate(R.layout.show_habit, null) .inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.historyCard) as HistoryCard .findViewById<View>(R.id.historyCard) as HistoryCardView
view.update( view.update(
HistoryCardPresenter( HistoryCardPresenter().present(
habit = habit, habit = habit,
firstWeekday = 1, firstWeekday = 1,
isSkipEnabled = false isSkipEnabled = false
).present() )
) )
measureView(view, 800f, 600f) measureView(view, 800f, 600f)
} }

@ -25,6 +25,7 @@ import androidx.test.filters.MediumTest
import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.equalTo
import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardViewModel
import org.junit.Assert.assertThat import org.junit.Assert.assertThat
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test

@ -25,6 +25,7 @@ import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardViewModel
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith

@ -24,15 +24,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@MediumTest @MediumTest
class ScoreCardTest : BaseViewTest() { class ScoreCardViewTest : BaseViewTest() {
val PATH = "habits/show/ScoreCard/" val PATH = "habits/show/ScoreCard/"
private lateinit var view: ScoreCard private lateinit var view: ScoreCardView
@Before @Before
override fun setUp() { override fun setUp() {
@ -41,8 +42,14 @@ class ScoreCardTest : BaseViewTest() {
view = LayoutInflater view = LayoutInflater
.from(targetContext) .from(targetContext)
.inflate(R.layout.show_habit, null) .inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.scoreCard) as ScoreCard .findViewById<View>(R.id.scoreCard) as ScoreCardView
view.update(ScoreCardPresenter(habit, 0).present(0)) view.update(
ScoreCardPresenter().present(
habit = habit,
firstWeekday = 0,
spinnerPosition = 0,
)
)
measureView(view, 800f, 600f) measureView(view, 800f, 600f)
} }

@ -24,6 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCardViewModel
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith

@ -23,7 +23,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import org.isoron.uhabits.BaseViewTest import org.isoron.uhabits.BaseViewTest
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.PaletteColor import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.models.WeekdayList.EVERY_DAY
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardViewModel
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -44,11 +48,12 @@ class SubtitleCardViewTest : BaseViewTest() {
view.update( view.update(
SubtitleCardViewModel( SubtitleCardViewModel(
color = PaletteColor(7), color = PaletteColor(7),
frequencyText = "3 times in 7 days", frequency = Frequency(3, 7),
isNumerical = false, isNumerical = false,
question = "Did you meditate this morning?", question = "Did you meditate this morning?",
reminderText = "8:30 AM", reminder = Reminder(8, 30, EVERY_DAY),
targetText = "", unit = "",
targetValue = 0.0,
) )
) )
measureView(view, 800f, 200f) measureView(view, 800f, 200f)

@ -25,8 +25,6 @@ import android.content.res.*;
import androidx.annotation.*; import androidx.annotation.*;
import androidx.appcompat.app.*; import androidx.appcompat.app.*;
import com.google.auto.factory.*;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.core.ui.callbacks.*; import org.isoron.uhabits.core.ui.callbacks.*;
import org.isoron.uhabits.inject.*; import org.isoron.uhabits.inject.*;
@ -34,12 +32,11 @@ import org.isoron.uhabits.inject.*;
/** /**
* Dialog that asks the user confirmation before executing a delete operation. * Dialog that asks the user confirmation before executing a delete operation.
*/ */
@AutoFactory(allowSubclasses = true)
public class ConfirmDeleteDialog extends AlertDialog public class ConfirmDeleteDialog extends AlertDialog
{ {
protected ConfirmDeleteDialog(@Provided @ActivityContext Context context, public ConfirmDeleteDialog(@ActivityContext Context context,
@NonNull OnConfirmedCallback callback, @NonNull OnConfirmedCallback callback,
int quantity) int quantity)
{ {
super(context); super(context);
Resources res = context.getResources(); Resources res = context.getResources();

@ -25,17 +25,14 @@ import android.content.res.*;
import androidx.annotation.*; import androidx.annotation.*;
import androidx.appcompat.app.*; import androidx.appcompat.app.*;
import com.google.auto.factory.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.core.ui.callbacks.*; import org.isoron.uhabits.core.ui.callbacks.*;
import org.isoron.uhabits.inject.*; import org.isoron.uhabits.inject.*;
@AutoFactory(allowSubclasses = true)
public class ConfirmSyncKeyDialog extends AlertDialog public class ConfirmSyncKeyDialog extends AlertDialog
{ {
protected ConfirmSyncKeyDialog(@Provided @ActivityContext Context context, public ConfirmSyncKeyDialog(@ActivityContext Context context,
@NonNull OnConfirmedCallback callback) @NonNull OnConfirmedCallback callback)
{ {
super(context); super(context);
setTitle(R.string.device_sync); setTitle(R.string.device_sync);

@ -36,6 +36,7 @@ import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*; import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.tasks.*; import org.isoron.uhabits.core.tasks.*;
import org.isoron.uhabits.core.ui.callbacks.*; import org.isoron.uhabits.core.ui.callbacks.*;
import org.isoron.uhabits.core.ui.screens.habits.show.views.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;
import org.jetbrains.annotations.*; import org.jetbrains.annotations.*;
@ -178,7 +179,12 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
@Override @Override
public void doInBackground() public void doInBackground()
{ {
checkmarks = habit.getComputedEntries().getAllValues(); HistoryCardViewModel model = new HistoryCardPresenter().present(
habit,
prefs.getFirstWeekday(),
prefs.isSkipEnabled()
);
checkmarks = model.getEntries();
} }
@Override @Override

@ -27,8 +27,8 @@ import androidx.appcompat.app.AppCompatActivity
import dagger.Lazy import dagger.Lazy
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory import org.isoron.uhabits.activities.common.dialogs.ColorPickerDialogFactory
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialogFactory import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialog
import org.isoron.uhabits.activities.common.dialogs.ConfirmSyncKeyDialogFactory import org.isoron.uhabits.activities.common.dialogs.ConfirmSyncKeyDialog
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
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
@ -91,8 +91,6 @@ class ListHabitsScreen
private val taskRunner: TaskRunner, private val taskRunner: TaskRunner,
private val exportDBFactory: ExportDBTaskFactory, private val exportDBFactory: ExportDBTaskFactory,
private val importTaskFactory: ImportDataTaskFactory, private val importTaskFactory: ImportDataTaskFactory,
private val confirmDeleteDialogFactory: ConfirmDeleteDialogFactory,
private val confirmSyncKeyDialogFactory: ConfirmSyncKeyDialogFactory,
private val colorPickerFactory: ColorPickerDialogFactory, private val colorPickerFactory: ColorPickerDialogFactory,
private val numberPickerFactory: NumberPickerFactory, private val numberPickerFactory: NumberPickerFactory,
private val behavior: Lazy<ListHabitsBehavior> private val behavior: Lazy<ListHabitsBehavior>
@ -172,7 +170,7 @@ class ListHabitsScreen
} }
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback, quantity: Int) { override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback, quantity: Int) {
confirmDeleteDialogFactory.create(callback, quantity).show() ConfirmDeleteDialog(activity, callback, quantity).show()
} }
override fun showEditHabitsScreen(habits: List<Habit>) { override fun showEditHabitsScreen(habits: List<Habit>) {
@ -250,7 +248,7 @@ class ListHabitsScreen
} }
override fun showConfirmInstallSyncKey(callback: OnConfirmedCallback) { override fun showConfirmInstallSyncKey(callback: OnConfirmedCallback) {
confirmSyncKeyDialogFactory.create(callback).show() ConfirmSyncKeyDialog(activity, callback).show()
} }
private fun getExecuteString(command: Command): String? { private fun getExecuteString(command: Command): String? {

@ -28,8 +28,6 @@ import android.text.TextPaint
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.View import android.view.View
import android.view.View.MeasureSpec.EXACTLY import android.view.View.MeasureSpec.EXACTLY
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Entry.Companion.NO import org.isoron.uhabits.core.models.Entry.Companion.NO
@ -43,11 +41,19 @@ import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.showMessage import org.isoron.uhabits.utils.showMessage
import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.sres
import org.isoron.uhabits.utils.toMeasureSpec import org.isoron.uhabits.utils.toMeasureSpec
import javax.inject.Inject
class CheckmarkButtonViewFactory
@Inject constructor(
@ActivityContext val context: Context,
val preferences: Preferences
) {
fun create() = CheckmarkButtonView(context, preferences)
}
@AutoFactory
class CheckmarkButtonView( class CheckmarkButtonView(
@Provided @ActivityContext context: Context, context: Context,
@Provided val preferences: Preferences val preferences: Preferences
) : View(context), ) : View(context),
View.OnClickListener, View.OnClickListener,
View.OnLongClickListener { View.OnLongClickListener {

@ -20,19 +20,26 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.content.Context import android.content.Context
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import javax.inject.Inject
class CheckmarkPanelViewFactory
@Inject constructor(
@ActivityContext val context: Context,
val preferences: Preferences,
private val buttonFactory: CheckmarkButtonViewFactory
) {
fun create() = CheckmarkPanelView(context, preferences, buttonFactory)
}
@AutoFactory
class CheckmarkPanelView( class CheckmarkPanelView(
@Provided @ActivityContext context: Context, context: Context,
@Provided preferences: Preferences, preferences: Preferences,
@Provided private val buttonFactory: CheckmarkButtonViewFactory private val buttonFactory: CheckmarkButtonViewFactory
) : ButtonPanelView<CheckmarkButtonView>(context, preferences) { ) : ButtonPanelView<CheckmarkButtonView>(context, preferences) {
var values = IntArray(0) var values = IntArray(0)

@ -32,20 +32,28 @@ import androidx.recyclerview.widget.ItemTouchHelper.START
import androidx.recyclerview.widget.ItemTouchHelper.UP import androidx.recyclerview.widget.ItemTouchHelper.UP
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import dagger.Lazy import dagger.Lazy
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.BundleSavedState import org.isoron.uhabits.activities.common.views.BundleSavedState
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import javax.inject.Inject
class HabitCardListViewFactory
@Inject constructor(
@ActivityContext val context: Context,
val adapter: HabitCardListAdapter,
val cardViewFactory: HabitCardViewFactory,
val controller: Lazy<HabitCardListController>
) {
fun create() = HabitCardListView(context, adapter, cardViewFactory, controller)
}
@AutoFactory
class HabitCardListView( class HabitCardListView(
@Provided @ActivityContext context: Context, @ActivityContext context: Context,
@Provided private val adapter: HabitCardListAdapter, private val adapter: HabitCardListAdapter,
@Provided private val cardViewFactory: HabitCardViewFactory, private val cardViewFactory: HabitCardViewFactory,
@Provided private val controller: Lazy<HabitCardListController> private val controller: Lazy<HabitCardListController>
) : RecyclerView(context, null, R.attr.scrollableRecyclerViewStyle) { ) : RecyclerView(context, null, R.attr.scrollableRecyclerViewStyle) {
var checkmarkCount: Int = 0 var checkmarkCount: Int = 0

@ -34,8 +34,6 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.views.RingView import org.isoron.uhabits.activities.common.views.RingView
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
@ -47,13 +45,23 @@ import org.isoron.uhabits.inject.ActivityContext
import org.isoron.uhabits.utils.dp import org.isoron.uhabits.utils.dp
import org.isoron.uhabits.utils.sres import org.isoron.uhabits.utils.sres
import org.isoron.uhabits.utils.toThemedAndroidColor import org.isoron.uhabits.utils.toThemedAndroidColor
import javax.inject.Inject
class HabitCardViewFactory
@Inject constructor(
@ActivityContext val context: Context,
private val checkmarkPanelFactory: CheckmarkPanelViewFactory,
private val numberPanelFactory: NumberPanelViewFactory,
private val behavior: ListHabitsBehavior
) {
fun create() = HabitCardView(context, checkmarkPanelFactory, numberPanelFactory, behavior)
}
@AutoFactory
class HabitCardView( class HabitCardView(
@Provided @ActivityContext context: Context, @ActivityContext context: Context,
@Provided private val checkmarkPanelFactory: CheckmarkPanelViewFactory, private val checkmarkPanelFactory: CheckmarkPanelViewFactory,
@Provided private val numberPanelFactory: NumberPanelViewFactory, private val numberPanelFactory: NumberPanelViewFactory,
@Provided private val behavior: ListHabitsBehavior private val behavior: ListHabitsBehavior
) : FrameLayout(context), ) : FrameLayout(context),
ModelObservable.Listener { ModelObservable.Listener {

@ -28,8 +28,6 @@ import android.text.TextPaint
import android.view.View import android.view.View
import android.view.View.OnClickListener import android.view.View.OnClickListener
import android.view.View.OnLongClickListener import android.view.View.OnLongClickListener
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
@ -38,6 +36,7 @@ import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.getFontAwesome import org.isoron.uhabits.utils.getFontAwesome
import org.isoron.uhabits.utils.showMessage import org.isoron.uhabits.utils.showMessage
import java.text.DecimalFormat import java.text.DecimalFormat
import javax.inject.Inject
private val BOLD_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.BOLD) private val BOLD_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.BOLD)
private val NORMAL_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.NORMAL) private val NORMAL_TYPEFACE = Typeface.create("sans-serif-condensed", Typeface.NORMAL)
@ -55,10 +54,17 @@ fun Double.toShortString(): String = when {
else -> DecimalFormat("#.##").format(this) else -> DecimalFormat("#.##").format(this)
} }
@AutoFactory class NumberButtonViewFactory
@Inject constructor(
@ActivityContext val context: Context,
val preferences: Preferences
) {
fun create() = NumberButtonView(context, preferences)
}
class NumberButtonView( class NumberButtonView(
@Provided @ActivityContext context: Context, @ActivityContext context: Context,
@Provided val preferences: Preferences val preferences: Preferences
) : View(context), ) : View(context),
OnClickListener, OnClickListener,
OnLongClickListener { OnLongClickListener {

@ -20,18 +20,25 @@
package org.isoron.uhabits.activities.habits.list.views package org.isoron.uhabits.activities.habits.list.views
import android.content.Context import android.content.Context
import com.google.auto.factory.AutoFactory
import com.google.auto.factory.Provided
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.utils.DateUtils import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.inject.ActivityContext import org.isoron.uhabits.inject.ActivityContext
import javax.inject.Inject
class NumberPanelViewFactory
@Inject constructor(
@ActivityContext val context: Context,
val preferences: Preferences,
val buttonFactory: NumberButtonViewFactory
) {
fun create() = NumberPanelView(context, preferences, buttonFactory)
}
@AutoFactory
class NumberPanelView( class NumberPanelView(
@Provided @ActivityContext context: Context, @ActivityContext context: Context,
@Provided preferences: Preferences, preferences: Preferences,
@Provided private val buttonFactory: NumberButtonViewFactory private val buttonFactory: NumberButtonViewFactory
) : ButtonPanelView<NumberButtonView>(context, preferences) { ) : ButtonPanelView<NumberButtonView>(context, preferences) {
var values = DoubleArray(0) var values = DoubleArray(0)

@ -30,20 +30,25 @@ import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.HabitsApplication import org.isoron.uhabits.HabitsApplication
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.ConfirmDeleteDialogFactory
import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
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.preferences.Preferences
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitBehavior import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitBehavior
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuBehavior import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitMenuBehavior
import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitPresenter
import org.isoron.uhabits.intents.IntentFactory import org.isoron.uhabits.intents.IntentFactory
class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener { class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
private val presenter = ShowHabitPresenter()
private lateinit var commandRunner: CommandRunner private lateinit var commandRunner: CommandRunner
private lateinit var menu: ShowHabitMenu private lateinit var menu: ShowHabitMenu
private lateinit var presenter: ShowHabitPresenter
private lateinit var view: ShowHabitView private lateinit var view: ShowHabitView
private lateinit var habit: Habit
private lateinit var preferences: Preferences
private val scope = CoroutineScope(Dispatchers.Main) private val scope = CoroutineScope(Dispatchers.Main)
@ -52,21 +57,15 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
val appComponent = (applicationContext as HabitsApplication).component val appComponent = (applicationContext as HabitsApplication).component
val habitList = appComponent.habitList val habitList = appComponent.habitList
val habit = habitList.getById(ContentUris.parseId(intent.data!!))!! habit = habitList.getById(ContentUris.parseId(intent.data!!))!!
val preferences = appComponent.preferences preferences = appComponent.preferences
commandRunner = appComponent.commandRunner commandRunner = appComponent.commandRunner
AndroidThemeSwitcher(this, preferences).apply() AndroidThemeSwitcher(this, preferences).apply()
view = ShowHabitView(this) view = ShowHabitView(this)
presenter = ShowHabitPresenter(
context = this,
habit = habit,
preferences = appComponent.preferences,
)
val screen = ShowHabitScreen( val screen = ShowHabitScreen(
activity = this, activity = this,
confirmDeleteDialogFactory = ConfirmDeleteDialogFactory { this },
habit = habit, habit = habit,
intentFactory = IntentFactory(), intentFactory = IntentFactory(),
numberPickerFactory = NumberPickerFactory(this), numberPickerFactory = NumberPickerFactory(this),
@ -129,7 +128,12 @@ class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
fun refresh() { fun refresh() {
scope.launch { scope.launch {
view.update(presenter.present()) view.update(
presenter.present(
habit = habit,
preferences = preferences,
)
)
} }
} }
} }

@ -20,7 +20,7 @@
package org.isoron.uhabits.activities.habits.show package org.isoron.uhabits.activities.habits.show
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.common.dialogs.ConfirmDeleteDialogFactory 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.NumberPickerFactory import org.isoron.uhabits.activities.common.dialogs.NumberPickerFactory
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
@ -36,7 +36,6 @@ import org.isoron.uhabits.widgets.WidgetUpdater
class ShowHabitScreen( class ShowHabitScreen(
val activity: ShowHabitActivity, val activity: ShowHabitActivity,
val confirmDeleteDialogFactory: ConfirmDeleteDialogFactory,
val habit: Habit, val habit: Habit,
val intentFactory: IntentFactory, val intentFactory: IntentFactory,
val numberPickerFactory: NumberPickerFactory, val numberPickerFactory: NumberPickerFactory,
@ -79,7 +78,7 @@ class ShowHabitScreen(
} }
override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback) { override fun showDeleteConfirmationScreen(callback: OnConfirmedCallback) {
confirmDeleteDialogFactory.create(callback, 1).show() ConfirmDeleteDialog(activity, callback, 1).show()
} }
override fun close() { override fun close() {

@ -22,45 +22,10 @@ package org.isoron.uhabits.activities.habits.show
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.FrameLayout import android.widget.FrameLayout
import org.isoron.uhabits.activities.habits.show.views.BarCardPresenter import org.isoron.uhabits.core.ui.screens.habits.show.ShowHabitViewModel
import org.isoron.uhabits.activities.habits.show.views.BarCardViewModel
import org.isoron.uhabits.activities.habits.show.views.FrequencyCardPresenter
import org.isoron.uhabits.activities.habits.show.views.FrequencyCardViewModel
import org.isoron.uhabits.activities.habits.show.views.HistoryCardPresenter
import org.isoron.uhabits.activities.habits.show.views.HistoryCardViewModel
import org.isoron.uhabits.activities.habits.show.views.NotesCardPresenter
import org.isoron.uhabits.activities.habits.show.views.NotesCardViewModel
import org.isoron.uhabits.activities.habits.show.views.OverviewCardPresenter
import org.isoron.uhabits.activities.habits.show.views.OverviewCardViewModel
import org.isoron.uhabits.activities.habits.show.views.ScoreCardPresenter
import org.isoron.uhabits.activities.habits.show.views.ScoreCardViewModel
import org.isoron.uhabits.activities.habits.show.views.StreakCardViewModel
import org.isoron.uhabits.activities.habits.show.views.StreakCartPresenter
import org.isoron.uhabits.activities.habits.show.views.SubtitleCardPresenter
import org.isoron.uhabits.activities.habits.show.views.SubtitleCardViewModel
import org.isoron.uhabits.activities.habits.show.views.TargetCardPresenter
import org.isoron.uhabits.activities.habits.show.views.TargetCardViewModel
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.databinding.ShowHabitBinding import org.isoron.uhabits.databinding.ShowHabitBinding
import org.isoron.uhabits.utils.setupToolbar import org.isoron.uhabits.utils.setupToolbar
data class ShowHabitViewModel(
val title: String = "",
val isNumerical: Boolean = false,
val color: PaletteColor = PaletteColor(1),
val subtitle: SubtitleCardViewModel,
val overview: OverviewCardViewModel,
val notes: NotesCardViewModel,
val target: TargetCardViewModel,
val streaks: StreakCardViewModel,
val scores: ScoreCardViewModel,
val frequency: FrequencyCardViewModel,
val history: HistoryCardViewModel,
val bar: BarCardViewModel,
)
class ShowHabitView(context: Context) : FrameLayout(context) { class ShowHabitView(context: Context) : FrameLayout(context) {
private val binding = ShowHabitBinding.inflate(LayoutInflater.from(context)) private val binding = ShowHabitBinding.inflate(LayoutInflater.from(context))
@ -96,58 +61,3 @@ class ShowHabitView(context: Context) : FrameLayout(context) {
} }
} }
} }
class ShowHabitPresenter(
val habit: Habit,
val context: Context,
val preferences: Preferences,
) {
private val subtitleCardPresenter = SubtitleCardPresenter(habit, context)
private val overviewCardPresenter = OverviewCardPresenter(habit)
private val notesCardPresenter = NotesCardPresenter(habit)
private val targetCardPresenter = TargetCardPresenter(
habit = habit,
firstWeekday = preferences.firstWeekday,
resources = context.resources,
)
private val streakCartPresenter = StreakCartPresenter(habit)
private val scoreCardPresenter = ScoreCardPresenter(
habit = habit,
firstWeekday = preferences.firstWeekday,
)
private val frequencyCardPresenter = FrequencyCardPresenter(
habit = habit,
firstWeekday = preferences.firstWeekday,
)
private val historyCardViewModel = HistoryCardPresenter(
habit = habit,
firstWeekday = preferences.firstWeekday,
isSkipEnabled = preferences.isSkipEnabled,
)
private val barCardPresenter = BarCardPresenter(
habit = habit,
firstWeekday = preferences.firstWeekday,
)
suspend fun present(): ShowHabitViewModel {
return ShowHabitViewModel(
title = habit.name,
color = habit.color,
isNumerical = habit.isNumerical,
subtitle = subtitleCardPresenter.present(),
overview = overviewCardPresenter.present(),
notes = notesCardPresenter.present(),
target = targetCardPresenter.present(),
streaks = streakCartPresenter.present(),
scores = scoreCardPresenter.present(
spinnerPosition = preferences.scoreCardSpinnerPosition
),
frequency = frequencyCardPresenter.present(),
history = historyCardViewModel.present(),
bar = barCardPresenter.present(
boolSpinnerPosition = preferences.barCardBoolSpinnerPosition,
numericalSpinnerPosition = preferences.barCardNumericalSpinnerPosition,
),
)
}
}

@ -1,123 +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.activities.habits.show.views
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.LinearLayout
import org.isoron.uhabits.activities.habits.show.views.ScoreCardPresenter.Companion.getTruncateField
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.databinding.ShowHabitBarBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
data class BarCardViewModel(
val entries: List<Entry>,
val bucketSize: Int,
val color: PaletteColor,
val isNumerical: Boolean,
val numericalSpinnerPosition: Int,
val boolSpinnerPosition: Int,
)
class BarCard(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private var binding = ShowHabitBarBinding.inflate(LayoutInflater.from(context), this)
var onNumericalSpinnerPosition: (position: Int) -> Unit = {}
var onBoolSpinnerPosition: (position: Int) -> Unit = {}
fun update(data: BarCardViewModel) {
binding.barChart.setEntries(data.entries)
binding.barChart.setBucketSize(data.bucketSize)
val androidColor = data.color.toThemedAndroidColor(context)
binding.title.setTextColor(androidColor)
binding.barChart.setColor(androidColor)
if (data.isNumerical) {
binding.boolSpinner.visibility = GONE
} else {
binding.numericalSpinner.visibility = GONE
}
binding.numericalSpinner.onItemSelectedListener = null
binding.numericalSpinner.setSelection(data.numericalSpinnerPosition)
binding.numericalSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
onNumericalSpinnerPosition(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
binding.boolSpinner.onItemSelectedListener = null
binding.boolSpinner.setSelection(data.boolSpinnerPosition)
binding.boolSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
onBoolSpinnerPosition(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
}
class BarCardPresenter(
val habit: Habit,
val firstWeekday: Int,
) {
val numericalBucketSizes = intArrayOf(1, 7, 31, 92, 365)
val boolBucketSizes = intArrayOf(7, 31, 92, 365)
fun present(
numericalSpinnerPosition: Int,
boolSpinnerPosition: Int,
): BarCardViewModel {
val bucketSize = if (habit.isNumerical) {
numericalBucketSizes[numericalSpinnerPosition]
} else {
boolBucketSizes[boolSpinnerPosition]
}
val today = DateUtils.getToday()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries = if (bucketSize == 1) {
habit.computedEntries.getByInterval(oldest, today).map {
if (it.value < 0) Entry(it.timestamp, 0) else it
}
} else {
habit.computedEntries.groupBy(
original = habit.computedEntries.getByInterval(oldest, today),
field = getTruncateField(bucketSize),
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical,
)
}
return BarCardViewModel(
entries = entries,
bucketSize = bucketSize,
color = habit.color,
isNumerical = habit.isNumerical,
numericalSpinnerPosition = numericalSpinnerPosition,
boolSpinnerPosition = boolSpinnerPosition,
)
}
}

@ -0,0 +1,82 @@
/*
* 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.activities.habits.show.views
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.LinearLayout
import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardViewModel
import org.isoron.uhabits.databinding.ShowHabitBarBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private var binding = ShowHabitBarBinding.inflate(LayoutInflater.from(context), this)
var onNumericalSpinnerPosition: (position: Int) -> Unit = {}
var onBoolSpinnerPosition: (position: Int) -> Unit = {}
fun update(data: BarCardViewModel) {
binding.barChart.setEntries(data.entries)
binding.barChart.setBucketSize(data.bucketSize)
val androidColor = data.color.toThemedAndroidColor(context)
binding.title.setTextColor(androidColor)
binding.barChart.setColor(androidColor)
if (data.isNumerical) {
binding.boolSpinner.visibility = GONE
} else {
binding.numericalSpinner.visibility = GONE
}
binding.numericalSpinner.onItemSelectedListener = null
binding.numericalSpinner.setSelection(data.numericalSpinnerPosition)
binding.numericalSpinner.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
onNumericalSpinnerPosition(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
binding.boolSpinner.onItemSelectedListener = null
binding.boolSpinner.setSelection(data.boolSpinnerPosition)
binding.boolSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
onBoolSpinnerPosition(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
}

@ -22,20 +22,11 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardViewModel
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.databinding.ShowHabitFrequencyBinding import org.isoron.uhabits.databinding.ShowHabitFrequencyBinding
import org.isoron.uhabits.utils.toThemedAndroidColor import org.isoron.uhabits.utils.toThemedAndroidColor
import java.util.HashMap
data class FrequencyCardViewModel( class FrequencyCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
val frequency: HashMap<Timestamp, Array<Int>>,
val firstWeekday: Int,
val color: PaletteColor,
)
class FrequencyCard(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private var binding = ShowHabitFrequencyBinding.inflate(LayoutInflater.from(context), this) private var binding = ShowHabitFrequencyBinding.inflate(LayoutInflater.from(context), this)
@ -47,16 +38,3 @@ class FrequencyCard(context: Context, attrs: AttributeSet) : LinearLayout(contex
binding.frequencyChart.setColor(androidColor) binding.frequencyChart.setColor(androidColor)
} }
} }
class FrequencyCardPresenter(
val habit: Habit,
val firstWeekday: Int,
) {
fun present() = FrequencyCardViewModel(
color = habit.color,
frequency = habit.originalEntries.computeWeekdayFrequency(
isNumerical = habit.isNumerical
),
firstWeekday = firstWeekday,
)
}

@ -22,20 +22,11 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardViewModel
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.databinding.ShowHabitHistoryBinding import org.isoron.uhabits.databinding.ShowHabitHistoryBinding
import org.isoron.uhabits.utils.toThemedAndroidColor import org.isoron.uhabits.utils.toThemedAndroidColor
data class HistoryCardViewModel( class HistoryCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
val entries: IntArray,
val color: PaletteColor,
val firstWeekday: Int,
val isNumerical: Boolean,
val isSkipEnabled: Boolean,
)
class HistoryCard(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private var binding = ShowHabitHistoryBinding.inflate(LayoutInflater.from(context), this) private var binding = ShowHabitHistoryBinding.inflate(LayoutInflater.from(context), this)
@ -57,17 +48,3 @@ class HistoryCard(context: Context, attrs: AttributeSet) : LinearLayout(context,
} }
} }
} }
class HistoryCardPresenter(
val habit: Habit,
val firstWeekday: Int,
val isSkipEnabled: Boolean,
) {
fun present() = HistoryCardViewModel(
entries = habit.computedEntries.getAllValues(),
color = habit.color,
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical,
isSkipEnabled = isSkipEnabled,
)
}

@ -23,11 +23,9 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardViewModel
import org.isoron.uhabits.databinding.ShowHabitNotesBinding import org.isoron.uhabits.databinding.ShowHabitNotesBinding
data class NotesCardViewModel(val description: String)
class NotesCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { class NotesCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private val binding = ShowHabitNotesBinding.inflate(LayoutInflater.from(context), this) private val binding = ShowHabitNotesBinding.inflate(LayoutInflater.from(context), this)
fun update(data: NotesCardViewModel) { fun update(data: NotesCardViewModel) {
@ -40,9 +38,3 @@ class NotesCardView(context: Context, attrs: AttributeSet) : LinearLayout(contex
invalidate() invalidate()
} }
} }
class NotesCardPresenter(val habit: Habit) {
fun present() = NotesCardViewModel(
description = habit.description,
)
}

@ -22,25 +22,12 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardViewModel
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.databinding.ShowHabitOverviewBinding import org.isoron.uhabits.databinding.ShowHabitOverviewBinding
import org.isoron.uhabits.utils.StyledResources import org.isoron.uhabits.utils.StyledResources
import org.isoron.uhabits.utils.toThemedAndroidColor import org.isoron.uhabits.utils.toThemedAndroidColor
data class OverviewCardViewModel(
val color: PaletteColor,
val scoreMonthDiff: Float,
val scoreYearDiff: Float,
val scoreToday: Float,
val totalCount: Long,
)
class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private val binding = ShowHabitOverviewBinding.inflate(LayoutInflater.from(context), this) private val binding = ShowHabitOverviewBinding.inflate(LayoutInflater.from(context), this)
@ -71,26 +58,3 @@ class OverviewCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
postInvalidate() postInvalidate()
} }
} }
class OverviewCardPresenter(val habit: Habit) {
suspend fun present(): OverviewCardViewModel = Dispatchers.IO {
val today = DateUtils.getTodayWithOffset()
val lastMonth = today.minus(30)
val lastYear = today.minus(365)
val scores = habit.scores
val scoreToday = scores.get(today).value.toFloat()
val scoreLastMonth = scores.get(lastMonth).value.toFloat()
val scoreLastYear = scores.get(lastYear).value.toFloat()
val totalCount = habit.originalEntries.getKnown()
.filter { it.value == YES_MANUAL }
.count()
.toLong()
return@IO OverviewCardViewModel(
color = habit.color,
scoreToday = scoreToday,
scoreMonthDiff = scoreToday - scoreLastMonth,
scoreYearDiff = scoreToday - scoreLastYear,
totalCount = totalCount,
)
}
}

@ -1,114 +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.activities.habits.show.views
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.LinearLayout
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Score
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.DAY
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.MONTH
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.QUARTER
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.WEEK_NUMBER
import org.isoron.uhabits.core.utils.DateUtils.TruncateField.YEAR
import org.isoron.uhabits.databinding.ShowHabitScoreBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
data class ScoreCardViewModel(
val scores: List<Score>,
val bucketSize: Int,
val spinnerPosition: Int,
val color: PaletteColor,
)
class ScoreCard(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private var binding = ShowHabitScoreBinding.inflate(LayoutInflater.from(context), this)
var onSpinnerPosition: (position: Int) -> Unit = {}
fun update(data: ScoreCardViewModel) {
val androidColor = data.color.toThemedAndroidColor(context)
binding.title.setTextColor(androidColor)
binding.spinner.setSelection(data.spinnerPosition)
binding.scoreView.setScores(data.scores)
binding.scoreView.reset()
binding.scoreView.setBucketSize(data.bucketSize)
binding.scoreView.setColor(androidColor)
binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
onSpinnerPosition(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
}
class ScoreCardPresenter(
val habit: Habit,
val firstWeekday: Int,
) {
companion object {
val BUCKET_SIZES = intArrayOf(1, 7, 31, 92, 365)
fun getTruncateField(bucketSize: Int): DateUtils.TruncateField {
when (bucketSize) {
1 -> return DAY
7 -> return WEEK_NUMBER
31 -> return MONTH
92 -> return QUARTER
365 -> return YEAR
else -> return MONTH
}
}
}
fun present(spinnerPosition: Int): ScoreCardViewModel {
val bucketSize = BUCKET_SIZES[spinnerPosition]
val today = DateUtils.getTodayWithOffset()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val field = getTruncateField(bucketSize)
val scores = habit.scores.getByInterval(oldest, today).groupBy {
DateUtils.truncate(field, it.timestamp, firstWeekday)
}.map { (timestamp, scores) ->
Score(
timestamp,
scores.map {
it.value
}.average()
)
}.sortedBy {
it.timestamp
}.reversed()
return ScoreCardViewModel(
color = habit.color,
scores = scores,
bucketSize = bucketSize,
spinnerPosition = spinnerPosition,
)
}
}

@ -0,0 +1,59 @@
/*
* 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.activities.habits.show.views
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.AdapterView
import android.widget.LinearLayout
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardViewModel
import org.isoron.uhabits.databinding.ShowHabitScoreBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
class ScoreCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private var binding = ShowHabitScoreBinding.inflate(LayoutInflater.from(context), this)
var onSpinnerPosition: (position: Int) -> Unit = {}
fun update(data: ScoreCardViewModel) {
val androidColor = data.color.toThemedAndroidColor(context)
binding.title.setTextColor(androidColor)
binding.spinner.setSelection(data.spinnerPosition)
binding.scoreView.setScores(data.scores)
binding.scoreView.reset()
binding.scoreView.setBucketSize(data.bucketSize)
binding.scoreView.setColor(androidColor)
binding.spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
onSpinnerPosition(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}
}

@ -22,19 +22,10 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import kotlinx.coroutines.Dispatchers import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCardViewModel
import kotlinx.coroutines.invoke
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Streak
import org.isoron.uhabits.databinding.ShowHabitStreakBinding import org.isoron.uhabits.databinding.ShowHabitStreakBinding
import org.isoron.uhabits.utils.toThemedAndroidColor import org.isoron.uhabits.utils.toThemedAndroidColor
data class StreakCardViewModel(
val color: PaletteColor,
val bestStreaks: List<Streak>
)
class StreakCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { class StreakCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private val binding = ShowHabitStreakBinding.inflate(LayoutInflater.from(context), this) private val binding = ShowHabitStreakBinding.inflate(LayoutInflater.from(context), this)
fun update(data: StreakCardViewModel) { fun update(data: StreakCardViewModel) {
@ -45,12 +36,3 @@ class StreakCardView(context: Context, attrs: AttributeSet) : LinearLayout(conte
postInvalidate() postInvalidate()
} }
} }
class StreakCartPresenter(val habit: Habit) {
suspend fun present(): StreakCardViewModel = Dispatchers.IO {
return@IO StreakCardViewModel(
color = habit.color,
bestStreaks = habit.streaks.getBest(10),
)
}
}

@ -28,23 +28,13 @@ import android.widget.LinearLayout
import org.isoron.uhabits.R import org.isoron.uhabits.R
import org.isoron.uhabits.activities.habits.list.views.toShortString import org.isoron.uhabits.activities.habits.list.views.toShortString
import org.isoron.uhabits.core.models.Frequency import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardViewModel
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding import org.isoron.uhabits.databinding.ShowHabitSubtitleBinding
import org.isoron.uhabits.utils.InterfaceUtils import org.isoron.uhabits.utils.InterfaceUtils
import org.isoron.uhabits.utils.formatTime import org.isoron.uhabits.utils.formatTime
import org.isoron.uhabits.utils.toThemedAndroidColor import org.isoron.uhabits.utils.toThemedAndroidColor
import java.util.Locale import java.util.Locale
data class SubtitleCardViewModel(
val color: PaletteColor,
val frequencyText: String,
val isNumerical: Boolean,
val question: String,
val reminderText: String,
val targetText: String,
)
class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private val binding = ShowHabitSubtitleBinding.inflate(LayoutInflater.from(context), this) private val binding = ShowHabitSubtitleBinding.inflate(LayoutInflater.from(context), this)
@ -56,13 +46,19 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
binding.reminderIcon.typeface = fontAwesome binding.reminderIcon.typeface = fontAwesome
} }
@SuppressLint("SetTextI18n")
fun update(data: SubtitleCardViewModel) { fun update(data: SubtitleCardViewModel) {
val color = data.color.toThemedAndroidColor(context) val color = data.color.toThemedAndroidColor(context)
binding.frequencyLabel.text = data.frequencyText val reminder = data.reminder
binding.frequencyLabel.text = data.frequency.format(resources)
binding.questionLabel.setTextColor(color) binding.questionLabel.setTextColor(color)
binding.questionLabel.text = data.question binding.questionLabel.text = data.question
binding.reminderLabel.text = data.reminderText binding.reminderLabel.text = if (reminder != null) {
binding.targetText.text = data.targetText formatTime(context, reminder.hour, reminder.minute)
} else {
resources.getString(R.string.reminder_off)
}
binding.targetText.text = "${data.targetValue.toShortString()} ${data.unit}"
binding.questionLabel.visibility = View.VISIBLE binding.questionLabel.visibility = View.VISIBLE
binding.targetIcon.visibility = View.VISIBLE binding.targetIcon.visibility = View.VISIBLE
@ -77,32 +73,9 @@ class SubtitleCardView(context: Context, attrs: AttributeSet) : LinearLayout(con
postInvalidate() postInvalidate()
} }
}
class SubtitleCardPresenter(
val habit: Habit,
val context: Context,
) {
val resources: Resources = context.resources
fun present(): SubtitleCardViewModel {
val reminderText = if (habit.hasReminder()) {
formatTime(context, habit.reminder!!.hour, habit.reminder!!.minute)!!
} else {
resources.getString(R.string.reminder_off)
}
return SubtitleCardViewModel(
color = habit.color,
frequencyText = habit.frequency.format(),
isNumerical = habit.isNumerical,
question = habit.question,
reminderText = reminderText,
targetText = "${habit.targetValue.toShortString()} ${habit.unit}",
)
}
@SuppressLint("StringFormatMatches") @SuppressLint("StringFormatMatches")
private fun Frequency.format(): String { private fun Frequency.format(resources: Resources): String {
val num = this.numerator val num = this.numerator
val den = this.denominator val den = this.denominator
if (num == den) { if (num == den) {

@ -1,110 +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.activities.habits.show.views
import android.content.Context
import android.content.res.Resources
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import org.isoron.uhabits.R
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.databinding.ShowHabitTargetBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
import java.util.ArrayList
import java.util.Calendar
data class TargetCardViewModel(
val color: PaletteColor,
val values: List<Double> = listOf(),
val targets: List<Double> = listOf(),
val labels: List<String> = listOf(),
)
class TargetCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private val binding = ShowHabitTargetBinding.inflate(LayoutInflater.from(context), this)
fun update(data: TargetCardViewModel) {
val androidColor = data.color.toThemedAndroidColor(context)
binding.targetChart.setValues(data.values)
binding.targetChart.setTargets(data.targets)
binding.targetChart.setLabels(data.labels)
binding.title.setTextColor(androidColor)
binding.targetChart.setColor(androidColor)
postInvalidate()
}
}
class TargetCardPresenter(
val habit: Habit,
val firstWeekday: Int,
val resources: Resources,
) {
suspend fun present(): TargetCardViewModel = Dispatchers.IO {
val today = DateUtils.getTodayWithOffset()
val entries = habit.computedEntries
val valueToday = entries.get(today).value / 1e3
val valueThisWeek = entries.getThisWeekValue(firstWeekday, habit.isNumerical) / 1e3
val valueThisMonth = entries.getThisMonthValue(habit.isNumerical) / 1e3
val valueThisQuarter = entries.getThisQuarterValue(habit.isNumerical) / 1e3
val valueThisYear = entries.getThisYearValue(habit.isNumerical) / 1e3
val cal = DateUtils.getStartOfTodayCalendarWithOffset()
val daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH)
val daysInQuarter = 91
val daysInYear = cal.getActualMaximum(Calendar.DAY_OF_YEAR)
val targetToday = habit.targetValue / habit.frequency.denominator
val targetThisWeek = targetToday * 7
val targetThisMonth = targetToday * daysInMonth
val targetThisQuarter = targetToday * daysInQuarter
val targetThisYear = targetToday * daysInYear
val values = ArrayList<Double>()
if (habit.frequency.denominator <= 1) values.add(valueToday)
if (habit.frequency.denominator <= 7) values.add(valueThisWeek)
values.add(valueThisMonth)
values.add(valueThisQuarter)
values.add(valueThisYear)
val targets = ArrayList<Double>()
if (habit.frequency.denominator <= 1) targets.add(targetToday)
if (habit.frequency.denominator <= 7) targets.add(targetThisWeek)
targets.add(targetThisMonth)
targets.add(targetThisQuarter)
targets.add(targetThisYear)
val labels = ArrayList<String>()
if (habit.frequency.denominator <= 1) labels.add(resources.getString(R.string.today))
if (habit.frequency.denominator <= 7) labels.add(resources.getString(R.string.week))
labels.add(resources.getString(R.string.month))
labels.add(resources.getString(R.string.quarter))
labels.add(resources.getString(R.string.year))
return@IO TargetCardViewModel(
color = habit.color,
values = values,
labels = labels,
targets = targets,
)
}
}

@ -0,0 +1,52 @@
/*
* 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.activities.habits.show.views
import android.content.Context
import android.content.res.Resources
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import org.isoron.uhabits.R
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardViewModel
import org.isoron.uhabits.databinding.ShowHabitTargetBinding
import org.isoron.uhabits.utils.toThemedAndroidColor
class TargetCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private val binding = ShowHabitTargetBinding.inflate(LayoutInflater.from(context), this)
fun update(data: TargetCardViewModel) {
val androidColor = data.color.toThemedAndroidColor(context)
binding.targetChart.setValues(data.values)
binding.targetChart.setTargets(data.targets)
binding.targetChart.setLabels(data.intervals.map { intervalToLabel(resources, it) })
binding.title.setTextColor(androidColor)
binding.targetChart.setColor(androidColor)
postInvalidate()
}
companion object {
fun intervalToLabel(resources: Resources, interval: Int) = when (interval) {
1 -> resources.getString(R.string.today)
7 -> resources.getString(R.string.week)
30 -> resources.getString(R.string.month)
91 -> resources.getString(R.string.quarter)
else -> resources.getString(R.string.year)
}
}
}

@ -19,9 +19,11 @@
package org.isoron.uhabits.inject; package org.isoron.uhabits.inject;
import android.content.*;
import dagger.*; import android.content.Context;
import dagger.Module;
import dagger.Provides;
@Module @Module
public class ActivityContextModule public class ActivityContextModule

@ -19,9 +19,10 @@
package org.isoron.uhabits.inject; package org.isoron.uhabits.inject;
import android.content.*; import android.content.Context;
import dagger.*; import dagger.Module;
import dagger.Provides;
@Module @Module
public class AppContextModule public class AppContextModule

@ -26,7 +26,8 @@ import org.isoron.uhabits.core.tasks.*;
import java.util.*; import java.util.*;
import dagger.*; import dagger.Module;
import dagger.Provides;
@Module @Module
public class AndroidTaskRunner implements TaskRunner public class AndroidTaskRunner implements TaskRunner

@ -21,10 +21,7 @@ package org.isoron.uhabits.tasks;
import android.content.*; import android.content.*;
import androidx.annotation.NonNull; import androidx.annotation.*;
import androidx.annotation.Nullable;
import com.google.auto.factory.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.core.tasks.*; import org.isoron.uhabits.core.tasks.*;
@ -33,7 +30,6 @@ import org.isoron.uhabits.utils.*;
import java.io.*; import java.io.*;
@AutoFactory(allowSubclasses = true)
public class ExportDBTask implements Task public class ExportDBTask implements Task
{ {
private String filename; private String filename;
@ -46,8 +42,8 @@ public class ExportDBTask implements Task
@NonNull @NonNull
private final Listener listener; private final Listener listener;
public ExportDBTask(@Provided @AppContext @NonNull Context context, public ExportDBTask(@AppContext @NonNull Context context,
@Provided @NonNull AndroidDirFinder system, @NonNull AndroidDirFinder system,
@NonNull Listener listener) @NonNull Listener listener)
{ {
this.system = system; this.system = system;

@ -0,0 +1,33 @@
/*
* Copyright (C) 2016-2020 Á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.tasks
import android.content.Context
import org.isoron.uhabits.AndroidDirFinder
import org.isoron.uhabits.inject.AppContext
import javax.inject.Inject
class ExportDBTaskFactory
@Inject constructor(
@AppContext private val context: Context,
private val system: AndroidDirFinder,
) {
fun create(listener: ExportDBTask.Listener) = ExportDBTask(context, system, listener)
}

@ -23,8 +23,6 @@ import android.util.*;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.google.auto.factory.*;
import org.isoron.uhabits.core.io.*; import org.isoron.uhabits.core.io.*;
import org.isoron.uhabits.core.models.ModelFactory; import org.isoron.uhabits.core.models.ModelFactory;
import org.isoron.uhabits.core.models.sqlite.SQLModelFactory; import org.isoron.uhabits.core.models.sqlite.SQLModelFactory;
@ -32,7 +30,6 @@ import org.isoron.uhabits.core.tasks.*;
import java.io.*; import java.io.*;
@AutoFactory(allowSubclasses = true)
public class ImportDataTask implements Task public class ImportDataTask implements Task
{ {
public static final int FAILED = 3; public static final int FAILED = 3;
@ -53,8 +50,8 @@ public class ImportDataTask implements Task
@NonNull @NonNull
private final Listener listener; private final Listener listener;
public ImportDataTask(@Provided @NonNull GenericImporter importer, public ImportDataTask(@NonNull GenericImporter importer,
@Provided @NonNull ModelFactory modelFactory, @NonNull ModelFactory modelFactory,
@NonNull File file, @NonNull File file,
@NonNull Listener listener) @NonNull Listener listener)
{ {

@ -0,0 +1,34 @@
/*
* Copyright (C) 2016-2020 Á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.tasks
import org.isoron.uhabits.core.io.GenericImporter
import org.isoron.uhabits.core.models.ModelFactory
import java.io.File
import javax.inject.Inject
class ImportDataTaskFactory
@Inject constructor(
private val importer: GenericImporter,
private val modelFactory: ModelFactory,
) {
fun create(file: File, listener: ImportDataTask.Listener) =
ImportDataTask(importer, modelFactory, file, listener)
}

@ -24,6 +24,7 @@ import android.content.Context
import android.view.View import android.view.View
import org.isoron.uhabits.activities.common.views.HistoryChart import org.isoron.uhabits.activities.common.views.HistoryChart
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter
import org.isoron.uhabits.utils.toThemedAndroidColor import org.isoron.uhabits.utils.toThemedAndroidColor
import org.isoron.uhabits.widgets.views.GraphWidgetView import org.isoron.uhabits.widgets.views.GraphWidgetView
@ -42,12 +43,17 @@ class HistoryWidget(
val widgetView = view as GraphWidgetView val widgetView = view as GraphWidgetView
widgetView.setBackgroundAlpha(preferedBackgroundAlpha) widgetView.setBackgroundAlpha(preferedBackgroundAlpha)
if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f) if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f)
val model = HistoryCardPresenter().present(
habit = habit,
isSkipEnabled = prefs.isSkipEnabled,
firstWeekday = prefs.firstWeekday,
)
(widgetView.dataView as HistoryChart).apply { (widgetView.dataView as HistoryChart).apply {
setFirstWeekday(firstWeekday) setFirstWeekday(model.firstWeekday)
setSkipEnabled(prefs.isSkipEnabled) setSkipEnabled(model.isSkipEnabled)
setColor(habit.color.toThemedAndroidColor(context)) setColor(model.color.toThemedAndroidColor(context))
setEntries(habit.computedEntries.getAllValues()) setEntries(model.entries)
setNumerical(habit.isNumerical) setNumerical(model.isNumerical)
} }
} }

@ -22,8 +22,8 @@ package org.isoron.uhabits.widgets
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import org.isoron.uhabits.activities.common.views.ScoreChart import org.isoron.uhabits.activities.common.views.ScoreChart
import org.isoron.uhabits.activities.habits.show.views.ScoreCardPresenter
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter
import org.isoron.uhabits.utils.toThemedAndroidColor import org.isoron.uhabits.utils.toThemedAndroidColor
import org.isoron.uhabits.widgets.views.GraphWidgetView import org.isoron.uhabits.widgets.views.GraphWidgetView
@ -37,8 +37,12 @@ class ScoreWidget(
pendingIntentFactory.showHabit(habit) pendingIntentFactory.showHabit(habit)
override fun refreshData(view: View) { override fun refreshData(view: View) {
val presenter = ScoreCardPresenter(habit, prefs.firstWeekday) val presenter = ScoreCardPresenter()
val viewModel = presenter.present(prefs.scoreCardSpinnerPosition) val viewModel = presenter.present(
habit = habit,
firstWeekday = prefs.firstWeekday,
spinnerPosition = prefs.scoreCardSpinnerPosition
)
val widgetView = view as GraphWidgetView val widgetView = view as GraphWidgetView
widgetView.setBackgroundAlpha(preferedBackgroundAlpha) widgetView.setBackgroundAlpha(preferedBackgroundAlpha)
if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f) if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f)

@ -25,8 +25,9 @@ import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.isoron.uhabits.activities.common.views.TargetChart import org.isoron.uhabits.activities.common.views.TargetChart
import org.isoron.uhabits.activities.habits.show.views.TargetCardPresenter import org.isoron.uhabits.activities.habits.show.views.TargetCardView.Companion.intervalToLabel
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardPresenter
import org.isoron.uhabits.utils.toThemedAndroidColor import org.isoron.uhabits.utils.toThemedAndroidColor
import org.isoron.uhabits.widgets.views.GraphWidgetView import org.isoron.uhabits.widgets.views.GraphWidgetView
@ -44,11 +45,11 @@ class TargetWidget(
widgetView.setBackgroundAlpha(preferedBackgroundAlpha) widgetView.setBackgroundAlpha(preferedBackgroundAlpha)
if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f) if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f)
val chart = (widgetView.dataView as TargetChart) val chart = (widgetView.dataView as TargetChart)
val presenter = TargetCardPresenter(habit, prefs.firstWeekday, context.resources) val presenter = TargetCardPresenter()
val data = presenter.present() val data = presenter.present(habit, prefs.firstWeekday)
chart.setColor(data.color.toThemedAndroidColor(context)) chart.setColor(data.color.toThemedAndroidColor(context))
chart.setTargets(data.targets) chart.setTargets(data.targets)
chart.setLabels(data.labels) chart.setLabels(data.intervals.map { intervalToLabel(context.resources, it) })
chart.setValues(data.values) chart.setValues(data.values)
} }

@ -62,17 +62,17 @@
style="@style/Card" style="@style/Card"
android:paddingTop="12dp"/> android:paddingTop="12dp"/>
<org.isoron.uhabits.activities.habits.show.views.ScoreCard <org.isoron.uhabits.activities.habits.show.views.ScoreCardView
android:id="@+id/scoreCard" android:id="@+id/scoreCard"
style="@style/Card" style="@style/Card"
android:gravity="center"/> android:gravity="center"/>
<org.isoron.uhabits.activities.habits.show.views.BarCard <org.isoron.uhabits.activities.habits.show.views.BarCardView
android:id="@+id/barCard" android:id="@+id/barCard"
style="@style/Card" style="@style/Card"
android:gravity="center"/> android:gravity="center"/>
<org.isoron.uhabits.activities.habits.show.views.HistoryCard <org.isoron.uhabits.activities.habits.show.views.HistoryCardView
android:id="@+id/historyCard" android:id="@+id/historyCard"
style="@style/Card" style="@style/Card"
android:gravity="center" android:gravity="center"
@ -82,7 +82,7 @@
android:id="@+id/streakCard" android:id="@+id/streakCard"
style="@style/Card"/> style="@style/Card"/>
<org.isoron.uhabits.activities.habits.show.views.FrequencyCard <org.isoron.uhabits.activities.habits.show.views.FrequencyCardView
android:id="@+id/frequencyCard" android:id="@+id/frequencyCard"
style="@style/Card"/> style="@style/Card"/>

@ -3,9 +3,7 @@ apply plugin: 'java'
apply plugin: 'kotlin' apply plugin: 'kotlin'
dependencies { dependencies {
annotationProcessor "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
annotationProcessor "com.google.dagger:dagger:$DAGGER_VERSION" annotationProcessor "com.google.dagger:dagger:$DAGGER_VERSION"
compileOnly "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
compileOnly "com.google.dagger:dagger:$DAGGER_VERSION" compileOnly "com.google.dagger:dagger:$DAGGER_VERSION"
compileOnly 'javax.annotation:jsr250-api:1.0' compileOnly 'javax.annotation:jsr250-api:1.0'
compileOnly 'org.jetbrains:annotations:18.0.0' compileOnly 'org.jetbrains:annotations:18.0.0'

@ -18,6 +18,7 @@
*/ */
package org.isoron.uhabits.core.io package org.isoron.uhabits.core.io
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.EntryList import org.isoron.uhabits.core.models.EntryList
import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList import org.isoron.uhabits.core.models.HabitList
@ -148,10 +149,10 @@ class HabitsCSVExporter(
val timeframe = getTimeframe() val timeframe = getTimeframe()
val oldest = timeframe[0] val oldest = timeframe[0]
val newest = DateUtils.getToday() val newest = DateUtils.getToday()
val checkmarks: MutableList<IntArray> = 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) {
checkmarks.add(habit.computedEntries.getValues(oldest, newest)) checkmarks.add(ArrayList(habit.computedEntries.getByInterval(oldest, newest)))
scores.add(ArrayList(habit.scores.getByInterval(oldest, newest))) scores.add(ArrayList(habit.scores.getByInterval(oldest, newest)))
} }

@ -28,6 +28,7 @@ import java.util.ArrayList
import java.util.Calendar import java.util.Calendar
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
import kotlin.collections.set import kotlin.collections.set
import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ThreadSafe @ThreadSafe
@ -79,45 +80,6 @@ open class EntryList {
return entriesByTimestamp.values.sortedBy { it.timestamp }.reversed() return entriesByTimestamp.values.sortedBy { it.timestamp }.reversed()
} }
/**
* Truncates the timestamps of all known entries, then aggregates their values. This function
* is used to generate bar plots where each bar shows the number of repetitions in a given week,
* month or year.
*
* For boolean habits, the value of the aggregated entry equals to the number of YES_MANUAL
* entries. For numerical habits, the value is the total sum. The field [firstWeekday] is only
* relevant when grouping by week.
*/
@Synchronized
open fun groupBy(
original: List<Entry>,
field: DateUtils.TruncateField,
firstWeekday: Int,
isNumerical: Boolean,
): List<Entry> {
val truncated = original.map {
Entry(it.timestamp.truncate(field, firstWeekday), it.value)
}
val timestamps = mutableListOf<Timestamp>()
val values = mutableListOf<Int>()
for (i in truncated.indices) {
if (i == 0 || timestamps.last() != truncated[i].timestamp) {
timestamps.add(truncated[i].timestamp)
values.add(0)
}
if (isNumerical) {
if (truncated[i].value > 0) {
values[values.lastIndex] += truncated[i].value
}
} else {
if (truncated[i].value == YES_MANUAL) {
values[values.lastIndex] += 1000
}
}
}
return timestamps.indices.map { Entry(timestamps[it], values[it]) }
}
/** /**
* Replaces all entries in this list by entries computed automatically from another list. * Replaces all entries in this list by entries computed automatically from another list.
* *
@ -189,88 +151,6 @@ open class EntryList {
return map return map
} }
/**
* Returns the values of the entries that fall inside a certain interval of time. The values
* are returned in an array containing one integer value for each day of the interval. The
* first entry corresponds to the most recent day in the interval. Each subsequent entry
* corresponds to one day older than the previous entry. The boundaries of the time interval
* are included.
*/
@Deprecated("")
@Synchronized
fun getValues(from: Timestamp, to: Timestamp): IntArray {
if (from.isNewerThan(to)) throw IllegalArgumentException()
val nDays = from.daysUntil(to) + 1
val result = IntArray(nDays) { UNKNOWN }
getKnown().filter { entry ->
!entry.timestamp.isNewerThan(to) && !entry.timestamp.isOlderThan(from)
}.forEach { entry ->
val offset = entry.timestamp.daysUntil(to)
result[offset] = entry.value
}
return result
}
@Deprecated("")
@Synchronized
fun getAllValues(): IntArray {
val entries = getKnown()
if (entries.isEmpty()) return IntArray(0)
var (fromTimestamp, _) = entries.last()
val toTimestamp = DateUtils.getTodayWithOffset()
if (fromTimestamp.isNewerThan(toTimestamp)) fromTimestamp = toTimestamp
return getValues(fromTimestamp, toTimestamp)
}
@Deprecated("")
@Synchronized
open fun getThisWeekValue(firstWeekday: Int, isNumerical: Boolean): Int {
return getThisIntervalValue(
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
firstWeekday = firstWeekday,
isNumerical = isNumerical
)
}
@Deprecated("")
@Synchronized
open fun getThisMonthValue(isNumerical: Boolean): Int {
return getThisIntervalValue(
truncateField = DateUtils.TruncateField.MONTH,
firstWeekday = Calendar.SATURDAY,
isNumerical = isNumerical
)
}
@Deprecated("")
@Synchronized
open fun getThisQuarterValue(isNumerical: Boolean): Int {
return getThisIntervalValue(
truncateField = DateUtils.TruncateField.QUARTER,
firstWeekday = Calendar.SATURDAY,
isNumerical = isNumerical
)
}
@Deprecated("")
@Synchronized
open fun getThisYearValue(isNumerical: Boolean): Int {
return getThisIntervalValue(
truncateField = DateUtils.TruncateField.YEAR,
firstWeekday = Calendar.SATURDAY,
isNumerical = isNumerical
)
}
private fun getThisIntervalValue(
truncateField: DateUtils.TruncateField,
firstWeekday: Int,
isNumerical: Boolean,
): Int {
val groups: List<Entry> = groupBy(getKnown(), truncateField, firstWeekday, isNumerical)
return if (groups.isEmpty()) 0 else groups[0].value
}
data class Interval(val begin: Timestamp, val center: Timestamp, val end: Timestamp) { data class Interval(val begin: Timestamp, val center: Timestamp, val end: Timestamp) {
val length: Int val length: Int
get() = begin.daysUntil(end) + 1 get() = begin.daysUntil(end) + 1
@ -377,3 +257,41 @@ open class EntryList {
} }
} }
} }
/**
* Given a list of entries, truncates the timestamp of each entry (according to the field given),
* groups the entries according to this truncated timestamp, then creates a new entry (t,v) for
* each group, where t is the truncated timestamp and v is the sum of the values of all entries in
* the group.
*
* For numerical habits, non-positive entry values are converted to zero. For boolean habits, each
* YES_MANUAL value is converted to 1000 and all other values are converted to zero.
*
* The returned list is sorted by timestamp, with the newest entry coming first and the oldest entry
* coming last. If the original list has gaps in it (for example, weeks or months without any
* entries), then the list produced by this method will also have gaps.
*
* The argument [firstWeekday] is only relevant when truncating by week.
*/
fun List<Entry>.groupedSum(
truncateField: DateUtils.TruncateField,
firstWeekday: Int = Calendar.SATURDAY,
isNumerical: Boolean,
): List<Entry> {
return this.map { (timestamp, value) ->
if (isNumerical) {
Entry(timestamp, max(0, value))
} else {
Entry(timestamp, if (value == YES_MANUAL) 1000 else 0)
}
}.groupBy { entry ->
entry.timestamp.truncate(
truncateField,
firstWeekday,
)
}.entries.map { (timestamp, entries) ->
Entry(timestamp, entries.sumOf { it.value })
}.sortedBy { (timestamp, _) ->
- timestamp.unixTime
}
}

@ -21,9 +21,12 @@ package org.isoron.uhabits.core.models;
import org.apache.commons.lang3.builder.*; import org.apache.commons.lang3.builder.*;
import org.isoron.uhabits.core.utils.*; import org.isoron.uhabits.core.utils.*;
import org.jetbrains.annotations.*;
import java.util.*; import java.util.*;
import kotlin.*;
import static java.util.Calendar.*; import static java.util.Calendar.*;
public final class Timestamp implements Comparable<Timestamp> public final class Timestamp implements Comparable<Timestamp>

@ -26,7 +26,6 @@ import org.isoron.uhabits.core.models.EntryList
import org.isoron.uhabits.core.models.Frequency import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.models.sqlite.records.EntryRecord import org.isoron.uhabits.core.models.sqlite.records.EntryRecord
import org.isoron.uhabits.core.utils.DateUtils
class SQLiteEntryList(database: Database) : EntryList() { class SQLiteEntryList(database: Database) : EntryList() {
val repository = Repository(EntryRecord::class.java, database) val repository = Repository(EntryRecord::class.java, database)
@ -79,16 +78,6 @@ class SQLiteEntryList(database: Database) : EntryList() {
return super.getKnown() return super.getKnown()
} }
override fun groupBy(
original: List<Entry>,
field: DateUtils.TruncateField,
firstWeekday: Int,
isNumerical: Boolean
): List<Entry> {
loadRecords()
return super.groupBy(original, field, firstWeekday, isNumerical)
}
override fun recomputeFrom(originalEntries: EntryList, frequency: Frequency, isNumerical: Boolean) { override fun recomputeFrom(originalEntries: EntryList, frequency: Frequency, isNumerical: Boolean) {
throw UnsupportedOperationException() throw UnsupportedOperationException()
} }

@ -21,15 +21,12 @@ package org.isoron.uhabits.core.tasks;
import androidx.annotation.*; import androidx.annotation.*;
import com.google.auto.factory.*;
import org.isoron.uhabits.core.io.*; import org.isoron.uhabits.core.io.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import java.io.*; import java.io.*;
import java.util.*; import java.util.*;
@AutoFactory(allowSubclasses = true)
public class ExportCSVTask implements Task public class ExportCSVTask implements Task
{ {
private String archiveFilename; private String archiveFilename;
@ -45,7 +42,7 @@ public class ExportCSVTask implements Task
@NonNull @NonNull
private final HabitList habitList; private final HabitList habitList;
public ExportCSVTask(@Provided @NonNull HabitList habitList, public ExportCSVTask(@NonNull HabitList habitList,
@NonNull List<Habit> selectedHabits, @NonNull List<Habit> selectedHabits,
@NonNull File outputDir, @NonNull File outputDir,
@NonNull Listener listener) @NonNull Listener listener)

@ -0,0 +1,36 @@
/*
* Copyright (C) 2016-2020 Á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.tasks
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import java.io.File
import javax.inject.Inject
class ExportCSVTaskFactory
@Inject constructor(
val habitList: HabitList
) {
fun create(
selectedHabits: List<Habit>,
outputDir: File,
listener: ExportCSVTask.Listener,
) = ExportCSVTask(habitList, selectedHabits, outputDir, listener)
}

@ -21,6 +21,7 @@ package org.isoron.uhabits.core.ui.screens.habits.list;
import androidx.annotation.*; import androidx.annotation.*;
import org.apache.commons.lang3.*;
import org.isoron.uhabits.core.*; import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.commands.*; import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
@ -363,9 +364,12 @@ public class HabitCardListCache implements CommandRunner.Listener
if (targetId != null && !targetId.equals(id)) continue; if (targetId != null && !targetId.equals(id)) continue;
newData.scores.put(id, habit.getScores().get(today).getValue()); newData.scores.put(id, habit.getScores().get(today).getValue());
newData.checkmarks.put( Integer[] entries = habit.getComputedEntries()
id, .getByInterval(dateFrom, today)
habit.getComputedEntries().getValues(dateFrom, today)); .stream()
.map(Entry::getValue)
.toArray(Integer[]::new);
newData.checkmarks.put(id, ArrayUtils.toPrimitive(entries));
runner.publishProgress(this, position); runner.publishProgress(this, position);
} }

@ -21,8 +21,6 @@ package org.isoron.uhabits.core.ui.screens.habits.list;
import androidx.annotation.*; import androidx.annotation.*;
import com.google.auto.factory.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*; import org.isoron.uhabits.core.preferences.*;
import org.isoron.uhabits.core.utils.*; import org.isoron.uhabits.core.utils.*;
@ -31,7 +29,6 @@ import org.isoron.uhabits.core.utils.*;
* Provides a list of hints to be shown at the application startup, and takes * Provides a list of hints to be shown at the application startup, and takes
* care of deciding when a new hint should be shown. * care of deciding when a new hint should be shown.
*/ */
@AutoFactory
public class HintList public class HintList
{ {
private final Preferences prefs; private final Preferences prefs;
@ -44,7 +41,7 @@ public class HintList
* *
* @param hints initial list of hints * @param hints initial list of hints
*/ */
public HintList(@Provided @NonNull Preferences prefs, public HintList(@NonNull Preferences prefs,
@NonNull String hints[]) @NonNull String hints[])
{ {
this.prefs = prefs; this.prefs = prefs;

@ -0,0 +1,30 @@
/*
* Copyright (C) 2016-2020 Á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.ui.screens.habits.list
import org.isoron.uhabits.core.preferences.Preferences
import javax.inject.Inject
class HintListFactory
@Inject constructor(
val preferences: Preferences,
) {
fun create(hints: Array<String>) = HintList(preferences, hints)
}

@ -0,0 +1,106 @@
/*
* Copyright (C) 2016-2020 Á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.ui.screens.habits.show
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCartPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardViewModel
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardPresenter
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardViewModel
data class ShowHabitViewModel(
val title: String = "",
val isNumerical: Boolean = false,
val color: PaletteColor = PaletteColor(1),
val subtitle: SubtitleCardViewModel,
val overview: OverviewCardViewModel,
val notes: NotesCardViewModel,
val target: TargetCardViewModel,
val streaks: StreakCardViewModel,
val scores: ScoreCardViewModel,
val frequency: FrequencyCardViewModel,
val history: HistoryCardViewModel,
val bar: BarCardViewModel,
)
class ShowHabitPresenter {
fun present(
habit: Habit,
preferences: Preferences,
): ShowHabitViewModel {
return ShowHabitViewModel(
title = habit.name,
color = habit.color,
isNumerical = habit.isNumerical,
subtitle = SubtitleCardPresenter().present(
habit = habit,
),
overview = OverviewCardPresenter().present(
habit = habit,
),
notes = NotesCardPresenter().present(
habit = habit,
),
target = TargetCardPresenter().present(
habit = habit,
firstWeekday = preferences.firstWeekday,
),
streaks = StreakCartPresenter().present(
habit = habit,
),
scores = ScoreCardPresenter().present(
spinnerPosition = preferences.scoreCardSpinnerPosition,
habit = habit,
firstWeekday = preferences.firstWeekday,
),
frequency = FrequencyCardPresenter().present(
habit = habit,
firstWeekday = preferences.firstWeekday,
),
history = HistoryCardPresenter().present(
habit = habit,
firstWeekday = preferences.firstWeekday,
isSkipEnabled = preferences.isSkipEnabled,
),
bar = BarCardPresenter().present(
habit = habit,
firstWeekday = preferences.firstWeekday,
boolSpinnerPosition = preferences.barCardBoolSpinnerPosition,
numericalSpinnerPosition = preferences.barCardNumericalSpinnerPosition,
),
)
}
}

@ -0,0 +1,68 @@
/*
* Copyright (C) 2016-2020 Á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.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.groupedSum
import org.isoron.uhabits.core.utils.DateUtils
data class BarCardViewModel(
val boolSpinnerPosition: Int,
val bucketSize: Int,
val color: PaletteColor,
val entries: List<Entry>,
val isNumerical: Boolean,
val numericalSpinnerPosition: Int,
)
class BarCardPresenter {
val numericalBucketSizes = intArrayOf(1, 7, 31, 92, 365)
val boolBucketSizes = intArrayOf(7, 31, 92, 365)
fun present(
habit: Habit,
firstWeekday: Int,
numericalSpinnerPosition: Int,
boolSpinnerPosition: Int,
): BarCardViewModel {
val bucketSize = if (habit.isNumerical) {
numericalBucketSizes[numericalSpinnerPosition]
} else {
boolBucketSizes[boolSpinnerPosition]
}
val today = DateUtils.getToday()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries = habit.computedEntries.getByInterval(oldest, today).groupedSum(
truncateField = ScoreCardPresenter.getTruncateField(bucketSize),
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical,
)
return BarCardViewModel(
entries = entries,
bucketSize = bucketSize,
color = habit.color,
isNumerical = habit.isNumerical,
numericalSpinnerPosition = numericalSpinnerPosition,
boolSpinnerPosition = boolSpinnerPosition,
)
}
}

@ -0,0 +1,44 @@
/*
* Copyright (C) 2016-2020 Á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.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Timestamp
import java.util.HashMap
data class FrequencyCardViewModel(
val color: PaletteColor,
val firstWeekday: Int,
val frequency: HashMap<Timestamp, Array<Int>>,
)
class FrequencyCardPresenter {
fun present(
habit: Habit,
firstWeekday: Int,
) = FrequencyCardViewModel(
color = habit.color,
frequency = habit.originalEntries.computeWeekdayFrequency(
isNumerical = habit.isNumerical
),
firstWeekday = firstWeekday,
)
}

@ -0,0 +1,53 @@
/*
* Copyright (C) 2016-2020 Á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.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.utils.DateUtils
data class HistoryCardViewModel(
val color: PaletteColor,
val entries: IntArray,
val firstWeekday: Int,
val isNumerical: Boolean,
val isSkipEnabled: Boolean,
)
class HistoryCardPresenter {
fun present(
habit: Habit,
firstWeekday: Int,
isSkipEnabled: Boolean,
): HistoryCardViewModel {
val today = DateUtils.getTodayWithOffset()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries =
habit.computedEntries.getByInterval(oldest, today).map { it.value }.toIntArray()
return HistoryCardViewModel(
entries = entries,
color = habit.color,
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical,
isSkipEnabled = isSkipEnabled,
)
}
}

@ -0,0 +1,32 @@
/*
* Copyright (C) 2016-2020 Á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.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Habit
data class NotesCardViewModel(
val description: String,
)
class NotesCardPresenter {
fun present(habit: Habit) = NotesCardViewModel(
description = habit.description,
)
}

@ -0,0 +1,56 @@
/*
* Copyright (C) 2016-2020 Á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.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.utils.DateUtils
data class OverviewCardViewModel(
val color: PaletteColor,
val scoreMonthDiff: Float,
val scoreYearDiff: Float,
val scoreToday: Float,
val totalCount: Long,
)
class OverviewCardPresenter {
fun present(habit: Habit): OverviewCardViewModel {
val today = DateUtils.getTodayWithOffset()
val lastMonth = today.minus(30)
val lastYear = today.minus(365)
val scores = habit.scores
val scoreToday = scores.get(today).value.toFloat()
val scoreLastMonth = scores.get(lastMonth).value.toFloat()
val scoreLastYear = scores.get(lastYear).value.toFloat()
val totalCount = habit.originalEntries.getKnown()
.filter { it.value == Entry.YES_MANUAL }
.count()
.toLong()
return OverviewCardViewModel(
color = habit.color,
scoreToday = scoreToday,
scoreMonthDiff = scoreToday - scoreLastMonth,
scoreYearDiff = scoreToday - scoreLastYear,
totalCount = totalCount,
)
}
}

@ -0,0 +1,79 @@
/*
* Copyright (C) 2016-2020 Á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.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Score
import org.isoron.uhabits.core.utils.DateUtils
data class ScoreCardViewModel(
val scores: List<Score>,
val bucketSize: Int,
val spinnerPosition: Int,
val color: PaletteColor,
)
class ScoreCardPresenter {
companion object {
val BUCKET_SIZES = intArrayOf(1, 7, 31, 92, 365)
fun getTruncateField(bucketSize: Int): DateUtils.TruncateField {
when (bucketSize) {
1 -> return DateUtils.TruncateField.DAY
7 -> return DateUtils.TruncateField.WEEK_NUMBER
31 -> return DateUtils.TruncateField.MONTH
92 -> return DateUtils.TruncateField.QUARTER
365 -> return DateUtils.TruncateField.YEAR
else -> return DateUtils.TruncateField.MONTH
}
}
}
fun present(
habit: Habit,
firstWeekday: Int,
spinnerPosition: Int,
): ScoreCardViewModel {
val bucketSize = BUCKET_SIZES[spinnerPosition]
val today = DateUtils.getTodayWithOffset()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val field = getTruncateField(bucketSize)
val scores = habit.scores.getByInterval(oldest, today).groupBy {
DateUtils.truncate(field, it.timestamp, firstWeekday)
}.map { (timestamp, scores) ->
Score(
timestamp,
scores.map {
it.value
}.average()
)
}.sortedBy {
it.timestamp
}.reversed()
return ScoreCardViewModel(
color = habit.color,
scores = scores,
bucketSize = bucketSize,
spinnerPosition = spinnerPosition,
)
}
}

@ -0,0 +1,38 @@
/*
* Copyright (C) 2016-2020 Á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.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Streak
data class StreakCardViewModel(
val color: PaletteColor,
val bestStreaks: List<Streak>
)
class StreakCartPresenter {
fun present(habit: Habit): StreakCardViewModel {
return StreakCardViewModel(
color = habit.color,
bestStreaks = habit.streaks.getBest(10),
)
}
}

@ -0,0 +1,49 @@
/*
* Copyright (C) 2016-2020 Á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.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Reminder
data class SubtitleCardViewModel(
val color: PaletteColor,
val frequency: Frequency,
val isNumerical: Boolean,
val question: String,
val reminder: Reminder?,
val targetValue: Double,
val unit: String,
)
class SubtitleCardPresenter {
fun present(
habit: Habit,
): SubtitleCardViewModel = SubtitleCardViewModel(
color = habit.color,
frequency = habit.frequency,
isNumerical = habit.isNumerical,
question = habit.question,
reminder = habit.reminder,
targetValue = habit.targetValue,
unit = habit.unit,
)
}

@ -0,0 +1,110 @@
/*
* Copyright (C) 2016-2020 Á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.ui.screens.habits.show.views
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.groupedSum
import org.isoron.uhabits.core.utils.DateUtils
import java.util.ArrayList
import java.util.Calendar
data class TargetCardViewModel(
val color: PaletteColor,
val values: List<Double> = listOf(),
val targets: List<Double> = listOf(),
val intervals: List<Int> = listOf(),
)
class TargetCardPresenter {
fun present(
habit: Habit,
firstWeekday: Int,
): TargetCardViewModel {
val today = DateUtils.getTodayWithOffset()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
val entries = habit.computedEntries.getByInterval(oldest, today)
val valueToday = entries.groupedSum(
truncateField = DateUtils.TruncateField.DAY,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val valueThisWeek = entries.groupedSum(
truncateField = DateUtils.TruncateField.WEEK_NUMBER,
firstWeekday = firstWeekday,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val valueThisMonth = entries.groupedSum(
truncateField = DateUtils.TruncateField.MONTH,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val valueThisQuarter = entries.groupedSum(
truncateField = DateUtils.TruncateField.QUARTER,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val valueThisYear = entries.groupedSum(
truncateField = DateUtils.TruncateField.YEAR,
isNumerical = habit.isNumerical
).firstOrNull()?.value ?: 0
val cal = DateUtils.getStartOfTodayCalendarWithOffset()
val daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH)
val daysInQuarter = 91
val daysInYear = cal.getActualMaximum(Calendar.DAY_OF_YEAR)
val targetToday = habit.targetValue / habit.frequency.denominator
val targetThisWeek = targetToday * 7
val targetThisMonth = targetToday * daysInMonth
val targetThisQuarter = targetToday * daysInQuarter
val targetThisYear = targetToday * daysInYear
val values = ArrayList<Double>()
if (habit.frequency.denominator <= 1) values.add(valueToday / 1e3)
if (habit.frequency.denominator <= 7) values.add(valueThisWeek / 1e3)
values.add(valueThisMonth / 1e3)
values.add(valueThisQuarter / 1e3)
values.add(valueThisYear / 1e3)
val targets = ArrayList<Double>()
if (habit.frequency.denominator <= 1) targets.add(targetToday)
if (habit.frequency.denominator <= 7) targets.add(targetThisWeek)
targets.add(targetThisMonth)
targets.add(targetThisQuarter)
targets.add(targetThisYear)
val intervals = ArrayList<Int>()
if (habit.frequency.denominator <= 1) intervals.add(1)
if (habit.frequency.denominator <= 7) intervals.add(7)
intervals.add(30)
intervals.add(91)
intervals.add(365)
return TargetCardViewModel(
color = habit.color,
values = values,
targets = targets,
intervals = intervals,
)
}
}

@ -81,13 +81,13 @@ public class WidgetBehavior
} }
public void onIncrement(@NotNull Habit habit, @NotNull Timestamp timestamp, int amount) { public void onIncrement(@NotNull Habit habit, @NotNull Timestamp timestamp, int amount) {
int currentValue = habit.getComputedEntries().getValues(timestamp, timestamp)[0]; int currentValue = habit.getComputedEntries().get(timestamp).getValue();
setValue(habit, timestamp, currentValue + amount); setValue(habit, timestamp, currentValue + amount);
notificationTray.cancel(habit); notificationTray.cancel(habit);
} }
public void onDecrement(@NotNull Habit habit, @NotNull Timestamp timestamp, int amount) { public void onDecrement(@NotNull Habit habit, @NotNull Timestamp timestamp, int amount) {
int currentValue = habit.getComputedEntries().getValues(timestamp, timestamp)[0]; int currentValue = habit.getComputedEntries().get(timestamp).getValue();
setValue(habit, timestamp, currentValue - amount); setValue(habit, timestamp, currentValue - amount);
notificationTray.cancel(habit); notificationTray.cancel(habit);
} }

@ -65,27 +65,6 @@ class EntryListTest {
assertEquals(Entry(today.minus(5), 20), actual[5]) assertEquals(Entry(today.minus(5), 20), actual[5])
} }
@Test
fun testGetValues() {
val entries = EntryList()
val today = DateUtils.getToday()
entries.add(Entry(today.minus(3), YES_MANUAL))
entries.add(Entry(today.minus(5), YES_MANUAL))
entries.add(Entry(today.minus(6), YES_MANUAL))
val expected = intArrayOf(
UNKNOWN, // 1
UNKNOWN, // 2
YES_MANUAL, // 3
UNKNOWN, // 4
YES_MANUAL, // 5
YES_MANUAL, // 6
UNKNOWN, // 7
)
assertThat(entries.getValues(today.minus(7), today.minus(1)), equalTo(expected))
}
@Test @Test
fun testComputeBoolean() { fun testComputeBoolean() {
val today = DateUtils.getToday() val today = DateUtils.getToday()
@ -163,10 +142,8 @@ class EntryListTest {
entries.add(Entry(reference.minus(offsets[it]), values[it])) entries.add(Entry(reference.minus(offsets[it]), values[it]))
} }
val byMonth = entries.groupBy( val byMonth = entries.getKnown().groupedSum(
original = entries.getKnown(), truncateField = DateUtils.TruncateField.MONTH,
field = DateUtils.TruncateField.MONTH,
firstWeekday = Calendar.SATURDAY,
isNumerical = true, isNumerical = true,
) )
assertThat(byMonth.size, equalTo(17)) assertThat(byMonth.size, equalTo(17))
@ -174,10 +151,8 @@ class EntryListTest {
assertThat(byMonth[6], equalTo(Entry(Timestamp.from(2013, Calendar.DECEMBER, 1), 1988))) assertThat(byMonth[6], equalTo(Entry(Timestamp.from(2013, Calendar.DECEMBER, 1), 1988)))
assertThat(byMonth[12], equalTo(Entry(Timestamp.from(2013, Calendar.MAY, 1), 1271))) assertThat(byMonth[12], equalTo(Entry(Timestamp.from(2013, Calendar.MAY, 1), 1271)))
val byQuarter = entries.groupBy( val byQuarter = entries.getKnown().groupedSum(
original = entries.getKnown(), truncateField = DateUtils.TruncateField.QUARTER,
field = DateUtils.TruncateField.QUARTER,
firstWeekday = Calendar.SATURDAY,
isNumerical = true, isNumerical = true,
) )
assertThat(byQuarter.size, equalTo(6)) assertThat(byQuarter.size, equalTo(6))
@ -185,10 +160,8 @@ class EntryListTest {
assertThat(byQuarter[3], equalTo(Entry(Timestamp.from(2013, Calendar.JULY, 1), 3838))) assertThat(byQuarter[3], equalTo(Entry(Timestamp.from(2013, Calendar.JULY, 1), 3838)))
assertThat(byQuarter[5], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 4975))) assertThat(byQuarter[5], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 4975)))
val byYear = entries.groupBy( val byYear = entries.getKnown().groupedSum(
original = entries.getKnown(), truncateField = DateUtils.TruncateField.YEAR,
field = DateUtils.TruncateField.YEAR,
firstWeekday = Calendar.SATURDAY,
isNumerical = true, isNumerical = true,
) )
assertThat(byYear.size, equalTo(2)) assertThat(byYear.size, equalTo(2))
@ -213,10 +186,8 @@ class EntryListTest {
entries.add(Entry(reference.minus(offsets[it]), YES_MANUAL)) entries.add(Entry(reference.minus(offsets[it]), YES_MANUAL))
} }
val byMonth = entries.groupBy( val byMonth = entries.getKnown().groupedSum(
original = entries.getKnown(), truncateField = DateUtils.TruncateField.MONTH,
field = DateUtils.TruncateField.MONTH,
firstWeekday = Calendar.SATURDAY,
isNumerical = false, isNumerical = false,
) )
assertThat(byMonth.size, equalTo(17)) assertThat(byMonth.size, equalTo(17))
@ -224,10 +195,8 @@ class EntryListTest {
assertThat(byMonth[6], equalTo(Entry(Timestamp.from(2013, Calendar.DECEMBER, 1), 7_000))) assertThat(byMonth[6], equalTo(Entry(Timestamp.from(2013, Calendar.DECEMBER, 1), 7_000)))
assertThat(byMonth[12], equalTo(Entry(Timestamp.from(2013, Calendar.MAY, 1), 6_000))) assertThat(byMonth[12], equalTo(Entry(Timestamp.from(2013, Calendar.MAY, 1), 6_000)))
val byQuarter = entries.groupBy( val byQuarter = entries.getKnown().groupedSum(
original = entries.getKnown(), truncateField = DateUtils.TruncateField.QUARTER,
field = DateUtils.TruncateField.QUARTER,
firstWeekday = Calendar.SATURDAY,
isNumerical = false, isNumerical = false,
) )
assertThat(byQuarter.size, equalTo(6)) assertThat(byQuarter.size, equalTo(6))
@ -235,10 +204,8 @@ class EntryListTest {
assertThat(byQuarter[3], equalTo(Entry(Timestamp.from(2013, Calendar.JULY, 1), 17_000))) assertThat(byQuarter[3], equalTo(Entry(Timestamp.from(2013, Calendar.JULY, 1), 17_000)))
assertThat(byQuarter[5], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 20_000))) assertThat(byQuarter[5], equalTo(Entry(Timestamp.from(2013, Calendar.JANUARY, 1), 20_000)))
val byYear = entries.groupBy( val byYear = entries.getKnown().groupedSum(
original = entries.getKnown(), truncateField = DateUtils.TruncateField.YEAR,
field = DateUtils.TruncateField.YEAR,
firstWeekday = Calendar.SATURDAY,
isNumerical = false, isNumerical = false,
) )
assertThat(byYear.size, equalTo(2)) assertThat(byYear.size, equalTo(2))

@ -19,6 +19,7 @@
package org.isoron.uhabits.core.ui.screens.habits.list; package org.isoron.uhabits.core.ui.screens.habits.list;
import org.apache.commons.lang3.*;
import org.isoron.uhabits.core.*; import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.commands.*; import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*; import org.isoron.uhabits.core.models.*;
@ -104,7 +105,11 @@ public class HabitCardListCacheTest extends BaseUnitTest
assertThat(cache.getScore(h.getId()), equalTo(score)); assertThat(cache.getScore(h.getId()), equalTo(score));
int[] actualCheckmarks = cache.getCheckmarks(h.getId()); int[] actualCheckmarks = cache.getCheckmarks(h.getId());
int[] expectedCheckmarks = h.getComputedEntries().getValues(today.minus(9), today); int[] expectedCheckmarks = ArrayUtils.toPrimitive(h.getComputedEntries()
.getByInterval(today.minus(9), today)
.stream()
.map(Entry::getValue)
.toArray(Integer[]::new));
assertThat(actualCheckmarks, equalTo(expectedCheckmarks)); assertThat(actualCheckmarks, equalTo(expectedCheckmarks));
} }

Loading…
Cancel
Save