A fork of Simple Time Tracker with WearOS support. (Now integrated upstream)
0

Configure Feed

Select the types of activity you want to include in your feed.

add separate screen for goals

razeeman (Jul 22, 2023, 12:35 PM +0300) 4594e826 a3cfa605

+666 -190
+1
README.md
··· 110 110 │ ├── feature_change_running_record # Edit timer screen. 111 111 │ ├── feature_date_edit # Data edit screen. 112 112 │ ├── feature_dialogs # Dialogs. 113 + │ ├── feature_goals # Separate screen for goals. 113 114 │ ├── feature_main # Main screen with tabs. 114 115 │ ├── feature_notification # Notifications. 115 116 │ ├── feature_records # One of main tabs, records list.
+1
app/build.gradle.kts
··· 80 80 implementation(project(":feature_tag_selection")) 81 81 implementation(project(":feature_data_edit")) 82 82 implementation(project(":feature_records_filter")) 83 + implementation(project(":feature_goals")) 83 84 84 85 implementation(Deps.Androidx.room) 85 86 implementation(Deps.Ktx.navigationFragment)
+102
core/src/main/java/com/example/util/simpletimetracker/core/mapper/GoalViewDataMapper.kt
··· 1 + package com.example.util.simpletimetracker.core.mapper 2 + 3 + import com.example.util.simpletimetracker.core.viewData.StatisticsDataHolder 4 + import com.example.util.simpletimetracker.domain.model.RangeLength 5 + import com.example.util.simpletimetracker.domain.model.Statistics 6 + import com.example.util.simpletimetracker.feature_base_adapter.statisticsGoal.StatisticsGoalViewData 7 + import javax.inject.Inject 8 + import kotlin.math.roundToLong 9 + 10 + class GoalViewDataMapper @Inject constructor( 11 + private val colorMapper: ColorMapper, 12 + private val iconMapper: IconMapper, 13 + private val timeMapper: TimeMapper, 14 + ) { 15 + 16 + fun mapList( 17 + rangeLength: RangeLength, 18 + statistics: List<Statistics>, 19 + data: Map<Long, StatisticsDataHolder>, 20 + isDarkTheme: Boolean, 21 + useProportionalMinutes: Boolean, 22 + showSeconds: Boolean, 23 + ): List<StatisticsGoalViewData> { 24 + return statistics 25 + .mapNotNull { statistic -> 26 + mapItem( 27 + rangeLength = rangeLength, 28 + statistics = statistic, 29 + dataHolder = data[statistic.id] ?: return@mapNotNull null, 30 + isDarkTheme = isDarkTheme, 31 + useProportionalMinutes = useProportionalMinutes, 32 + showSeconds = showSeconds, 33 + ) 34 + } 35 + .sortedBy { it.goal.percent } 36 + } 37 + 38 + fun mapItem( 39 + rangeLength: RangeLength, 40 + statistics: Statistics, 41 + dataHolder: StatisticsDataHolder, 42 + isDarkTheme: Boolean, 43 + useProportionalMinutes: Boolean, 44 + showSeconds: Boolean, 45 + ): StatisticsGoalViewData { 46 + return StatisticsGoalViewData( 47 + id = statistics.id, 48 + name = dataHolder.name, 49 + icon = dataHolder.icon.orEmpty() 50 + .let(iconMapper::mapIcon), 51 + color = dataHolder.color 52 + .let { colorMapper.mapToColorInt(it, isDarkTheme) }, 53 + goal = mapGoal( 54 + statistics = statistics, 55 + dataHolder = dataHolder, 56 + rangeLength = rangeLength, 57 + useProportionalMinutes = useProportionalMinutes, 58 + showSeconds = showSeconds, 59 + ), 60 + ) 61 + } 62 + 63 + private fun mapGoal( 64 + statistics: Statistics, 65 + dataHolder: StatisticsDataHolder, 66 + rangeLength: RangeLength, 67 + useProportionalMinutes: Boolean, 68 + showSeconds: Boolean, 69 + ): StatisticsGoalViewData.Goal { 70 + val goal = when (rangeLength) { 71 + is RangeLength.Day -> dataHolder.dailyGoalTime 72 + is RangeLength.Week -> dataHolder.weeklyGoalTime 73 + is RangeLength.Month -> dataHolder.monthlyGoalTime 74 + else -> return StatisticsGoalViewData.Goal.empty() 75 + } * 1000 76 + 77 + if (goal == 0L) return StatisticsGoalViewData.Goal.empty() 78 + 79 + val current = statistics.duration 80 + val goalComplete = goal - current <= 0L 81 + val currentString = timeMapper.formatInterval( 82 + interval = current, 83 + forceSeconds = showSeconds, 84 + useProportionalMinutes = useProportionalMinutes, 85 + ) 86 + val goalString = timeMapper.formatInterval( 87 + interval = goal, 88 + forceSeconds = showSeconds, 89 + useProportionalMinutes = useProportionalMinutes, 90 + ) 91 + val goalTimeString = "$currentString\n($goalString)" 92 + val goalTimePercent = (current * 100f / goal).roundToLong() 93 + .coerceAtMost(100) 94 + 95 + return StatisticsGoalViewData.Goal( 96 + goalTime = goalTimeString, 97 + goalPercent = goalTimePercent.let { "$it%" }, 98 + goalComplete = goalComplete, 99 + percent = goalTimePercent, 100 + ) 101 + } 102 + }
+1
core/src/main/java/com/example/util/simpletimetracker/core/model/NavigationTab.kt
··· 5 5 object Records : NavigationTab 6 6 object Statistics : NavigationTab 7 7 object Settings : NavigationTab 8 + object Goals : NavigationTab 8 9 }
+1
core/src/main/res/values-ar/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">ترتيب عكسي للمتتبعات</string> 210 210 <string name="settings_days_in_calendar">الأيام في التقويم</string> 211 211 <string name="settings_show_activity_filters">إظهار عوامل تصفية النشاط على الشاشة الرئيسية</string> 212 + <string name="settings_show_goals_separately">إظهار الأهداف في علامة تبويب منفصلة</string> 212 213 <string name="settings_allow_multitasking">السماح بتعدد المهام</string> 213 214 <string name="settings_allow_multitasking_hint">ًإذا كان غير مفعل، فبدء تشغيل مهمة جديدة سيوقف الأنشطة الأخرى تلقائيا.</string> 214 215 <string name="settings_show_notifications">إظهار إشعارات المؤقت</string>
+1
core/src/main/res/values-ca/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">Inverteix l\'ordre dels enregistraments</string> 210 210 <string name="settings_days_in_calendar">Dies al calendari</string> 211 211 <string name="settings_show_activity_filters">Mostra els filtres d\'activitat a la pantalla principal.</string> 212 + <string name="settings_show_goals_separately">Mostra els objectius en una pestanya separada</string> 212 213 <string name="settings_allow_multitasking">Permet la multitasca</string> 213 214 <string name="settings_allow_multitasking_hint">Si és desactivat, en començar un nou temporitzador s\'aturaran automàticament les altres activitats.</string> 214 215 <string name="settings_show_notifications">Mostra notificacions del temporitzador</string>
+1
core/src/main/res/values-de/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">Umgekehrte Reihenfolge der Aufzeichnungen</string> 210 210 <string name="settings_days_in_calendar">Tage im Kalender</string> 211 211 <string name="settings_show_activity_filters">Aktivitätsfilter auf dem Hauptbildschirm anzeigen</string> 212 + <string name="settings_show_goals_separately">Ziele auf einer separaten Registerkarte anzeigen</string> 212 213 <string name="settings_allow_multitasking">Multitasking erlauben</string> 213 214 <string name="settings_allow_multitasking_hint">Wenn diese Funktion deaktiviert ist, werden beim Starten eines neuen Timers andere Aktivitäten automatisch gestoppt.</string> 214 215 <string name="settings_show_notifications">Timer-Benachrichtigungen anzeigen</string>
+1
core/src/main/res/values-es/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">Orden inverso de registros</string> 210 210 <string name="settings_days_in_calendar">Días en el calendario</string> 211 211 <string name="settings_show_activity_filters">Mostrar filtros de actividad en la pantalla principal</string> 212 + <string name="settings_show_goals_separately">Mostrar objetivos en una pestaña separada</string> 212 213 <string name="settings_allow_multitasking">Permitir el registro multitarea</string> 213 214 <string name="settings_allow_multitasking_hint">Si está deshabilitado, al iniciar un nuevo temporizador se detendrá automáticamente el de las otras actividades.</string> 214 215 <string name="settings_show_notifications">Mostrar notificaciones del temporizador</string>
+1
core/src/main/res/values-fa/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">ترتیب برعکس تاریخچه ها</string> 210 210 <string name="settings_days_in_calendar">روزها در تقویم</string> 211 211 <string name="settings_show_activity_filters">فیلتر فعالیت ها را در صفحه اصلی نمایش بده</string> 212 + <string name="settings_show_goals_separately">نمایش اهداف در یک برگه جداگانه</string> 212 213 <string name="settings_allow_multitasking">انجام دادن همزمان فعالیت ها</string> 213 214 <string name="settings_allow_multitasking_hint">اگر غیر فعال باشد شروع تایمر های جدید فعالیت های دیگر را متوقف می کند</string> 214 215 <string name="settings_show_notifications">تایمر را در اعلان ها نمایش بده</string>
+1
core/src/main/res/values-fr/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">Inverser l\'ordre des enregistrements</string> 210 210 <string name="settings_days_in_calendar">Jours dans le calendrier</string> 211 211 <string name="settings_show_activity_filters">Afficher les filtres d\'activité sur l\'écran principal</string> 212 + <string name="settings_show_goals_separately">Afficher les objectifs dans un onglet séparé</string> 212 213 <string name="settings_allow_multitasking">Autoriser le multitâche</string> 213 214 <string name="settings_allow_multitasking_hint">Si désactivé, le démarrage d\'une nouvelle activité arrêtera automatiquement les autres activités en cours.</string> 214 215 <string name="settings_show_notifications">Afficher les notifications d\'activité</string>
+1
core/src/main/res/values-hi/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">अभिलेखों का उल्टा क्रम</string> 210 210 <string name="settings_days_in_calendar">कैलेंडर में दिन</string> 211 211 <string name="settings_show_activity_filters">मुख्य स्क्रीन पर गतिविधि फ़िल्टर दिखाएं</string> 212 + <string name="settings_show_goals_separately">एक अलग टैब पर लक्ष्य दिखाएँ</string> 212 213 <string name="settings_allow_multitasking">मल्टीटास्किंग की अनुमति दें</string> 213 214 <string name="settings_allow_multitasking_hint">यदि बंद हे तो नया टाइमर शुरू होने पर यह अन्य स्वचालित गतिविधियों को रोक देगा।</string> 214 215 <string name="settings_show_notifications">टाइमर नोटिफिकेशन दिखाएं</string>
+1
core/src/main/res/values-in/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">Urutan terbalik catatan</string> 210 210 <string name="settings_days_in_calendar">Hari dalam kalender</string> 211 211 <string name="settings_show_activity_filters">Tampilkan filter aktivitas di layar utama</string> 212 + <string name="settings_show_goals_separately">Tampilkan sasaran di tab terpisah</string> 212 213 <string name="settings_allow_multitasking">Izinkan tugas ganda</string> 213 214 <string name="settings_allow_multitasking_hint">Jika dinonaktifkan memulai pewaktu baru akan secara otomatis menghentikan kegiatan-kegiatan lain.</string> 214 215 <string name="settings_show_notifications">Tampilkan notifikasi pewaktu</string>
+1
core/src/main/res/values-it/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">Ordine inverso dei record</string> 210 210 <string name="settings_days_in_calendar">Giorni in calendario</string> 211 211 <string name="settings_show_activity_filters">Mostra i filtri delle attività sulla schermata principale</string> 212 + <string name="settings_show_goals_separately">Mostra gli obiettivi in una scheda separata</string> 212 213 <string name="settings_allow_multitasking">Abilita multitasking</string> 213 214 <string name="settings_allow_multitasking_hint">Se disabilitato, avviare un nuovo timer fermerà automaticamente le altre attività.</string> 214 215 <string name="settings_show_notifications">Mostra le notifiche relative al timer.</string>
+1
core/src/main/res/values-ja/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">レコードの逆順</string> 210 210 <string name="settings_days_in_calendar">カレンダーの日数</string> 211 211 <string name="settings_show_activity_filters">メイン画面にアクティビティ フィルタを表示する</string> 212 + <string name="settings_show_goals_separately">別のタブに目標を表示する</string> 212 213 <string name="settings_allow_multitasking">マルチタスクを可能にする</string> 213 214 <string name="settings_allow_multitasking_hint">無効の場合、新しいタイマーを開始すると、他のアクティビティが自動的に停止します。</string> 214 215 <string name="settings_show_notifications">タイマーの通知を表示する</string>
+1
core/src/main/res/values-nl/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">Omgekeerde volgorde van records</string> 210 210 <string name="settings_days_in_calendar">Dagen in kalender</string> 211 211 <string name="settings_show_activity_filters">Activiteitsfilters weergeven op hoofdscherm</string> 212 + <string name="settings_show_goals_separately">Toon doelen op een apart tabblad</string> 212 213 <string name="settings_allow_multitasking">Multitasking toestaan</string> 213 214 <string name="settings_allow_multitasking_hint">Indien uitgeschakeld, zal het starten van een nieuwe timer automatisch andere activiteiten stoppen.</string> 214 215 <string name="settings_show_notifications">Timer-meldingen tonen</string>
+1
core/src/main/res/values-pt/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">Ordem inversa dos registros</string> 210 210 <string name="settings_days_in_calendar">Dias no calendário</string> 211 211 <string name="settings_show_activity_filters">Mostrar filtros de atividade na tela principal</string> 212 + <string name="settings_show_goals_separately">Mostrar metas em uma guia separada</string> 212 213 <string name="settings_allow_multitasking">Permitir atividades simultâneas</string> 213 214 <string name="settings_allow_multitasking_hint">Se desativado, iniciar um novo timer interromperá automaticamente outras atividades.</string> 214 215 <string name="settings_show_notifications">Mostrar notificações de timer</string>
+1
core/src/main/res/values-ru/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">Обратный порядок записей</string> 210 210 <string name="settings_days_in_calendar">Дней в календаре</string> 211 211 <string name="settings_show_activity_filters">Показывать фильтры активности на главном экране</string> 212 + <string name="settings_show_goals_separately">Показывать цели на отдельной вкладке</string> 212 213 <string name="settings_allow_multitasking">Разрешить многозадачность</string> 213 214 <string name="settings_allow_multitasking_hint">Если отключено, запуск нового таймера автоматически остановит другие активности.</string> 214 215 <string name="settings_show_notifications">Показывать уведомления таймера</string>
+1
core/src/main/res/values-sv/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">Byt ordning på händelser</string> 210 210 <string name="settings_days_in_calendar">Dagar i kalendern</string> 211 211 <string name="settings_show_activity_filters">Visa aktivitetsfilter på huvudskärmen</string> 212 + <string name="settings_show_goals_separately">Visa mål på en separat flik</string> 212 213 <string name="settings_allow_multitasking">Tillåt multitasking</string> 213 214 <string name="settings_allow_multitasking_hint">Om inaktiverad så stoppas pågående aktivitet vid start av annan aktivitet.</string> 214 215 <string name="settings_show_notifications">Visa aktivitetsaviseringar</string>
+1
core/src/main/res/values-tr/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">Kayıtların ters sırası</string> 210 210 <string name="settings_days_in_calendar">Takvimdeki günler</string> 211 211 <string name="settings_show_activity_filters">Aktivite filtrelerini ana ekranda göster</string> 212 + <string name="settings_show_goals_separately">Hedefleri ayrı bir sekmede göster</string> 212 213 <string name="settings_allow_multitasking">Çoklu göreve izin ver</string> 213 214 <string name="settings_allow_multitasking_hint">Devre dışı bırakılırsa, yeni zamanlayıcı başlatmak diğer aktivitelerin zamanlayıcısını otomatik olarak durdurur.</string> 214 215 <string name="settings_show_notifications">Zamanlayıcı bildirimlerini göster</string>
+1
core/src/main/res/values-uk/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">Зворотний порядок записів</string> 210 210 <string name="settings_days_in_calendar">Дні в календарі</string> 211 211 <string name="settings_show_activity_filters">Показувати фільтри активності на головному екрані</string> 212 + <string name="settings_show_goals_separately">Показати цілі на окремій вкладці</string> 212 213 <string name="settings_allow_multitasking">Дозволити багатозадачність</string> 213 214 <string name="settings_allow_multitasking_hint">Якщо вимкнено, запуск нового таймера автоматично зупинить інші дії.</string> 214 215 <string name="settings_show_notifications">Показувати сповіщення таймера</string>
+1
core/src/main/res/values-zh-rTW/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">反轉記錄順序</string> 210 210 <string name="settings_days_in_calendar">日曆中的天數</string> 211 211 <string name="settings_show_activity_filters">主視窗上顯示活動過濾器</string> 212 + <string name="settings_show_goals_separately">在單獨的選項卡上顯示目標</string> 212 213 <string name="settings_allow_multitasking">允許多任務</string> 213 214 <string name="settings_allow_multitasking_hint">如果禁用啟動新計時器將自動停止其他活動。</string> 214 215 <string name="settings_show_notifications">顯示計時器提醒</string>
+1
core/src/main/res/values-zh/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">倒序记录</string> 210 210 <string name="settings_days_in_calendar">日历中的天数</string> 211 211 <string name="settings_show_activity_filters">在主屏幕上显示活动过滤器</string> 212 + <string name="settings_show_goals_separately">在单独的选项卡上显示目标</string> 212 213 <string name="settings_allow_multitasking">允许同时对多个活动进行追踪</string> 213 214 <string name="settings_allow_multitasking_hint">如果不启用,启动新活动追踪时,将自动停止追踪其他活动。</string> 214 215 <string name="settings_show_notifications">显示计时通知</string>
+1
core/src/main/res/values/strings.xml
··· 209 209 <string name="settings_reverse_order_in_calendar">Reverse order of records</string> 210 210 <string name="settings_days_in_calendar">Days in calendar</string> 211 211 <string name="settings_show_activity_filters">Show activity filters on main screen</string> 212 + <string name="settings_show_goals_separately">Show goals on a separate tab</string> 212 213 <string name="settings_allow_multitasking">Allow multitasking</string> 213 214 <string name="settings_allow_multitasking_hint">If disabled starting new timer will automatically stop other activities.</string> 214 215 <string name="settings_show_notifications">Show timer notifications</string>
+5
data_local/src/main/java/com/example/util/simpletimetracker/data_local/repo/PrefsRepoImpl.kt
··· 97 97 KEY_SHOW_ACTIVITY_FILTERS, false 98 98 ) 99 99 100 + override var showGoalsSeparately: Boolean by prefs.delegate( 101 + KEY_SHOW_GOALS_SEPARATELY, false 102 + ) 103 + 100 104 override var allowMultitasking: Boolean by prefs.delegate( 101 105 KEY_ALLOW_MULTITASKING, true 102 106 ) ··· 386 390 private const val KEY_REVERSE_ORDER_IN_CALENDAR = "reverseOrderInCalendar" 387 391 private const val KEY_DAYS_IN_CALENDAR = "daysInCalendar" 388 392 private const val KEY_SHOW_ACTIVITY_FILTERS = "showActivityFilters" 393 + private const val KEY_SHOW_GOALS_SEPARATELY = "showGoalsSeparately" 389 394 private const val KEY_ALLOW_MULTITASKING = "allowMultitasking" 390 395 private const val KEY_SHOW_NOTIFICATIONS = "showNotifications" 391 396 private const val KEY_SHOW_NOTIFICATIONS_CONTROLS = "showNotificationsControls"
+8
domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/PrefsInteractor.kt
··· 202 202 prefsRepo.showActivityFilters = isEnabled 203 203 } 204 204 205 + suspend fun getShowGoalsSeparately(): Boolean = withContext(Dispatchers.IO) { 206 + prefsRepo.showGoalsSeparately 207 + } 208 + 209 + suspend fun setShowGoalsSeparately(isEnabled: Boolean) = withContext(Dispatchers.IO) { 210 + prefsRepo.showGoalsSeparately = isEnabled 211 + } 212 + 205 213 suspend fun getAllowMultitasking(): Boolean = withContext(Dispatchers.IO) { 206 214 prefsRepo.allowMultitasking 207 215 }
+2
domain/src/main/java/com/example/util/simpletimetracker/domain/repo/PrefsRepo.kt
··· 39 39 40 40 var showActivityFilters: Boolean 41 41 42 + var showGoalsSeparately: Boolean 43 + 42 44 var allowMultitasking: Boolean 43 45 44 46 var showNotifications: Boolean
+1
features/feature_goals/.gitignore
··· 1 + /build
+17
features/feature_goals/build.gradle.kts
··· 1 + import com.example.util.simpletimetracker.Deps 2 + import com.example.util.simpletimetracker.applyAndroidLibrary 3 + 4 + plugins { 5 + id("com.android.library") 6 + id("kotlin-android") 7 + id("kotlin-kapt") 8 + id("dagger.hilt.android.plugin") 9 + } 10 + 11 + applyAndroidLibrary() 12 + 13 + dependencies { 14 + implementation(project(":core")) 15 + implementation(Deps.Google.dagger) 16 + kapt(Deps.Kapt.dagger) 17 + }
+5
features/feature_goals/src/main/AndroidManifest.xml
··· 1 + <manifest xmlns:android="http://schemas.android.com/apk/res/android" 2 + package="com.example.util.simpletimetracker.feature_goals"> 3 + 4 + <application android:theme="@style/AppTheme" /> 5 + </manifest>
+135
features/feature_goals/src/main/java/com/example/util/simpletimetracker/feature_goals/interactor/GoalsViewDataInteractor.kt
··· 1 + package com.example.util.simpletimetracker.feature_goals.interactor 2 + 3 + import com.example.util.simpletimetracker.core.interactor.StatisticsMediator 4 + import com.example.util.simpletimetracker.core.mapper.GoalViewDataMapper 5 + import com.example.util.simpletimetracker.core.repo.ResourceRepo 6 + import com.example.util.simpletimetracker.core.viewData.StatisticsDataHolder 7 + import com.example.util.simpletimetracker.domain.interactor.PrefsInteractor 8 + import com.example.util.simpletimetracker.domain.interactor.RecordTypeGoalInteractor 9 + import com.example.util.simpletimetracker.domain.interactor.RecordTypeInteractor 10 + import com.example.util.simpletimetracker.domain.model.ChartFilterType 11 + import com.example.util.simpletimetracker.domain.model.RangeLength 12 + import com.example.util.simpletimetracker.domain.model.RecordType 13 + import com.example.util.simpletimetracker.domain.model.RecordTypeGoal 14 + import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType 15 + import com.example.util.simpletimetracker.feature_base_adapter.empty.EmptyViewData 16 + import com.example.util.simpletimetracker.feature_base_adapter.hint.HintViewData 17 + import com.example.util.simpletimetracker.feature_goals.R 18 + import kotlinx.coroutines.Dispatchers 19 + import kotlinx.coroutines.withContext 20 + import javax.inject.Inject 21 + 22 + class GoalsViewDataInteractor @Inject constructor( 23 + private val recordTypeInteractor: RecordTypeInteractor, 24 + private val recordTypeGoalInteractor: RecordTypeGoalInteractor, 25 + private val statisticsMediator: StatisticsMediator, 26 + private val prefsInteractor: PrefsInteractor, 27 + private val goalViewDataMapper: GoalViewDataMapper, 28 + private val resourceRepo: ResourceRepo, 29 + ) { 30 + 31 + suspend fun getViewData(): List<ViewHolderType> = withContext(Dispatchers.Default) { 32 + val filterType = prefsInteractor.getChartFilterType() 33 + val isDarkTheme = prefsInteractor.getDarkMode() 34 + val useProportionalMinutes = prefsInteractor.getUseProportionalMinutes() 35 + val showSeconds = prefsInteractor.getShowSeconds() 36 + val types = recordTypeInteractor.getAll().associateBy(RecordType::id) 37 + val goals = recordTypeGoalInteractor.getAll() 38 + val goalsMap = goals.groupBy(RecordTypeGoal::typeId) 39 + 40 + val dataHolders = statisticsMediator.getDataHolders( 41 + filterType = filterType, 42 + types = types, 43 + goals = goalsMap, 44 + ) 45 + 46 + val items = goals 47 + .asSequence() 48 + .map(RecordTypeGoal::range) 49 + .toSet() 50 + .sortedBy { 51 + when (it) { 52 + is RecordTypeGoal.Range.Session -> 0 53 + is RecordTypeGoal.Range.Daily -> 1 54 + is RecordTypeGoal.Range.Weekly -> 2 55 + is RecordTypeGoal.Range.Monthly -> 3 56 + } 57 + } 58 + .mapNotNull { 59 + when (it) { 60 + // No point in statistics for session goals. 61 + is RecordTypeGoal.Range.Session -> return@mapNotNull null 62 + is RecordTypeGoal.Range.Daily -> RangeLength.Day 63 + is RecordTypeGoal.Range.Weekly -> RangeLength.Week 64 + is RecordTypeGoal.Range.Monthly -> RangeLength.Month 65 + } 66 + } 67 + .map { rangeLength -> 68 + getViewDataForRange( 69 + filterType = filterType, 70 + rangeLength = rangeLength, 71 + dataHolders = dataHolders, 72 + isDarkTheme = isDarkTheme, 73 + useProportionalMinutes = useProportionalMinutes, 74 + showSeconds = showSeconds, 75 + ) 76 + } 77 + .toList() 78 + 79 + 80 + return@withContext items 81 + .takeUnless { it.isEmpty() } 82 + ?.flatten() 83 + ?: mapToEmpty() 84 + } 85 + 86 + private suspend fun getViewDataForRange( 87 + filterType: ChartFilterType, 88 + rangeLength: RangeLength, 89 + dataHolders: Map<Long, StatisticsDataHolder>, 90 + isDarkTheme: Boolean, 91 + useProportionalMinutes: Boolean, 92 + showSeconds: Boolean, 93 + ): List<ViewHolderType> { 94 + val statistics = statisticsMediator.getStatistics( 95 + filterType = filterType, 96 + filteredIds = emptyList(), 97 + rangeLength = rangeLength, 98 + shift = 0, 99 + ) 100 + val goalsStatistics = statisticsMediator.getGoals( 101 + statistics = statistics, 102 + rangeLength = rangeLength, 103 + filterType = filterType, 104 + ) 105 + val result = mutableListOf<ViewHolderType>() 106 + val items = goalViewDataMapper.mapList( 107 + rangeLength = rangeLength, 108 + statistics = goalsStatistics, 109 + data = dataHolders, 110 + isDarkTheme = isDarkTheme, 111 + useProportionalMinutes = useProportionalMinutes, 112 + showSeconds = showSeconds, 113 + ) 114 + 115 + if (items.isNotEmpty()) { 116 + val title = when (rangeLength) { 117 + is RangeLength.Day -> R.string.title_today 118 + is RangeLength.Week -> R.string.title_this_week 119 + is RangeLength.Month -> R.string.title_this_month 120 + else -> return emptyList() 121 + }.let(resourceRepo::getString) 122 + HintViewData(title).let(result::add) 123 + 124 + result.addAll(items) 125 + } 126 + 127 + return result 128 + } 129 + 130 + private fun mapToEmpty(): List<ViewHolderType> { 131 + return EmptyViewData( 132 + message = R.string.records_empty.let(resourceRepo::getString), 133 + ).let(::listOf) 134 + } 135 + }
+82
features/feature_goals/src/main/java/com/example/util/simpletimetracker/feature_goals/view/GoalsFragment.kt
··· 1 + package com.example.util.simpletimetracker.feature_goals.view 2 + 3 + import android.view.LayoutInflater 4 + import android.view.ViewGroup 5 + import androidx.fragment.app.activityViewModels 6 + import androidx.fragment.app.viewModels 7 + import androidx.recyclerview.widget.LinearLayoutManager 8 + import com.example.util.simpletimetracker.core.base.BaseFragment 9 + import com.example.util.simpletimetracker.core.di.BaseViewModelFactory 10 + import com.example.util.simpletimetracker.core.sharedViewModel.MainTabsViewModel 11 + import com.example.util.simpletimetracker.feature_base_adapter.BaseRecyclerAdapter 12 + import com.example.util.simpletimetracker.feature_base_adapter.hint.createHintAdapterDelegate 13 + import com.example.util.simpletimetracker.feature_base_adapter.loader.createLoaderAdapterDelegate 14 + import com.example.util.simpletimetracker.feature_base_adapter.statisticsGoal.createStatisticsGoalAdapterDelegate 15 + import com.example.util.simpletimetracker.feature_goals.viewModel.GoalsViewModel 16 + import dagger.hilt.android.AndroidEntryPoint 17 + import javax.inject.Inject 18 + import com.example.util.simpletimetracker.feature_goals.databinding.GoalsFragmentBinding as Binding 19 + 20 + @AndroidEntryPoint 21 + class GoalsFragment : BaseFragment<Binding>() { 22 + 23 + override val inflater: (LayoutInflater, ViewGroup?, Boolean) -> Binding = 24 + Binding::inflate 25 + 26 + @Inject 27 + lateinit var mainTabsViewModelFactory: BaseViewModelFactory<MainTabsViewModel> 28 + 29 + private val viewModel: GoalsViewModel by viewModels() 30 + private val mainTabsViewModel: MainTabsViewModel by activityViewModels( 31 + factoryProducer = { mainTabsViewModelFactory }, 32 + ) 33 + 34 + private val goalsAdapter: BaseRecyclerAdapter by lazy { 35 + BaseRecyclerAdapter( 36 + createLoaderAdapterDelegate(), 37 + createHintAdapterDelegate(), 38 + createStatisticsGoalAdapterDelegate(), 39 + ) 40 + } 41 + 42 + override fun initUi(): Unit = with(binding) { 43 + parentFragment?.postponeEnterTransition() 44 + 45 + rvGoalsList.apply { 46 + layoutManager = LinearLayoutManager(context) 47 + adapter = goalsAdapter 48 + } 49 + 50 + setOnPreDrawListener { 51 + parentFragment?.startPostponedEnterTransition() 52 + } 53 + } 54 + 55 + override fun initViewModel() = with(binding) { 56 + with(viewModel) { 57 + goals.observe(goalsAdapter::replace) 58 + resetScreen.observe { 59 + rvGoalsList.smoothScrollToPosition(0) 60 + mainTabsViewModel.onHandled() 61 + } 62 + } 63 + with(mainTabsViewModel) { 64 + tabReselected.observe(viewModel::onTabReselected) 65 + } 66 + } 67 + 68 + override fun onResume() { 69 + super.onResume() 70 + viewModel.onVisible() 71 + } 72 + 73 + override fun onPause() { 74 + super.onPause() 75 + viewModel.onHidden() 76 + } 77 + 78 + 79 + companion object { 80 + fun newInstance(): GoalsFragment = GoalsFragment() 81 + } 82 + }
+78
features/feature_goals/src/main/java/com/example/util/simpletimetracker/feature_goals/viewModel/GoalsViewModel.kt
··· 1 + package com.example.util.simpletimetracker.feature_goals.viewModel 2 + 3 + import androidx.lifecycle.LiveData 4 + import androidx.lifecycle.MutableLiveData 5 + import androidx.lifecycle.ViewModel 6 + import androidx.lifecycle.viewModelScope 7 + import com.example.util.simpletimetracker.core.base.SingleLiveEvent 8 + import com.example.util.simpletimetracker.core.extension.set 9 + import com.example.util.simpletimetracker.core.model.NavigationTab 10 + import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType 11 + import com.example.util.simpletimetracker.feature_base_adapter.loader.LoaderViewData 12 + import com.example.util.simpletimetracker.feature_goals.interactor.GoalsViewDataInteractor 13 + import dagger.hilt.android.lifecycle.HiltViewModel 14 + import kotlinx.coroutines.Job 15 + import kotlinx.coroutines.cancelAndJoin 16 + import kotlinx.coroutines.delay 17 + import kotlinx.coroutines.isActive 18 + import kotlinx.coroutines.launch 19 + import javax.inject.Inject 20 + 21 + @HiltViewModel 22 + class GoalsViewModel @Inject constructor( 23 + private val goalsViewDataInteractor: GoalsViewDataInteractor, 24 + ) : ViewModel() { 25 + 26 + val goals: LiveData<List<ViewHolderType>> by lazy { 27 + MutableLiveData(listOf(LoaderViewData() as ViewHolderType)) 28 + } 29 + val resetScreen: SingleLiveEvent<Unit> = SingleLiveEvent() 30 + 31 + private var isVisible: Boolean = false 32 + private var timerJob: Job? = null 33 + 34 + fun onVisible() { 35 + isVisible = true 36 + startUpdate() 37 + } 38 + 39 + fun onHidden() { 40 + isVisible = false 41 + stopUpdate() 42 + } 43 + 44 + fun onTabReselected(tab: NavigationTab?) { 45 + if (isVisible && tab is NavigationTab.Goals) { 46 + resetScreen.set(Unit) 47 + } 48 + } 49 + 50 + private fun updateStatistics() = viewModelScope.launch { 51 + val data = loadStatisticsViewData() 52 + goals.set(data) 53 + } 54 + 55 + private suspend fun loadStatisticsViewData(): List<ViewHolderType> { 56 + return goalsViewDataInteractor.getViewData() 57 + } 58 + 59 + private fun startUpdate() { 60 + timerJob = viewModelScope.launch { 61 + timerJob?.cancelAndJoin() 62 + while (isActive) { 63 + updateStatistics() 64 + delay(TIMER_UPDATE) 65 + } 66 + } 67 + } 68 + 69 + private fun stopUpdate() { 70 + viewModelScope.launch { 71 + timerJob?.cancelAndJoin() 72 + } 73 + } 74 + 75 + companion object { 76 + private const val TIMER_UPDATE = 1000L 77 + } 78 + }
+8
features/feature_goals/src/main/res/layout/goals_fragment.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" 3 + xmlns:tools="http://schemas.android.com/tools" 4 + android:id="@+id/rvGoalsList" 5 + android:layout_width="match_parent" 6 + android:layout_height="match_parent" 7 + tools:itemCount="15" 8 + tools:listitem="@layout/item_statistics_goal_layout" />
+1
features/feature_main/build.gradle.kts
··· 16 16 implementation(project(":feature_records")) 17 17 implementation(project(":feature_statistics")) 18 18 implementation(project(":feature_settings")) 19 + implementation(project(":feature_goals")) 19 20 implementation(Deps.Google.dagger) 20 21 kapt(Deps.Kapt.dagger) 21 22 }
+15 -7
features/feature_main/src/main/java/com/example/util/simpletimetracker/feature_main/adapter/MainContentAdapter.kt
··· 2 2 3 3 import androidx.fragment.app.Fragment 4 4 import androidx.viewpager2.adapter.FragmentStateAdapter 5 + import com.example.util.simpletimetracker.core.model.NavigationTab 6 + import com.example.util.simpletimetracker.feature_goals.view.GoalsFragment 5 7 import com.example.util.simpletimetracker.feature_records.view.RecordsContainerFragment 6 8 import com.example.util.simpletimetracker.feature_running_records.view.RunningRecordsFragment 7 9 import com.example.util.simpletimetracker.feature_settings.view.SettingsFragment 8 10 import com.example.util.simpletimetracker.feature_statistics.view.StatisticsContainerFragment 9 11 10 - class MainContentAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { 12 + class MainContentAdapter( 13 + fragment: Fragment, 14 + tabs: List<NavigationTab>, 15 + ) : FragmentStateAdapter(fragment) { 11 16 12 - private val fragments = listOf( 13 - lazy { RunningRecordsFragment.newInstance() }, 14 - lazy { RecordsContainerFragment.newInstance() }, 15 - lazy { StatisticsContainerFragment.newInstance() }, 16 - lazy { SettingsFragment.newInstance() } 17 - ) 17 + private val fragments = tabs.map { 18 + when (it) { 19 + is NavigationTab.RunningRecords -> lazy { RunningRecordsFragment.newInstance() } 20 + is NavigationTab.Records -> lazy { RecordsContainerFragment.newInstance() } 21 + is NavigationTab.Statistics -> lazy { StatisticsContainerFragment.newInstance() } 22 + is NavigationTab.Settings -> lazy { SettingsFragment.newInstance() } 23 + is NavigationTab.Goals -> lazy { GoalsFragment.newInstance() } 24 + } 25 + } 18 26 19 27 override fun getItemCount(): Int = 20 28 fragments.size
-50
features/feature_main/src/main/java/com/example/util/simpletimetracker/feature_main/mapper/MainMapper.kt
··· 1 - package com.example.util.simpletimetracker.feature_main.mapper 2 - 3 - import androidx.annotation.DrawableRes 4 - import com.example.util.simpletimetracker.core.model.NavigationTab 5 - import com.example.util.simpletimetracker.core.utils.SHORTCUT_NAVIGATION_RECORDS 6 - import com.example.util.simpletimetracker.core.utils.SHORTCUT_NAVIGATION_SETTINGS 7 - import com.example.util.simpletimetracker.core.utils.SHORTCUT_NAVIGATION_STATISTICS 8 - import com.example.util.simpletimetracker.feature_main.R 9 - import javax.inject.Inject 10 - 11 - class MainMapper @Inject constructor() { 12 - 13 - fun mapPositionToTab(position: Int): NavigationTab? { 14 - return when (position) { 15 - 0 -> NavigationTab.RunningRecords 16 - 1 -> NavigationTab.Records 17 - 2 -> NavigationTab.Statistics 18 - 3 -> NavigationTab.Settings 19 - else -> null 20 - } 21 - } 22 - 23 - fun mapTabToPosition(tab: NavigationTab): Int { 24 - return when (tab) { 25 - NavigationTab.RunningRecords -> 0 26 - NavigationTab.Records -> 1 27 - NavigationTab.Statistics -> 2 28 - NavigationTab.Settings -> 3 29 - } 30 - } 31 - 32 - fun mapNavigationToTab(value: String): NavigationTab? { 33 - return when (value) { 34 - SHORTCUT_NAVIGATION_RECORDS -> NavigationTab.Records 35 - SHORTCUT_NAVIGATION_STATISTICS -> NavigationTab.Statistics 36 - SHORTCUT_NAVIGATION_SETTINGS -> NavigationTab.Settings 37 - else -> null 38 - } 39 - } 40 - 41 - @DrawableRes fun mapToIcon(tab: NavigationTab?): Int { 42 - return when (tab) { 43 - NavigationTab.RunningRecords -> R.drawable.ic_tab_running_records 44 - NavigationTab.Records -> R.drawable.ic_tab_records 45 - NavigationTab.Statistics -> R.drawable.ic_tab_statistics 46 - NavigationTab.Settings -> R.drawable.ic_tab_settings 47 - else -> R.drawable.unknown 48 - } 49 - } 50 - }
+79
features/feature_main/src/main/java/com/example/util/simpletimetracker/feature_main/provider/MainTabsProvider.kt
··· 1 + package com.example.util.simpletimetracker.feature_main.provider 2 + 3 + import androidx.annotation.DrawableRes 4 + import com.example.util.simpletimetracker.core.model.NavigationTab 5 + import com.example.util.simpletimetracker.core.utils.SHORTCUT_NAVIGATION_RECORDS 6 + import com.example.util.simpletimetracker.core.utils.SHORTCUT_NAVIGATION_SETTINGS 7 + import com.example.util.simpletimetracker.core.utils.SHORTCUT_NAVIGATION_STATISTICS 8 + import com.example.util.simpletimetracker.domain.extension.orZero 9 + import com.example.util.simpletimetracker.domain.interactor.PrefsInteractor 10 + import com.example.util.simpletimetracker.feature_main.R 11 + import kotlinx.coroutines.runBlocking 12 + import javax.inject.Inject 13 + 14 + class MainTabsProvider @Inject constructor( 15 + private val prefsInteractor: PrefsInteractor, 16 + ) { 17 + 18 + val tabsList: List<NavigationTab> by lazy { loadTabsList() } 19 + val mainTab: NavigationTab by lazy { loadMainTab() } 20 + 21 + fun mapPositionToTab(position: Int): NavigationTab? { 22 + return tabsList.getOrNull(position) 23 + } 24 + 25 + fun mapTabToPosition(tab: NavigationTab): Int { 26 + return tabsList.indexOf(tab).takeUnless { it == -1 }.orZero() 27 + } 28 + 29 + @DrawableRes 30 + fun mapPositionToIcon(position: Int): Int { 31 + return position 32 + .let(::mapPositionToTab) 33 + .let(::mapToIcon) 34 + } 35 + 36 + fun mapNavigationToPosition(value: String): Int? { 37 + return mapNavigationToTab(value)?.let(::mapTabToPosition) 38 + } 39 + 40 + @DrawableRes 41 + fun mapToIcon(tab: NavigationTab?): Int { 42 + if (tab == null) return R.drawable.unknown 43 + 44 + return when (tab) { 45 + NavigationTab.RunningRecords -> R.drawable.ic_tab_running_records 46 + NavigationTab.Records -> R.drawable.ic_tab_records 47 + NavigationTab.Statistics -> R.drawable.ic_tab_statistics 48 + NavigationTab.Settings -> R.drawable.ic_tab_settings 49 + NavigationTab.Goals -> R.drawable.ic_tab_goals 50 + } 51 + } 52 + 53 + private fun mapNavigationToTab(value: String): NavigationTab? { 54 + return when (value) { 55 + SHORTCUT_NAVIGATION_RECORDS -> NavigationTab.Records 56 + SHORTCUT_NAVIGATION_STATISTICS -> NavigationTab.Statistics 57 + SHORTCUT_NAVIGATION_SETTINGS -> NavigationTab.Settings 58 + else -> null 59 + } 60 + } 61 + 62 + private fun loadTabsList(): List<NavigationTab> { 63 + val showGoals = runBlocking { prefsInteractor.getShowGoalsSeparately() } 64 + 65 + return listOfNotNull( 66 + NavigationTab.RunningRecords, 67 + NavigationTab.Records, 68 + NavigationTab.Goals.takeIf { showGoals }, 69 + NavigationTab.Statistics, 70 + NavigationTab.Settings, 71 + ) 72 + } 73 + 74 + private fun loadMainTab(): NavigationTab { 75 + return runBlocking { 76 + NavigationTab.RunningRecords 77 + } 78 + } 79 + }
+30 -29
features/feature_main/src/main/java/com/example/util/simpletimetracker/feature_main/view/MainFragment.kt
··· 13 13 import com.example.util.simpletimetracker.core.base.BaseFragment 14 14 import com.example.util.simpletimetracker.core.di.BaseViewModelFactory 15 15 import com.example.util.simpletimetracker.core.extension.addOnPageChangeCallback 16 - import com.example.util.simpletimetracker.core.model.NavigationTab 17 16 import com.example.util.simpletimetracker.core.sharedViewModel.MainTabsViewModel 18 17 import com.example.util.simpletimetracker.core.utils.SHORTCUT_NAVIGATION_KEY 19 18 import com.example.util.simpletimetracker.core.view.SafeFragmentStateAdapter ··· 21 20 import com.example.util.simpletimetracker.feature_base_adapter.extensions.getThemedAttr 22 21 import com.example.util.simpletimetracker.feature_main.R 23 22 import com.example.util.simpletimetracker.feature_main.adapter.MainContentAdapter 24 - import com.example.util.simpletimetracker.feature_main.mapper.MainMapper 23 + import com.example.util.simpletimetracker.feature_main.provider.MainTabsProvider 25 24 import com.example.util.simpletimetracker.feature_main.viewModel.MainViewModel 26 25 import com.google.android.material.tabs.TabLayout 27 26 import com.google.android.material.tabs.TabLayoutMediator ··· 39 38 lateinit var mainTabsViewModelFactory: BaseViewModelFactory<MainTabsViewModel> 40 39 41 40 @Inject 42 - lateinit var mainMapper: MainMapper 41 + lateinit var mainTabsProvider: MainTabsProvider 43 42 44 43 private val viewModel: MainViewModel by viewModels() 45 44 private val mainTabsViewModel: MainTabsViewModel by activityViewModels( 46 - factoryProducer = { mainTabsViewModelFactory } 45 + factoryProducer = { mainTabsViewModelFactory }, 47 46 ) 48 47 49 48 private val selectedColorFilter by lazy { getColorFilter(R.attr.appTabSelectedColor) } ··· 51 50 private val backPressedCallback: OnBackPressedCallback = getOnBackPressedCallback() 52 51 private var shortcutNavigationHandled = false 53 52 private val mainPagePosition by lazy { 54 - NavigationTab.RunningRecords.let(mainMapper::mapTabToPosition) 53 + mainTabsProvider.mainTab.let(mainTabsProvider::mapTabToPosition) 55 54 } 56 55 57 56 override fun initUi() { ··· 65 64 } 66 65 67 66 private fun setupPager() = with(binding) { 68 - mainPager.adapter = SafeFragmentStateAdapter(MainContentAdapter(this@MainFragment)) 67 + mainPager.adapter = SafeFragmentStateAdapter( 68 + MainContentAdapter( 69 + fragment = this@MainFragment, 70 + tabs = mainTabsProvider.tabsList, 71 + ), 72 + ) 69 73 mainPager.offscreenPageLimit = 3 // Same as number of pages to avoid recreating. 70 74 mainPager.addOnPageChangeCallback(lifecycleOwner = this@MainFragment) { state -> 71 75 mainTabsViewModel.onScrollStateChanged( 72 - isScrolling = state != ViewPager2.SCROLL_STATE_IDLE 76 + isScrolling = state != ViewPager2.SCROLL_STATE_IDLE, 73 77 ) 74 78 } 75 79 76 80 TabLayoutMediator(mainTabs, mainPager) { tab, position -> 77 - position 78 - .let(mainMapper::mapPositionToTab) 79 - .let(mainMapper::mapToIcon) 80 - .let(tab::setIcon) 81 - 81 + position.let(mainTabsProvider::mapPositionToIcon).let(tab::setIcon) 82 82 tab.icon?.colorFilter = if (position == mainPagePosition) { 83 83 selectedColorFilter 84 84 } else { ··· 86 86 } 87 87 }.attach() 88 88 89 - mainTabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { 90 - override fun onTabReselected(tab: TabLayout.Tab?) { 91 - tab?.position 92 - ?.let(mainMapper::mapPositionToTab) 93 - ?.let(mainTabsViewModel::onTabReselected) 94 - } 89 + mainTabs.addOnTabSelectedListener( 90 + object : TabLayout.OnTabSelectedListener { 91 + override fun onTabReselected(tab: TabLayout.Tab?) { 92 + tab?.position 93 + ?.let(mainTabsProvider::mapPositionToTab) 94 + ?.let(mainTabsViewModel::onTabReselected) 95 + } 95 96 96 - override fun onTabUnselected(tab: TabLayout.Tab?) { 97 - tab?.icon?.colorFilter = unselectedColorFilter 98 - } 97 + override fun onTabUnselected(tab: TabLayout.Tab?) { 98 + tab?.icon?.colorFilter = unselectedColorFilter 99 + } 99 100 100 - override fun onTabSelected(tab: TabLayout.Tab?) { 101 - tab?.icon?.colorFilter = selectedColorFilter 102 - backPressedCallback.isEnabled = tab?.position.orZero() != mainPagePosition 103 - } 104 - }) 101 + override fun onTabSelected(tab: TabLayout.Tab?) { 102 + tab?.icon?.colorFilter = selectedColorFilter 103 + backPressedCallback.isEnabled = tab?.position.orZero() != mainPagePosition 104 + } 105 + }, 106 + ) 105 107 } 106 108 107 109 private fun getOnBackPressedCallback(): OnBackPressedCallback { ··· 117 119 118 120 activity?.intent?.extras 119 121 ?.getString(SHORTCUT_NAVIGATION_KEY) 120 - ?.let(mainMapper::mapNavigationToTab) 121 - ?.let(mainMapper::mapTabToPosition) 122 + ?.let(mainTabsProvider::mapNavigationToPosition) 122 123 ?.let { 123 124 mainPager.setCurrentItem(it, true) 124 125 shortcutNavigationHandled = true ··· 128 129 private fun getColorFilter(@AttrRes attrRes: Int): ColorFilter? { 129 130 return BlendModeColorFilterCompat.createBlendModeColorFilterCompat( 130 131 requireContext().getThemedAttr(attrRes), 131 - BlendModeCompat.SRC_IN 132 + BlendModeCompat.SRC_IN, 132 133 ) 133 134 } 134 135 }
+9
features/feature_main/src/main/res/drawable/ic_tab_goals.xml
··· 1 + <vector xmlns:android="http://schemas.android.com/apk/res/android" 2 + android:width="24dp" 3 + android:height="24dp" 4 + android:viewportWidth="24" 5 + android:viewportHeight="24"> 6 + <path 7 + android:pathData="M12,4c-4.97,0 -9,4.03 -9,9s4.02,9 9,9c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,20c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7s7,3.13 7,7s-3.13,7 -7,7zM10.54,14.53L8.41,12.4l-1.06,1.06l3.18,3.18l6,-6l-1.06,-1.06l-4.93,4.95z" 8 + android:fillColor="#000000"/> 9 + </vector>
+3
features/feature_settings/src/main/java/com/example/util/simpletimetracker/feature_settings/view/SettingsFragment.kt
··· 114 114 checkboxSettingsShowRecordsCalendar.setOnClick(viewModel::onShowRecordsCalendarClicked) 115 115 checkboxSettingsReverseOrderInCalendar.setOnClick(viewModel::onReverseOrderInCalendarClicked) 116 116 checkboxSettingsShowActivityFilters.setOnClick(viewModel::onShowActivityFiltersClicked) 117 + checkboxSettingsShowGoalsSeparately.setOnClick(viewModel::onShowGoalsSeparatelyClicked) 117 118 checkboxSettingsUseMilitaryTime.setOnClick(viewModel::onUseMilitaryTimeClicked) 118 119 checkboxSettingsUseMonthDayTime.setOnClick(viewModel::onUseMonthDayTimeClicked) 119 120 checkboxSettingsUseProportionalMinutes.setOnClick(viewModel::onUseProportionalMinutesClicked) ··· 187 188 daysInCalendarViewData.observe(::updateDaysInCalendarViewData) 188 189 reverseOrderInCalendarCheckbox.observe(checkboxSettingsReverseOrderInCalendar::setChecked) 189 190 showActivityFiltersCheckbox.observe(checkboxSettingsShowActivityFilters::setChecked) 191 + showGoalsSeparatelyCheckbox.observe(checkboxSettingsShowGoalsSeparately::setChecked) 190 192 useMilitaryTimeCheckbox.observe(checkboxSettingsUseMilitaryTime::setChecked) 191 193 useMonthDayTimeCheckbox.observe(checkboxSettingsUseMonthDayTime::setChecked) 192 194 useProportionalMinutesCheckbox.observe(checkboxSettingsUseProportionalMinutes::setChecked) ··· 265 267 checkboxSettingsShowRecordsCalendar.jumpDrawablesToCurrentState() 266 268 checkboxSettingsReverseOrderInCalendar.jumpDrawablesToCurrentState() 267 269 checkboxSettingsShowActivityFilters.jumpDrawablesToCurrentState() 270 + checkboxSettingsShowGoalsSeparately.jumpDrawablesToCurrentState() 268 271 checkboxSettingsUseMilitaryTime.jumpDrawablesToCurrentState() 269 272 checkboxSettingsUseMonthDayTime.jumpDrawablesToCurrentState() 270 273 checkboxSettingsUseProportionalMinutes.jumpDrawablesToCurrentState()
+18
features/feature_settings/src/main/java/com/example/util/simpletimetracker/feature_settings/viewModel/SettingsViewModel.kt
··· 148 148 } 149 149 } 150 150 151 + val showGoalsSeparatelyCheckbox: LiveData<Boolean> by lazy { 152 + MutableLiveData<Boolean>().let { initial -> 153 + viewModelScope.launch { 154 + initial.value = prefsInteractor.getShowGoalsSeparately() 155 + } 156 + initial 157 + } 158 + } 159 + 151 160 val allowMultitaskingCheckbox: LiveData<Boolean> by lazy { 152 161 MutableLiveData<Boolean>().let { initial -> 153 162 viewModelScope.launch { ··· 591 600 val newValue = !prefsInteractor.getShowActivityFilters() 592 601 prefsInteractor.setShowActivityFilters(newValue) 593 602 (showActivityFiltersCheckbox as MutableLiveData).value = newValue 603 + } 604 + } 605 + 606 + fun onShowGoalsSeparatelyClicked() { 607 + viewModelScope.launch { 608 + val newValue = !prefsInteractor.getShowGoalsSeparately() 609 + prefsInteractor.setShowGoalsSeparately(newValue) 610 + (showGoalsSeparatelyCheckbox as MutableLiveData).value = newValue 611 + router.restartApp() 594 612 } 595 613 } 596 614
+28
features/feature_settings/src/main/res/layout/settings_display_layout.xml
··· 369 369 android:layout_height="wrap_content"> 370 370 371 371 <androidx.appcompat.widget.AppCompatTextView 372 + android:id="@+id/tvSettingsShowGoalsSeparately" 373 + style="@style/SettingsText" 374 + android:layout_width="0dp" 375 + android:layout_height="wrap_content" 376 + android:text="@string/settings_show_goals_separately" 377 + app:layout_constraintEnd_toStartOf="@id/checkboxSettingsShowGoalsSeparately" 378 + app:layout_constraintStart_toStartOf="parent" 379 + app:layout_constraintTop_toTopOf="parent" /> 380 + 381 + <androidx.appcompat.widget.AppCompatCheckBox 382 + android:id="@+id/checkboxSettingsShowGoalsSeparately" 383 + android:layout_width="wrap_content" 384 + android:layout_height="0dp" 385 + app:layout_constraintBottom_toBottomOf="@id/tvSettingsShowGoalsSeparately" 386 + app:layout_constraintEnd_toEndOf="parent" 387 + app:layout_constraintTop_toTopOf="@id/tvSettingsShowGoalsSeparately" /> 388 + 389 + </androidx.constraintlayout.widget.ConstraintLayout> 390 + 391 + <View 392 + style="@style/SettingsDivider" 393 + android:layout_width="match_parent" /> 394 + 395 + <androidx.constraintlayout.widget.ConstraintLayout 396 + android:layout_width="match_parent" 397 + android:layout_height="wrap_content"> 398 + 399 + <androidx.appcompat.widget.AppCompatTextView 372 400 android:id="@+id/tvSettingsKeepScreenOn" 373 401 style="@style/SettingsText" 374 402 android:layout_width="0dp"
+18 -11
features/feature_statistics/src/main/java/com/example/util/simpletimetracker/feature_statistics/interactor/StatisticsViewDataInteractor.kt
··· 3 3 import com.example.util.simpletimetracker.core.interactor.StatisticsChartViewDataInteractor 4 4 import com.example.util.simpletimetracker.core.interactor.StatisticsMediator 5 5 import com.example.util.simpletimetracker.core.mapper.ColorMapper 6 + import com.example.util.simpletimetracker.core.mapper.GoalViewDataMapper 6 7 import com.example.util.simpletimetracker.core.mapper.RangeViewDataMapper 7 8 import com.example.util.simpletimetracker.domain.UNCATEGORIZED_ITEM_ID 8 9 import com.example.util.simpletimetracker.domain.UNTRACKED_ITEM_ID ··· 33 34 private val statisticsViewDataMapper: StatisticsViewDataMapper, 34 35 private val rangeViewDataMapper: RangeViewDataMapper, 35 36 private val colorMapper: ColorMapper, 37 + private val goalViewDataMapper: GoalViewDataMapper, 36 38 ) { 37 39 38 40 fun mapFilter( ··· 63 65 64 66 return when (filterType) { 65 67 ChartFilterType.ACTIVITY -> { 66 - listOf(selectedId).let(RecordsFilter::Activity) 68 + listOf(selectedId) 69 + .let(RecordsFilter::Activity) 67 70 } 68 71 ChartFilterType.CATEGORY -> { 69 - listOf(selectedId).map(RecordsFilter.CategoryItem::Categorized).let(RecordsFilter::Category) 72 + listOf(selectedId).map(RecordsFilter.CategoryItem::Categorized) 73 + .let(RecordsFilter::Category) 70 74 } 71 75 ChartFilterType.RECORD_TAG -> { 72 - listOf(selectedId).map(RecordsFilter.TagItem::Tagged).let(RecordsFilter::SelectedTags) 76 + listOf(selectedId).map(RecordsFilter.TagItem::Tagged) 77 + .let(RecordsFilter::SelectedTags) 73 78 } 74 79 } 75 80 } ··· 86 91 val showDuration = rangeLength !is RangeLength.All 87 92 val types = recordTypeInteractor.getAll().associateBy(RecordType::id) 88 93 val goals = recordTypeGoalInteractor.getAll().groupBy(RecordTypeGoal::typeId) 94 + val showGoalsSeparately = prefsInteractor.getShowGoalsSeparately() 89 95 90 96 val filteredIds = when (filterType) { 91 97 ChartFilterType.ACTIVITY -> prefsInteractor.getFilteredTypes() ··· 106 112 shift = shift, 107 113 ) 108 114 // Don't show goals in the future if there is no records there. 109 - val goalsStatistics = if (shift > 0 && statistics.isEmpty()) { 115 + val goalsStatistics = if ( 116 + (shift > 0 && statistics.isEmpty()) || 117 + showGoalsSeparately 118 + ) { 110 119 emptyList() 111 120 } else { 112 121 statisticsMediator.getGoals( ··· 121 130 statistics = statistics, 122 131 dataHolders = dataHolders, 123 132 types = types, 124 - isDarkTheme = isDarkTheme 133 + isDarkTheme = isDarkTheme, 125 134 ).let { 126 135 // If there is no data but have goals - show empty chart. 127 136 val data = it 128 137 .takeUnless { it.isEmpty() } 129 138 ?: PiePortion( 130 139 value = 0, 131 - colorInt = colorMapper.toUntrackedColor(isDarkTheme) 140 + colorInt = colorMapper.toUntrackedColor(isDarkTheme), 132 141 ).let(::listOf) 133 142 134 143 StatisticsChartViewData( ··· 148 157 useProportionalMinutes = useProportionalMinutes, 149 158 showSeconds = showSeconds, 150 159 ) 151 - val goalsList = statisticsViewDataMapper.mapGoalItemsList( 160 + val goalsList = goalViewDataMapper.mapList( 152 161 rangeLength = rangeLength, 153 - statistics = goalsStatistics, 162 + statistics = goalsStatistics.filterNot { it.id in filteredIds }, 154 163 data = dataHolders, 155 - filteredIds = filteredIds, 156 164 isDarkTheme = isDarkTheme, 157 165 useProportionalMinutes = useProportionalMinutes, 158 166 showSeconds = showSeconds, ··· 179 187 statisticsViewDataMapper.mapToHint().let(result::add) 180 188 } 181 189 if (goalsList.isNotEmpty()) { 182 - DividerViewData(1).let(result::add) 183 190 statisticsViewDataMapper.mapToGoalHint().let(result::add) 184 191 goalsList.let(result::addAll) 185 192 } ··· 196 203 rangeLength = rangeLength, 197 204 position = shift, 198 205 startOfDayShift = prefsInteractor.getStartOfDayShift(), 199 - firstDayOfWeek = prefsInteractor.getFirstDayOfWeek() 206 + firstDayOfWeek = prefsInteractor.getFirstDayOfWeek(), 200 207 ) 201 208 StatisticsTitleViewData(title).let(::add) 202 209 DividerViewData(1).let(::add)
-93
features/feature_statistics/src/main/java/com/example/util/simpletimetracker/feature_statistics/mapper/StatisticsViewDataMapper.kt
··· 9 9 import com.example.util.simpletimetracker.domain.UNTRACKED_ITEM_ID 10 10 import com.example.util.simpletimetracker.domain.mapper.StatisticsMapper 11 11 import com.example.util.simpletimetracker.domain.model.ChartFilterType 12 - import com.example.util.simpletimetracker.domain.model.RangeLength 13 12 import com.example.util.simpletimetracker.domain.model.Statistics 14 13 import com.example.util.simpletimetracker.feature_base_adapter.ViewHolderType 15 14 import com.example.util.simpletimetracker.feature_base_adapter.empty.EmptyViewData 16 15 import com.example.util.simpletimetracker.feature_base_adapter.hint.HintViewData 17 16 import com.example.util.simpletimetracker.feature_base_adapter.statistics.StatisticsViewData 18 - import com.example.util.simpletimetracker.feature_base_adapter.statisticsGoal.StatisticsGoalViewData 19 17 import com.example.util.simpletimetracker.feature_statistics.R 20 18 import com.example.util.simpletimetracker.feature_statistics.viewData.StatisticsInfoViewData 21 19 import com.example.util.simpletimetracker.feature_views.TransitionNames 22 20 import com.example.util.simpletimetracker.feature_views.viewData.RecordTypeIcon 23 21 import javax.inject.Inject 24 - import kotlin.math.roundToLong 25 22 26 23 class StatisticsViewDataMapper @Inject constructor( 27 24 private val iconMapper: IconMapper, ··· 65 62 } 66 63 .sortedByDescending { (_, duration) -> duration } 67 64 .map { (statistics, _) -> statistics } 68 - } 69 - 70 - fun mapGoalItemsList( 71 - rangeLength: RangeLength, 72 - statistics: List<Statistics>, 73 - data: Map<Long, StatisticsDataHolder>, 74 - filteredIds: List<Long>, 75 - isDarkTheme: Boolean, 76 - useProportionalMinutes: Boolean, 77 - showSeconds: Boolean, 78 - ): List<StatisticsGoalViewData> { 79 - val statisticsFiltered = statistics.filterNot { it.id in filteredIds } 80 - 81 - return statisticsFiltered 82 - .mapNotNull { statistic -> 83 - mapGoalItem( 84 - rangeLength = rangeLength, 85 - statistics = statistic, 86 - dataHolder = data[statistic.id] ?: return@mapNotNull null, 87 - isDarkTheme = isDarkTheme, 88 - useProportionalMinutes = useProportionalMinutes, 89 - showSeconds = showSeconds, 90 - ) 91 - } 92 - .sortedBy { it.goal.percent } 93 65 } 94 66 95 67 fun mapStatisticsTotalTracked(totalTracked: String): ViewHolderType { ··· 213 185 } else { 214 186 "" 215 187 } 216 - } 217 - 218 - private fun mapGoalItem( 219 - rangeLength: RangeLength, 220 - statistics: Statistics, 221 - dataHolder: StatisticsDataHolder, 222 - isDarkTheme: Boolean, 223 - useProportionalMinutes: Boolean, 224 - showSeconds: Boolean, 225 - ): StatisticsGoalViewData { 226 - return StatisticsGoalViewData( 227 - id = statistics.id, 228 - name = dataHolder.name, 229 - icon = dataHolder.icon.orEmpty() 230 - .let(iconMapper::mapIcon), 231 - color = dataHolder.color 232 - .let { colorMapper.mapToColorInt(it, isDarkTheme) }, 233 - goal = mapGoal( 234 - statistics = statistics, 235 - dataHolder = dataHolder, 236 - rangeLength = rangeLength, 237 - useProportionalMinutes = useProportionalMinutes, 238 - showSeconds = showSeconds, 239 - ), 240 - ) 241 - } 242 - 243 - private fun mapGoal( 244 - statistics: Statistics, 245 - dataHolder: StatisticsDataHolder, 246 - rangeLength: RangeLength, 247 - useProportionalMinutes: Boolean, 248 - showSeconds: Boolean, 249 - ): StatisticsGoalViewData.Goal { 250 - val goal = when (rangeLength) { 251 - is RangeLength.Day -> dataHolder.dailyGoalTime 252 - is RangeLength.Week -> dataHolder.weeklyGoalTime 253 - is RangeLength.Month -> dataHolder.monthlyGoalTime 254 - else -> return StatisticsGoalViewData.Goal.empty() 255 - } * 1000 256 - 257 - if (goal == 0L) return StatisticsGoalViewData.Goal.empty() 258 - 259 - val current = statistics.duration 260 - val goalComplete = goal - current <= 0L 261 - val currentString = timeMapper.formatInterval( 262 - interval = current, 263 - forceSeconds = showSeconds, 264 - useProportionalMinutes = useProportionalMinutes, 265 - ) 266 - val goalString = timeMapper.formatInterval( 267 - interval = goal, 268 - forceSeconds = showSeconds, 269 - useProportionalMinutes = useProportionalMinutes, 270 - ) 271 - val goalTimeString = "$currentString\n($goalString)" 272 - val goalTimePercent = (current * 100f / goal).roundToLong() 273 - .coerceAtMost(100) 274 - 275 - return StatisticsGoalViewData.Goal( 276 - goalTime = goalTimeString, 277 - goalPercent = goalTimePercent.let { "$it%" }, 278 - goalComplete = goalComplete, 279 - percent = goalTimePercent, 280 - ) 281 188 } 282 189 }