Restructure ShowHabitActivity

pull/699/head
Alinson S. Xavier 5 years ago
parent bda3e42e2b
commit ae4ac801a0

@ -38,7 +38,6 @@ class TestModule {
ActivityContextModule::class,
HabitsActivityModule::class,
ListHabitsModule::class,
ShowHabitModule::class,
HabitModule::class,
TestModule::class
), dependencies = arrayOf(HabitsApplicationComponent::class))

@ -19,18 +19,20 @@
package org.isoron.uhabits.activities.habits.show.views
import android.view.*
import android.view.View.*
import androidx.test.ext.junit.runners.*
import androidx.test.filters.*
import org.hamcrest.Matchers.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.habits.show.*
import org.junit.*
import org.junit.Assert.*
import org.junit.runner.*
@RunWith(AndroidJUnit4::class)
@MediumTest
class NotesCardTest : BaseViewTest() {
class NotesCardViewTest : BaseViewTest() {
val PATH = "habits/show/NotesCard"
private lateinit var view: NotesCard
private lateinit var view: NotesCardView
@Before
override fun setUp() {
@ -39,24 +41,18 @@ class NotesCardTest : BaseViewTest() {
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById(R.id.notesCard)
view.onData(ShowHabitViewModel(
description = "This is a test description",
))
view.update(NotesCardViewModel(description = "This is a test description"))
measureView(view, 800f, 200f)
}
@Test
@Throws(Exception::class)
fun testRender() {
assertRenders(view, "$PATH/render.png")
}
@Test
@Throws(Exception::class)
fun testRenderEmptyDescription() {
view.onData(ShowHabitViewModel(
description = "",
))
assertRenders(view, "$PATH/render-empty-description.png")
view.update(NotesCardViewModel(description = ""))
assertThat(view.visibility, equalTo(GONE))
}
}

@ -22,16 +22,15 @@ import android.view.*
import androidx.test.ext.junit.runners.*
import androidx.test.filters.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.habits.show.*
import org.isoron.uhabits.core.models.*
import org.junit.*
import org.junit.runner.*
@RunWith(AndroidJUnit4::class)
@MediumTest
class OverviewCardTest : BaseViewTest() {
class OverviewCardViewTest : BaseViewTest() {
val PATH = "habits/show/OverviewCard/"
private lateinit var view: OverviewCard
private lateinit var view: OverviewCardView
@Before
override fun setUp() {
@ -39,8 +38,8 @@ class OverviewCardTest : BaseViewTest() {
view = LayoutInflater
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById<View>(R.id.overviewCard) as OverviewCard
view.onData(ShowHabitViewModel(
.findViewById<View>(R.id.overviewCard) as OverviewCardView
view.update(OverviewCardViewModel(
scoreToday = 0.74f,
scoreMonthDiff = 0.23f,
scoreYearDiff = 0.74f,

@ -22,15 +22,15 @@ import android.view.*
import androidx.test.ext.junit.runners.*
import androidx.test.filters.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.habits.show.*
import org.isoron.uhabits.core.models.*
import org.junit.*
import org.junit.runner.*
@RunWith(AndroidJUnit4::class)
@MediumTest
class SubtitleCardTest : BaseViewTest() {
class SubtitleCardViewTest : BaseViewTest() {
val PATH = "habits/show/SubtitleCard/"
private lateinit var view: SubtitleCard
private lateinit var view: SubtitleCardView
@Before
override fun setUp() {
@ -39,10 +39,13 @@ class SubtitleCardTest : BaseViewTest() {
.from(targetContext)
.inflate(R.layout.show_habit, null)
.findViewById(R.id.subtitleCard)
view.onData(ShowHabitViewModel(
view.update(SubtitleCardViewModel(
color = PaletteColor(7),
frequencyText = "3 times in 7 days",
isNumerical = false,
question = "Did you meditate this morning?",
reminderText = "8:30 AM",
frequencyText = "3 times in 7 days",
targetText = "",
))
measureView(view, 800f, 200f)
}

@ -1,45 +0,0 @@
/*
* 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.activities
import android.content.*
import android.util.*
import android.widget.*
abstract class DataView<T>(
context: Context,
attrs: AttributeSet,
) : LinearLayout(context, attrs), Presenter.Listener<T> {
lateinit var presenter: Presenter<T>
override fun onAttachedToWindow() {
super.onAttachedToWindow()
presenter.addListener(this)
presenter.requestData(this)
}
override fun onDetachedFromWindow() {
presenter.removeListener(this)
super.onDetachedFromWindow()
}
abstract override fun onData(data: T)
}

@ -36,11 +36,9 @@ import org.isoron.uhabits.core.ui.screens.habits.list.*
BaseActivityModule::class,
HabitsActivityModule::class,
ListHabitsModule::class,
ShowHabitModule::class,
HabitModule::class
), dependencies = arrayOf(HabitsApplicationComponent::class))
interface HabitsActivityComponent {
val showHabitPresenter: ShowHabitPresenter
val colorPickerDialogFactory: ColorPickerDialogFactory
val habitCardListAdapter: HabitCardListAdapter
val listHabitsBehavior: ListHabitsBehavior
@ -48,6 +46,5 @@ interface HabitsActivityComponent {
val listHabitsRootView: ListHabitsRootView
val listHabitsScreen: ListHabitsScreen
val listHabitsSelectionMenu: ListHabitsSelectionMenu
val showHabitScreen: ShowHabitScreen
val themeSwitcher: ThemeSwitcher
}

@ -1,68 +0,0 @@
/*
* 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.activities
import org.isoron.uhabits.core.commands.*
abstract class Presenter<M>(
val commandRunner: CommandRunner,
) : CommandRunner.Listener {
private val listeners = mutableListOf<Listener<M>>()
private var data: M? = null
fun onResume() {
commandRunner.addListener(this)
data = refresh()
notifyListeners()
}
abstract fun refresh(): M
fun onPause() {
commandRunner.removeListener(this)
}
fun addListener(listener: Listener<M>) {
listeners.add(listener)
}
fun removeListener(listener: Listener<M>) {
listeners.remove(listener)
}
fun requestData(listener: Listener<M>) {
if (data == null) data = refresh()
listener.onData(data!!)
}
override fun onCommandExecuted(command: Command?, refreshKey: Long?) {
data = refresh()
notifyListeners()
}
private fun notifyListeners() {
for (l in listeners) l.onData(data!!)
}
interface Listener<T> {
fun onData(data: T)
}
}

@ -18,22 +18,127 @@
*/
package org.isoron.uhabits.activities.habits.show
import android.content.*
import android.os.*
import org.isoron.uhabits.activities.*
import android.view.*
import android.widget.*
import androidx.appcompat.app.*
import kotlinx.coroutines.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.habits.show.views.*
import org.isoron.uhabits.core.commands.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
class ShowHabitActivity : AppCompatActivity(), CommandRunner.Listener {
private lateinit var view: ShowHabitView
private lateinit var presenter: ShowHabitPresenter
private lateinit var commandRunner: CommandRunner
private val scope = CoroutineScope(Dispatchers.Main)
class ShowHabitActivity : HabitsActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setScreen(component.showHabitScreen)
val appComponent = (applicationContext as HabitsApplication).component
val habitList = appComponent.habitList
val habit = habitList.getById(ContentUris.parseId(intent.data!!))!!
commandRunner = appComponent.commandRunner
view = ShowHabitView(this)
presenter = ShowHabitPresenter(
habit = habit,
context = this,
preferences = appComponent.preferences
)
setContentView(view)
}
override fun onResume() {
super.onResume()
component.showHabitPresenter.onResume()
commandRunner.addListener(this)
updateViews()
}
override fun onPause() {
component.showHabitPresenter.onPause()
commandRunner.removeListener(this)
super.onPause()
}
override fun onCommandExecuted(command: Command?, refreshKey: Long?) {
updateViews()
}
private fun updateViews() {
scope.launch {
view.update(presenter.present())
}
}
}
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,
)
class ShowHabitView(context: Context) : FrameLayout(context) {
private val binding = ShowHabitBinding.inflate(LayoutInflater.from(context))
init {
addView(binding.root)
}
fun update(data: ShowHabitViewModel) {
setupToolbar(binding.toolbar, title = data.title, color = data.color)
binding.subtitleCard.update(data.subtitle)
binding.overviewCard.update(data.overview)
binding.notesCard.update(data.notes)
binding.targetCard.update(data.target)
if (data.isNumerical) {
binding.overviewCard.visibility = GONE
binding.streakCard.visibility = GONE
} else {
binding.targetCard.visibility = GONE
}
}
fun setController(controller: Controller) {
binding.historyCard.setController(controller)
}
interface Controller : HistoryCard.Controller
}
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
)
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(),
)
}
}

@ -1,35 +0,0 @@
/*
* Copyright (C) 2017 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.activities.habits.show
import dagger.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.core.ui.screens.habits.show.*
@Module
abstract class ShowHabitModule {
@Binds
abstract fun getScreen(screen: ShowHabitScreen): ShowHabitBehavior.Screen
@Binds
abstract fun getMenuScreen(screen: ShowHabitScreen): ShowHabitMenuBehavior.Screen
@Binds
abstract fun getSystem(system: HabitsDirFinder): ShowHabitMenuBehavior.System
}

@ -1,113 +0,0 @@
/*
* 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.activities.habits.show
import android.annotation.*
import android.content.*
import org.isoron.androidbase.activities.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.commands.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.utils.*
import java.util.*
import javax.inject.*
@ActivityScope
class ShowHabitPresenter
@Inject constructor(
val habit: Habit,
val preferences: Preferences,
commandRunner: CommandRunner,
@ActivityContext val context: Context,
) : Presenter<ShowHabitViewModel>(commandRunner) {
private val resources = context.resources
override fun refresh(): ShowHabitViewModel {
val today = DateUtils.getTodayWithOffset()
val lastMonth = today.minus(30)
val lastYear = today.minus(365)
val reminderText = if (habit.hasReminder()) {
formatTime(context, habit.reminder.hour, habit.reminder.minute)!!
} else {
resources.getString(R.string.reminder_off)
}
val scores = habit.scores
val scoreToday = scores.todayValue.toFloat()
val scoreLastMonth = scores.getValue(lastMonth).toFloat()
val scoreLastYear = scores.getValue(lastYear).toFloat()
return ShowHabitViewModel(
title = habit.name,
description = habit.description,
question = habit.question,
color = habit.color,
isNumerical = habit.isNumerical,
scoreToday = scoreToday,
scoreMonthDiff = scoreToday - scoreLastMonth,
scoreYearDiff = scoreToday - scoreLastYear,
totalCount = habit.repetitions.totalCount,
targetText = "${habit.targetValue.toShortString()} ${habit.unit}",
frequencyText = habit.frequency.format(),
reminderText = reminderText,
)
}
@SuppressLint("StringFormatMatches")
private fun Frequency.format(): String {
val num = this.numerator
val den = this.denominator
if (num == den) {
return resources.getString(R.string.every_day)
}
if (den == 7) {
return resources.getString(R.string.x_times_per_week, num)
}
if (den == 30 || den == 31) {
return resources.getString(R.string.x_times_per_month, num)
}
if (num == 1) {
if (den == 7) {
return resources.getString(R.string.every_week)
}
if (den % 7 == 0) {
return resources.getString(R.string.every_x_weeks, den / 7)
}
if (den == 30 || den == 31) {
return resources.getString(R.string.every_month)
}
return resources.getString(R.string.every_x_days, den)
}
return String.format(
Locale.US,
"%d %s %d %s",
num,
resources.getString(R.string.times_every),
den,
resources.getString(R.string.days),
)
}
}

@ -1,100 +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
import android.content.*
import android.view.*
import org.isoron.androidbase.activities.*
import org.isoron.androidbase.utils.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.habits.show.views.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import javax.inject.*
@ActivityScope
class ShowHabitRootView
@Inject constructor(
@ActivityContext context: Context,
private val habit: Habit,
private val presenter: ShowHabitPresenter,
targetCardPresenter: TargetCardPresenter,
) : BaseRootView(context), Presenter.Listener<ShowHabitViewModel> {
private var controller: Controller = object : Controller {}
private var binding = ShowHabitBinding.inflate(LayoutInflater.from(context))
init {
addView(binding.root)
displayHomeAsUp = true
binding.overviewCard.presenter = presenter
binding.notesCard.presenter = presenter
binding.subtitleCard.presenter = presenter
binding.targetCard.presenter = targetCardPresenter
binding.scoreCard.habit = habit
binding.historyCard.habit = habit
binding.streakCard.habit = habit
binding.frequencyCard.habit = habit
binding.barCard.habit = habit
initToolbar()
}
override fun getToolbarColor(): Int {
val res = StyledResources(context)
return if (!res.getBoolean(R.attr.useHabitColorAsPrimary)) super.getToolbarColor()
else habit.color.toThemedAndroidColor(context)
}
fun setController(controller: Controller) {
this.controller = controller
binding.historyCard.setController(controller)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
presenter.addListener(this)
presenter.requestData(this)
}
override fun onDetachedFromWindow() {
presenter.removeListener(this)
super.onDetachedFromWindow()
}
override fun onData(data: ShowHabitViewModel) {
binding.toolbar.title = data.title
if (data.isNumerical) {
binding.overviewCard.visibility = GONE
binding.streakCard.visibility = GONE
} else {
binding.targetCard.visibility = GONE
}
controller.onToolbarChanged()
}
interface Controller : HistoryCard.Controller {
fun onToolbarChanged() {}
}
}

@ -40,8 +40,8 @@ import dagger.*;
public class ShowHabitScreen extends BaseScreen
implements ShowHabitMenuBehavior.Screen,
ShowHabitBehavior.Screen,
HistoryEditorDialog.Controller,
ShowHabitRootView.Controller
HistoryEditorDialog.Controller
//ShowHabitRootView.Controller
{
@NonNull
private final Habit habit;
@ -60,7 +60,7 @@ public class ShowHabitScreen extends BaseScreen
@Inject
public ShowHabitScreen(@NonNull BaseActivity activity,
@NonNull Habit habit,
@NonNull ShowHabitRootView view,
//@NonNull ShowHabitRootView view,
@NonNull ShowHabitsMenu menu,
@NonNull ConfirmDeleteDialogFactory confirmDeleteDialogFactory,
@NonNull IntentFactory intentFactory,
@ -70,20 +70,20 @@ public class ShowHabitScreen extends BaseScreen
super(activity);
this.intentFactory = intentFactory;
setMenu(menu);
setRootView(view);
//setRootView(view);
this.habit = habit;
this.behavior = behavior;
this.confirmDeleteDialogFactory = confirmDeleteDialogFactory;
this.numberPickerFactory = numberPickerFactory;
view.setController(this);
//view.setController(this);
}
@Override
public void onEditHistoryButtonClick()
{
behavior.get().onEditHistory();
}
// @Override
// public void onEditHistoryButtonClick()
// {
// behavior.get().onEditHistory();
// }
@Override
public void showNumberPicker(double value,
@ -99,11 +99,11 @@ public class ShowHabitScreen extends BaseScreen
behavior.get().onToggleCheckmark(timestamp, value);
}
@Override
public void onToolbarChanged()
{
invalidateToolbar();
}
// @Override
// public void onToolbarChanged()
// {
// invalidateToolbar();
// }
@Override
public void reattachDialogs()

@ -1,37 +0,0 @@
/*
* 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.activities.habits.show
import org.isoron.uhabits.core.models.*
data class ShowHabitViewModel(
val title: String = "",
val description: String = "",
val question: String = "",
val isNumerical: Boolean = false,
val scoreToday: Float = 0f,
val scoreMonthDiff: Float = 0f,
val scoreYearDiff: Float = 0f,
val totalCount: Long = 0L,
val color: PaletteColor = PaletteColor(1),
val targetText: String = "",
val frequencyText: String = "",
val reminderText: String = "",
)

@ -35,18 +35,19 @@ import dagger.*;
@ActivityScope
public class ShowHabitsMenu extends BaseMenu
{
@NonNull
private Lazy<ShowHabitMenuBehavior> behavior;
// @NonNull
// private Lazy<ShowHabitMenuBehavior> behavior;
@NonNull
private final Preferences prefs;
@Inject
public ShowHabitsMenu(@NonNull BaseActivity activity,
@NonNull Lazy<ShowHabitMenuBehavior> behavior,
//@NonNull Lazy<ShowHabitMenuBehavior> behavior,
@NonNull Preferences prefs)
{
super(activity);
this.behavior = behavior;
//this.behavior = behavior;
this.prefs = prefs;
}
@ -64,21 +65,21 @@ public class ShowHabitsMenu extends BaseMenu
{
switch (item.getItemId())
{
case R.id.action_edit_habit:
behavior.get().onEditHabit();
return true;
case R.id.export:
behavior.get().onExportCSV();
return true;
case R.id.action_delete:
behavior.get().onDeleteHabit();
return true;
case R.id.action_randomize:
behavior.get().onRandomize();
return true;
// case R.id.action_edit_habit:
// behavior.get().onEditHabit();
// return true;
//
// case R.id.export:
// behavior.get().onExportCSV();
// return true;
//
// case R.id.action_delete:
// behavior.get().onDeleteHabit();
// return true;
//
// case R.id.action_randomize:
// behavior.get().onRandomize();
// return true;
default:
return false;

@ -22,20 +22,27 @@ package org.isoron.uhabits.activities.habits.show.views
import android.content.*
import android.util.*
import android.view.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.habits.show.*
import android.widget.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.databinding.*
class NotesCard(context: Context, attrs: AttributeSet) : DataView<ShowHabitViewModel>(context, attrs) {
data class NotesCardViewModel(val description: String)
class NotesCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private val binding = ShowHabitNotesBinding.inflate(LayoutInflater.from(context), this)
override fun onData(data: ShowHabitViewModel) {
fun update(data: NotesCardViewModel) {
if (data.description.isEmpty()) {
visibility = GONE
} else {
visibility = VISIBLE
binding.habitNotes.text = data.description
}
invalidate()
}
}
class NotesCardPresenter(val habit: Habit) {
fun present() = NotesCardViewModel(
description = habit.description,
)
}

@ -21,14 +21,24 @@ package org.isoron.uhabits.activities.habits.show.views
import android.content.*
import android.util.*
import android.view.*
import android.widget.*
import kotlinx.coroutines.*
import org.isoron.androidbase.utils.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.habits.show.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
class OverviewCard(context: Context, attrs: AttributeSet) : DataView<ShowHabitViewModel>(context, attrs) {
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) {
private val binding = ShowHabitOverviewBinding.inflate(LayoutInflater.from(context), this)
@ -37,7 +47,7 @@ class OverviewCard(context: Context, attrs: AttributeSet) : DataView<ShowHabitVi
Math.abs(percentageDiff) * 100)
}
override fun onData(data: ShowHabitViewModel) {
fun update(data: OverviewCardViewModel) {
val androidColor = data.color.toThemedAndroidColor(context)
val res = StyledResources(context)
val inactiveColor = res.getColor(R.attr.mediumContrastTextColor)
@ -55,3 +65,22 @@ class OverviewCard(context: Context, attrs: AttributeSet) : DataView<ShowHabitVi
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.todayValue.toFloat()
val scoreLastMonth = scores.getValue(lastMonth).toFloat()
val scoreLastYear = scores.getValue(lastYear).toFloat()
return@IO OverviewCardViewModel(
color = habit.color,
scoreToday = scoreToday,
scoreMonthDiff = scoreToday - scoreLastMonth,
scoreYearDiff = scoreToday - scoreLastYear,
totalCount = habit.repetitions.totalCount,
)
}
}

@ -18,16 +18,30 @@
*/
package org.isoron.uhabits.activities.habits.show.views
import android.annotation.*
import android.content.*
import android.content.res.*
import android.util.*
import android.view.*
import android.widget.*
import org.isoron.androidbase.utils.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.activities.habits.show.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.habits.list.views.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import java.util.*
class SubtitleCard(context: Context, attrs: AttributeSet) : DataView<ShowHabitViewModel>(context, attrs) {
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) {
private val binding = ShowHabitSubtitleBinding.inflate(LayoutInflater.from(context), this)
@ -38,7 +52,7 @@ class SubtitleCard(context: Context, attrs: AttributeSet) : DataView<ShowHabitVi
binding.reminderIcon.typeface = fontAwesome
}
override fun onData(data: ShowHabitViewModel) {
fun update(data: SubtitleCardViewModel) {
val color = data.color.toThemedAndroidColor(context)
binding.frequencyLabel.text = data.frequencyText
binding.questionLabel.setTextColor(color)
@ -60,3 +74,61 @@ class SubtitleCard(context: Context, attrs: AttributeSet) : DataView<ShowHabitVi
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")
private fun Frequency.format(): String {
val num = this.numerator
val den = this.denominator
if (num == den) {
return resources.getString(R.string.every_day)
}
if (den == 7) {
return resources.getString(R.string.x_times_per_week, num)
}
if (den == 30 || den == 31) {
return resources.getString(R.string.x_times_per_month, num)
}
if (num == 1) {
if (den == 7) {
return resources.getString(R.string.every_week)
}
if (den % 7 == 0) {
return resources.getString(R.string.every_x_weeks, den / 7)
}
if (den == 30 || den == 31) {
return resources.getString(R.string.every_month)
}
return resources.getString(R.string.every_x_days, den)
}
return String.format(
Locale.US,
"%d %s %d %s",
num,
resources.getString(R.string.times_every),
den,
resources.getString(R.string.days),
)
}
}

@ -19,19 +19,17 @@
package org.isoron.uhabits.activities.habits.show.views
import android.content.*
import android.content.res.*
import android.util.*
import android.view.*
import org.isoron.androidbase.activities.*
import android.widget.*
import kotlinx.coroutines.*
import org.isoron.uhabits.*
import org.isoron.uhabits.activities.*
import org.isoron.uhabits.core.commands.*
import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.preferences.*
import org.isoron.uhabits.core.utils.*
import org.isoron.uhabits.databinding.*
import org.isoron.uhabits.utils.*
import java.util.*
import javax.inject.*
data class TargetCardViewModel(
val color: PaletteColor,
@ -40,35 +38,28 @@ data class TargetCardViewModel(
val labels: List<String> = listOf(),
)
class TargetCardView(context: Context, attrs: AttributeSet) : DataView<TargetCardViewModel>(context, attrs) {
class TargetCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
private val binding = ShowHabitTargetBinding.inflate(LayoutInflater.from(context), this)
override fun onData(data: TargetCardViewModel) {
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()
}
}
@ActivityScope
class TargetCardPresenter
@Inject constructor(
class TargetCardPresenter(
val habit: Habit,
val preferences: Preferences,
@ActivityContext val context: Context,
commandRunner: CommandRunner,
) : Presenter<TargetCardViewModel>(commandRunner) {
val resources = context.resources
override fun refresh(): TargetCardViewModel {
val firstWeekday: Int,
val resources: Resources,
) {
suspend fun present(): TargetCardViewModel = Dispatchers.IO {
val checkmarks = habit.checkmarks
val valueToday = checkmarks.todayValue / 1e3
val valueThisWeek = checkmarks.getThisWeekValue(preferences.firstWeekday) / 1e3
val valueThisWeek = checkmarks.getThisWeekValue(firstWeekday) / 1e3
val valueThisMonth = checkmarks.thisMonthValue / 1e3
val valueThisQuarter = checkmarks.thisQuarterValue / 1e3
val valueThisYear = checkmarks.thisYearValue / 1e3
@ -105,7 +96,7 @@ class TargetCardPresenter
labels.add(resources.getString(R.string.quarter))
labels.add(resources.getString(R.string.year))
return TargetCardViewModel(
return@IO TargetCardViewModel(
color = habit.color,
values = values,
labels = labels,

@ -20,15 +20,18 @@
package org.isoron.uhabits.utils
import android.graphics.*
import androidx.annotation.*
import androidx.appcompat.widget.Toolbar
import android.graphics.drawable.*
import android.view.*
import android.view.ViewGroup.LayoutParams.*
import android.widget.*
import android.widget.RelativeLayout.*
import com.google.android.material.snackbar.Snackbar
import androidx.annotation.*
import androidx.appcompat.app.*
import androidx.appcompat.widget.Toolbar
import com.google.android.material.snackbar.*
import org.isoron.androidbase.utils.*
import org.isoron.uhabits.*
import org.isoron.uhabits.core.models.*
fun RelativeLayout.addBelow(view: View,
subject: View,
@ -83,6 +86,23 @@ fun View.showMessage(@StringRes stringId: Int) {
}
}
fun View.setupToolbar(toolbar: Toolbar, title: String, color: PaletteColor) {
toolbar.elevation = InterfaceUtils.dpToPixels(context, 2f)
val res = StyledResources(context)
toolbar.title = title
val toolbarColor = if (!res.getBoolean(R.attr.useHabitColorAsPrimary)) {
StyledResources(context).getColor(org.isoron.androidbase.R.attr.colorPrimary)
} else {
color.toThemedAndroidColor(context)
}
val darkerColor = ColorUtils.mixColors(toolbarColor, Color.BLACK, 0.75f)
toolbar.background = ColorDrawable(toolbarColor)
val activity = context as AppCompatActivity
activity.window.statusBarColor = darkerColor
activity.setSupportActionBar(toolbar)
activity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
fun Int.toMeasureSpec(mode: Int) =
View.MeasureSpec.makeMeasureSpec(this, mode)

@ -23,6 +23,7 @@ import android.content.*
import android.view.*
import android.view.ViewGroup.*
import android.view.ViewGroup.LayoutParams.*
import kotlinx.coroutines.*
import org.isoron.uhabits.activities.common.views.*
import org.isoron.uhabits.activities.habits.show.views.*
import org.isoron.uhabits.core.models.*
@ -38,13 +39,13 @@ class TargetWidget(
override fun getOnClickPendingIntent(context: Context) =
pendingIntentFactory.showHabit(habit)
override fun refreshData(view: View) {
override fun refreshData(view: View) = runBlocking {
val widgetView = view as GraphWidgetView
widgetView.setBackgroundAlpha(preferedBackgroundAlpha)
if (preferedBackgroundAlpha >= 255) widgetView.setShadowAlpha(0x4f)
val chart = (widgetView.dataView as TargetChart)
val presenter = TargetCardPresenter(habit, prefs, context, commandRunner)
val data = presenter.refresh()
val presenter = TargetCardPresenter(habit, prefs.firstWeekday, context.resources)
val data = presenter.present()
chart.setColor(data.color.toThemedAndroidColor(context))
chart.setTargets(data.targets)
chart.setLabels(data.labels)

@ -43,16 +43,16 @@
style="@style/CardList"
android:clipToPadding="false">
<org.isoron.uhabits.activities.habits.show.views.SubtitleCard
<org.isoron.uhabits.activities.habits.show.views.SubtitleCardView
android:id="@+id/subtitleCard"
style="@style/ShowHabit.Subtitle"/>
<org.isoron.uhabits.activities.habits.show.views.NotesCard
<org.isoron.uhabits.activities.habits.show.views.NotesCardView
android:id="@+id/notesCard"
style="@style/Card"
android:gravity="center" />
<org.isoron.uhabits.activities.habits.show.views.OverviewCard
<org.isoron.uhabits.activities.habits.show.views.OverviewCardView
android:id="@+id/overviewCard"
style="@style/Card"
android:paddingTop="12dp"/>

Loading…
Cancel
Save