Compare commits
No commits in common. "275a2bdcb6c5b0dedd504187f0e45be2daeebab7" and "a226686431a44a8b74d624c076f67cac97015ca6" have entirely different histories.
275a2bdcb6
...
a226686431
254
README.md
@ -1,159 +1,187 @@
|
||||
# 💧🏃 Step & Drink
|
||||
Step & Drink
|
||||
Aplikasi Android untuk tracking langkah harian dan kebutuhan air minum menggunakan Kotlin dan Jetpack Compose.
|
||||
|
||||
Aplikasi Android untuk tracking langkah harian dan asupan air minum.
|
||||
📋 Deskripsi
|
||||
Step & Drink adalah aplikasi mobile yang membantu pengguna untuk:
|
||||
|
||||
[](https://kotlinlang.org)
|
||||
[](https://developer.android.com/jetpack/compose)
|
||||
[](https://developer.android.com)
|
||||
Melacak langkah harian menggunakan sensor step counter bawaan smartphone
|
||||
Mencatat asupan air minum untuk memenuhi kebutuhan hidrasi harian
|
||||
Menetapkan dan memantau target langkah dan air minum yang dapat disesuaikan
|
||||
Melihat riwayat aktivitas untuk evaluasi kebiasaan sehat
|
||||
|
||||
---
|
||||
Aplikasi ini dibuat sebagai tugas akhir mata kuliah Pemrograman Bergerak dengan fokus pada penggunaan sensor hardware, database lokal, dan multi-halaman navigation.
|
||||
|
||||
## 📱 Tentang
|
||||
✨ Fitur
|
||||
1. Home Screen
|
||||
|
||||
Step & Drink membantu kamu menjaga kesehatan dengan:
|
||||
- 🏃 **Step Counter** - Track langkah harian secara real-time
|
||||
- 💧 **Water Tracker** - Catat asupan air dengan mudah
|
||||
- ⏰ **Smart Reminder** - Notifikasi otomatis untuk minum teratur
|
||||
- 📊 **Progress Monitor** - Lihat pencapaian target harian
|
||||
Dashboard dengan ringkasan aktivitas hari ini
|
||||
Card interaktif untuk langkah dan air minum
|
||||
Progress bar visual untuk tracking target
|
||||
Greeting dengan nama pengguna
|
||||
|
||||
---
|
||||
2. Step Tracker
|
||||
|
||||
## ✨ Fitur Utama
|
||||
Real-time tracking langkah menggunakan sensor TYPE_STEP_COUNTER
|
||||
Start/Stop tracking dengan tombol floating action button
|
||||
Riwayat langkah 7 hari terakhir
|
||||
Target langkah yang dapat disesuaikan
|
||||
|
||||
### Step Counter
|
||||
- Real-time tracking dengan hardware sensor
|
||||
- Target harian (default 10,000 langkah)
|
||||
- History 7 hari
|
||||
- Start/Stop control
|
||||
3. Water Tracker
|
||||
|
||||
### Water Tracker
|
||||
Pilihan container cepat:
|
||||
- 🥛 Gelas Kecil (200ml)
|
||||
- 💧 Gelas Sedang (250ml)
|
||||
- 🌊 Gelas Besar (350ml)
|
||||
- 🚰 Botol Aqua (600ml)
|
||||
- 🫗 Tumbler (500ml)
|
||||
Quick add buttons (250ml, 500ml, 1000ml)
|
||||
Input custom untuk jumlah air
|
||||
Riwayat minum harian dengan timestamp
|
||||
Hapus record jika salah input
|
||||
Target air minum yang dapat disesuaikan
|
||||
|
||||
Plus input custom & history lengkap.
|
||||
4. Profile & Settings
|
||||
|
||||
### Smart Reminder
|
||||
- Notifikasi otomatis setelah 3 jam tidak minum
|
||||
- Aktif jam 06:00 - 22:00
|
||||
- Battery efficient
|
||||
- Bisa di-toggle ON/OFF
|
||||
Edit nama pengguna
|
||||
Ubah target langkah harian
|
||||
Ubah target air minum harian
|
||||
Informasi aplikasi
|
||||
|
||||
### Profile
|
||||
- Input nama & berat badan
|
||||
- Set target langkah & air harian
|
||||
- Data tersimpan lokal
|
||||
|
||||
---
|
||||
🛠️ Teknologi
|
||||
Bahasa & Framework
|
||||
|
||||
## 🛠️ Teknologi
|
||||
Kotlin - Bahasa pemrograman
|
||||
Jetpack Compose - UI Framework
|
||||
Material Design 3 - Design system
|
||||
|
||||
**Core:**
|
||||
- Kotlin 100%
|
||||
- Jetpack Compose
|
||||
- Material Design 3
|
||||
Architecture
|
||||
|
||||
**Architecture:**
|
||||
- MVVM Pattern
|
||||
- Room Database
|
||||
- DataStore Preferences
|
||||
- WorkManager
|
||||
- Coroutines & Flow
|
||||
MVVM (Model-View-ViewModel)
|
||||
Repository Pattern
|
||||
Clean Architecture
|
||||
|
||||
---
|
||||
Database & Storage
|
||||
|
||||
## 🚀 Instalasi
|
||||
Room Database - Local database (SQLite)
|
||||
DataStore Preferences - Settings storage
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/HagaDalpintoGinting/Steps-Drink.git
|
||||
Components
|
||||
|
||||
# Buka di Android Studio
|
||||
# Sync Gradle
|
||||
# Run aplikasi
|
||||
```
|
||||
Navigation Compose - Multi-halaman navigation
|
||||
Sensor Manager - Akses hardware sensor (Step Counter)
|
||||
ViewModel - State management
|
||||
Kotlin Flow - Reactive data stream
|
||||
Coroutines - Asynchronous operations
|
||||
|
||||
**Requirements:**
|
||||
- Android Studio (latest)
|
||||
- Min SDK 26 (Android 8.0)
|
||||
- Device dengan step counter sensor (untuk fitur langkah)
|
||||
|
||||
---
|
||||
📦 Struktur Project
|
||||
stepdrink/
|
||||
├── data/
|
||||
│ ├── local/
|
||||
│ │ ├── entity/ # StepRecord, WaterRecord
|
||||
│ │ ├── dao/ # StepDao, WaterDao
|
||||
│ │ ├── database/ # AppDatabase
|
||||
│ │ └── PreferencesManager.kt
|
||||
│ └── repository/ # StepRepository, WaterRepository
|
||||
├── ui/
|
||||
│ ├── screen/ # HomeScreen, StepsScreen, WaterScreen, ProfileScreen
|
||||
│ ├── navigation/ # Navigation setup
|
||||
│ ├── components/ # Reusable UI components
|
||||
│ └── theme/ # App theming
|
||||
├── viewmodel/ # StepViewModel, WaterViewModel, ProfileViewModel
|
||||
├── sensor/ # StepCounterManager
|
||||
├── util/ # DateUtils
|
||||
└── MainActivity.kt
|
||||
|
||||
## 📖 Penggunaan
|
||||
🗄️ Database
|
||||
step_records
|
||||
Menyimpan data langkah harian
|
||||
|
||||
### Setup
|
||||
1. Buka aplikasi
|
||||
2. Masuk ke Profile
|
||||
3. Input data pribadi & set target
|
||||
id - Primary key
|
||||
date - Tanggal (yyyy-MM-dd)
|
||||
steps - Jumlah langkah
|
||||
timestamp - Waktu pencatatan
|
||||
|
||||
### Track Langkah
|
||||
1. Buka halaman Steps
|
||||
2. Tap tombol Start
|
||||
3. Mulai berjalan
|
||||
water_records
|
||||
Menyimpan data minum air
|
||||
|
||||
### Track Air
|
||||
1. Buka halaman Water
|
||||
2. Tap container (contoh: 🚰 600ml)
|
||||
3. Atau klik "Input Custom" untuk jumlah lain
|
||||
id - Primary key
|
||||
date - Tanggal (yyyy-MM-dd)
|
||||
amount - Jumlah air (ml)
|
||||
timestamp - Waktu pencatatan
|
||||
|
||||
### Aktifkan Reminder
|
||||
1. Buka halaman Water
|
||||
2. Toggle "Pengingat Minum" ke ON
|
||||
3. Izinkan permission notifikasi
|
||||
Preferences (DataStore)
|
||||
Menyimpan pengaturan pengguna
|
||||
|
||||
---
|
||||
user_name - Nama pengguna
|
||||
step_goal - Target langkah harian
|
||||
water_goal - Target air minum harian (ml)
|
||||
|
||||
## ⚙️ Konfigurasi
|
||||
|
||||
Ubah settings reminder di `WaterReminderManager.kt`:
|
||||
```kotlin
|
||||
const val CHECK_INTERVAL_MINUTES = 30L // Interval cek
|
||||
const val REMIND_AFTER_HOURS = 3 // Reminder setelah X jam
|
||||
const val ACTIVE_START_HOUR = 6 // Jam mulai
|
||||
const val ACTIVE_END_HOUR = 22 // Jam selesai
|
||||
```
|
||||
🚀 Instalasi
|
||||
Requirements
|
||||
|
||||
---
|
||||
Android Studio (versi terbaru)
|
||||
Minimum SDK: API 26 (Android 8.0)
|
||||
Kotlin 1.9.22
|
||||
|
||||
## 📝 Catatan
|
||||
Cara Menjalankan
|
||||
|
||||
**Tested on:** Samsung Galaxy A35 5G (Android 14)
|
||||
Clone atau download project
|
||||
Buka di Android Studio
|
||||
Sync Gradle (File → Sync Project with Gradle Files)
|
||||
Build project (Build → Make Project)
|
||||
Run di device atau emulator
|
||||
Izinkan permission ACTIVITY_RECOGNITION saat diminta
|
||||
|
||||
**Limitations:**
|
||||
- Step counter butuh device fisik (emulator tidak support)
|
||||
- Data tersimpan lokal saja
|
||||
- Reminder bergantung pada battery optimization device
|
||||
|
||||
**Troubleshooting:**
|
||||
- Steps tidak terhitung? Pastikan permission ACTIVITY_RECOGNITION diizinkan
|
||||
- Reminder tidak muncul? Check permission POST_NOTIFICATIONS dan pastikan jam aktif
|
||||
📱 Cara Penggunaan
|
||||
|
||||
---
|
||||
Tracking Langkah
|
||||
|
||||
## 🎓 Project
|
||||
Buka Step Tracker
|
||||
Klik tombol Play
|
||||
Izinkan permission
|
||||
Mulai berjalan
|
||||
Data otomatis tersimpan
|
||||
|
||||
Tugas Akhir Pemrograman Bergerak dengan requirement:
|
||||
- ✅ Multi-screen
|
||||
- ✅ Hardware sensor
|
||||
- ✅ Local database
|
||||
- ✅ Kotlin & Jetpack Compose
|
||||
|
||||
---
|
||||
Catat Air Minum
|
||||
|
||||
## 👨💻 Developer
|
||||
Buka Water Tracker
|
||||
Klik quick add atau tombol +
|
||||
Input jumlah air
|
||||
Data tersimpan dengan timestamp
|
||||
|
||||
**Haga Dalpinto Ginting**
|
||||
|
||||
GitHub: [@HagaDalpintoGinting](https://github.com/HagaDalpintoGinting)
|
||||
Ubah Target
|
||||
|
||||
---
|
||||
Buka Profile
|
||||
Klik card yang ingin diubah
|
||||
Input nilai baru
|
||||
Simpan
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License © 2025
|
||||
|
||||
---
|
||||
|
||||
🎯 Tujuan Project
|
||||
Project ini dibuat untuk memenuhi tugas akhir mata kuliah Pemrograman Bergerak dengan requirements:
|
||||
|
||||
✅ Multi-halaman (4 screens)
|
||||
✅ Penggunaan sensor (Step Counter)
|
||||
✅ Database lokal (Room)
|
||||
✅ IDE Android Studio
|
||||
✅ Bahasa Kotlin
|
||||
✅ UI Framework Jetpack Compose
|
||||
|
||||
|
||||
👨💻 Developer
|
||||
Nama: Haga Dalpinto Ginting
|
||||
|
||||
📝 Catatan
|
||||
|
||||
Aplikasi memerlukan device dengan sensor step counter untuk fitur tracking langkah
|
||||
Emulator Android biasanya tidak memiliki sensor step counter
|
||||
Data disimpan secara lokal di device
|
||||
Aplikasi masih dalam tahap pengembangan dan dapat dikembangkan lebih lanjut
|
||||
|
||||
|
||||
📄 License
|
||||
MIT License - Copyright (c) 2025
|
||||
|
||||
Made with Kotlin & Jetpack Compose
|
||||
|
||||
@ -51,8 +51,6 @@ dependencies {
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
// WorkManager untuk reminder
|
||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||
// Core Android
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
||||
|
||||
@ -8,9 +8,6 @@
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- Require step counter sensor -->
|
||||
<uses-feature
|
||||
|
||||
@ -4,25 +4,18 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.example.stepdrink.ui.navigation.AppNavigation
|
||||
import com.example.stepdrink.ui.navigation.NavGraph
|
||||
import com.example.stepdrink.ui.theme.StepDrinkTheme
|
||||
import com.example.stepdrink.viewmodel.ProfileViewModel
|
||||
import com.example.stepdrink.viewmodel.StepViewModel
|
||||
import com.example.stepdrink.viewmodel.WaterViewModel
|
||||
|
||||
import com.example.stepdrink.viewmodel.ProfileViewModel
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
// Inisialisasi ViewModels menggunakan viewModels() delegate
|
||||
private val stepViewModel: StepViewModel by viewModels()
|
||||
private val waterViewModel: WaterViewModel by viewModels()
|
||||
private val profileViewModel: ProfileViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
@ -33,9 +26,11 @@ class MainActivity : ComponentActivity() {
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val stepViewModel: StepViewModel = viewModel()
|
||||
val waterViewModel: WaterViewModel = viewModel()
|
||||
val profileViewModel: ProfileViewModel = viewModel()
|
||||
|
||||
// Gunakan AppNavigation yang sudah include splash
|
||||
AppNavigation(
|
||||
NavGraph(
|
||||
navController = navController,
|
||||
stepViewModel = stepViewModel,
|
||||
waterViewModel = waterViewModel,
|
||||
|
||||
@ -6,40 +6,18 @@ import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface WaterDao {
|
||||
@Insert
|
||||
suspend fun insertWaterRecord(waterRecord: WaterRecord)
|
||||
|
||||
/**
|
||||
* Insert new water record
|
||||
*/
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertWater(waterRecord: WaterRecord)
|
||||
|
||||
/**
|
||||
* Delete water record
|
||||
*/
|
||||
@Delete
|
||||
suspend fun deleteWater(waterRecord: WaterRecord)
|
||||
|
||||
/**
|
||||
* Get all water records for a specific date
|
||||
*/
|
||||
@Query("SELECT * FROM water_records WHERE date = :date ORDER BY timestamp DESC")
|
||||
fun getWaterByDate(date: String): Flow<List<WaterRecord>>
|
||||
fun getWaterRecordsByDate(date: String): Flow<List<WaterRecord>>
|
||||
|
||||
/**
|
||||
* Get last 7 days water records
|
||||
*/
|
||||
@Query("SELECT * FROM water_records ORDER BY date DESC, timestamp DESC LIMIT 50")
|
||||
@Query("SELECT SUM(amount) FROM water_records WHERE date = :date")
|
||||
fun getTotalWaterByDate(date: String): Flow<Int?>
|
||||
|
||||
@Query("SELECT * FROM water_records ORDER BY date DESC LIMIT 7")
|
||||
fun getLast7DaysWater(): Flow<List<WaterRecord>>
|
||||
|
||||
/**
|
||||
* Get all water records (for backup/export)
|
||||
*/
|
||||
@Query("SELECT * FROM water_records ORDER BY date DESC, timestamp DESC")
|
||||
fun getAllWater(): Flow<List<WaterRecord>>
|
||||
|
||||
/**
|
||||
* Delete all water records (for reset)
|
||||
*/
|
||||
@Query("DELETE FROM water_records")
|
||||
suspend fun deleteAllWater()
|
||||
@Delete
|
||||
suspend fun deleteWaterRecord(waterRecord: WaterRecord)
|
||||
}
|
||||
@ -6,36 +6,23 @@ import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class WaterRepository(private val waterDao: WaterDao) {
|
||||
|
||||
/**
|
||||
* Get all water records for a specific date
|
||||
*/
|
||||
fun getWaterByDate(date: String): Flow<List<WaterRecord>> {
|
||||
return waterDao.getWaterByDate(date)
|
||||
fun getWaterRecordsByDate(date: String): Flow<List<WaterRecord>> {
|
||||
return waterDao.getWaterRecordsByDate(date)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new water record
|
||||
*/
|
||||
suspend fun insertWater(date: String, amount: Int) {
|
||||
val record = WaterRecord(
|
||||
date = date,
|
||||
amount = amount,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
waterDao.insertWater(record)
|
||||
fun getTotalWaterByDate(date: String): Flow<Int?> {
|
||||
return waterDao.getTotalWaterByDate(date)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete water record
|
||||
*/
|
||||
suspend fun deleteWater(record: WaterRecord) {
|
||||
waterDao.deleteWater(record)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last 7 days water records
|
||||
*/
|
||||
fun getLast7DaysWater(): Flow<List<WaterRecord>> {
|
||||
return waterDao.getLast7DaysWater()
|
||||
}
|
||||
|
||||
suspend fun addWaterRecord(date: String, amount: Int) {
|
||||
waterDao.insertWaterRecord(WaterRecord(date = date, amount = amount))
|
||||
}
|
||||
|
||||
suspend fun deleteWaterRecord(waterRecord: WaterRecord) {
|
||||
waterDao.deleteWaterRecord(waterRecord)
|
||||
}
|
||||
}
|
||||
@ -5,20 +5,15 @@ import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import com.example.stepdrink.ui.screen.home.HomeScreen
|
||||
import com.example.stepdrink.ui.screen.profile.ProfileScreen
|
||||
import com.example.stepdrink.ui.screen.splash.SplashScreen
|
||||
import com.example.stepdrink.ui.screen.steps.StepsScreen
|
||||
import com.example.stepdrink.ui.screen.water.WaterScreen
|
||||
import com.example.stepdrink.viewmodel.ProfileViewModel
|
||||
import com.example.stepdrink.ui.screen.profile.ProfileScreen
|
||||
import com.example.stepdrink.viewmodel.StepViewModel
|
||||
import com.example.stepdrink.viewmodel.WaterViewModel
|
||||
import com.example.stepdrink.viewmodel.ProfileViewModel
|
||||
|
||||
/**
|
||||
* AppNavigation - Single NavHost untuk semua screen (termasuk splash)
|
||||
* Ini menghindari nested NavHost yang menyebabkan ViewModelStore error
|
||||
*/
|
||||
@Composable
|
||||
fun AppNavigation(
|
||||
fun NavGraph(
|
||||
navController: NavHostController,
|
||||
stepViewModel: StepViewModel,
|
||||
waterViewModel: WaterViewModel,
|
||||
@ -26,14 +21,8 @@ fun AppNavigation(
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screen.Splash.route
|
||||
startDestination = Screen.Home.route
|
||||
) {
|
||||
// Splash Screen
|
||||
composable(Screen.Splash.route) {
|
||||
SplashScreen(navController = navController)
|
||||
}
|
||||
|
||||
// Home Screen
|
||||
composable(Screen.Home.route) {
|
||||
HomeScreen(
|
||||
navController = navController,
|
||||
@ -43,7 +32,6 @@ fun AppNavigation(
|
||||
)
|
||||
}
|
||||
|
||||
// Steps Screen
|
||||
composable(Screen.Steps.route) {
|
||||
StepsScreen(
|
||||
navController = navController,
|
||||
@ -51,20 +39,17 @@ fun AppNavigation(
|
||||
)
|
||||
}
|
||||
|
||||
// Water Screen
|
||||
composable(Screen.Water.route) {
|
||||
WaterScreen(
|
||||
navController = navController,
|
||||
viewModel = waterViewModel
|
||||
)
|
||||
}
|
||||
|
||||
// Profile Screen
|
||||
composable(Screen.Profile.route) {
|
||||
ProfileScreen(
|
||||
navController = navController,
|
||||
viewModel = profileViewModel
|
||||
)
|
||||
viewModel = profileViewModel )
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
package com.example.stepdrink.ui.navigation
|
||||
|
||||
sealed class Screen(val route: String) {
|
||||
object Splash : Screen("splash")
|
||||
object Home : Screen("home")
|
||||
object Steps : Screen("steps")
|
||||
object Water : Screen("water")
|
||||
|
||||
@ -1,78 +1,2 @@
|
||||
package com.example.stepdrink.ui.screen.splash
|
||||
package ui.screen.splash
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.example.stepdrink.R
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Composable
|
||||
fun SplashScreen(navController: NavController) {
|
||||
LaunchedEffect(key1 = true) {
|
||||
delay(2500) // Show splash for 2.5 seconds
|
||||
navController.navigate("home") {
|
||||
popUpTo("splash") { inclusive = true }
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF7BC8BC), // Mint top
|
||||
Color(0xFF5DADE2) // Blue bottom
|
||||
)
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
// App Logo - Ganti dengan logo kamu
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.icon_apps_steps_drink),
|
||||
contentDescription = "App Logo",
|
||||
modifier = Modifier
|
||||
.size(180.dp)
|
||||
.padding(bottom = 32.dp),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
|
||||
// App Name
|
||||
Text(
|
||||
text = "Step & Drink",
|
||||
fontSize = 40.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White,
|
||||
letterSpacing = 1.sp
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Tagline
|
||||
Text(
|
||||
text = "Track Your Health Journey",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = Color.White.copy(alpha = 0.9f),
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,30 +1,17 @@
|
||||
package com.example.stepdrink.ui.screen.water
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.example.stepdrink.util.WaterReminderManager
|
||||
import com.example.stepdrink.viewmodel.WaterViewModel
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@ -35,35 +22,11 @@ fun WaterScreen(
|
||||
navController: NavController,
|
||||
viewModel: WaterViewModel
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val reminderManager = remember { WaterReminderManager(context) }
|
||||
|
||||
val todayWater by viewModel.todayTotalWater.collectAsState()
|
||||
val waterGoal by viewModel.dailyGoal.collectAsState()
|
||||
val todayRecords by viewModel.todayWaterRecords.collectAsState()
|
||||
val totalWater by viewModel.todayTotalWater.collectAsState()
|
||||
val waterGoal by viewModel.dailyGoal.collectAsState()
|
||||
|
||||
var showCustomDialog by remember { mutableStateOf(false) }
|
||||
var showSuccessAnimation by remember { mutableStateOf(false) }
|
||||
var reminderEnabled by remember { mutableStateOf(false) }
|
||||
|
||||
// Permission launcher untuk notifikasi (Android 13+)
|
||||
val notificationPermissionLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (isGranted) {
|
||||
reminderManager.scheduleReminder()
|
||||
reminderEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
// Success animation
|
||||
LaunchedEffect(todayRecords.size) {
|
||||
if (todayRecords.isNotEmpty()) {
|
||||
showSuccessAnimation = true
|
||||
kotlinx.coroutines.delay(800)
|
||||
showSuccessAnimation = false
|
||||
}
|
||||
}
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
@ -73,15 +36,17 @@ fun WaterScreen(
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(Icons.Default.ArrowBack, "Kembali")
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
)
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = { showDialog = true }
|
||||
) {
|
||||
Icon(Icons.Default.Add, "Tambah")
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@ -89,392 +54,142 @@ fun WaterScreen(
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Main Progress Card
|
||||
item {
|
||||
// Water Progress Card
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
|
||||
elevation = CardDefaults.cardElevation(4.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
listOf(Color(0xFF2196F3), Color(0xFF64B5F6))
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer
|
||||
)
|
||||
)
|
||||
.padding(24.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.WaterDrop,
|
||||
imageVector = Icons.Default.WaterDrop,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = Color.White
|
||||
tint = MaterialTheme.colorScheme.tertiary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "${todayWater}ml",
|
||||
text = "${totalWater}ml",
|
||||
style = MaterialTheme.typography.displayLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Air Minum Hari Ini",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = Color.White.copy(alpha = 0.9f)
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
LinearProgressIndicator(
|
||||
progress = { todayWater.toFloat() / waterGoal.toFloat() },
|
||||
progress = { totalWater.toFloat() / waterGoal.toFloat() },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(10.dp)
|
||||
.clip(MaterialTheme.shapes.small),
|
||||
color = Color.White,
|
||||
trackColor = Color.White.copy(alpha = 0.3f)
|
||||
.height(8.dp),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Target: ${waterGoal}ml",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.White.copy(alpha = 0.9f)
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
val remaining = waterGoal - todayWater
|
||||
if (remaining > 0) {
|
||||
Text(
|
||||
text = "Sisa ${remaining}ml lagi!",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.White.copy(alpha = 0.8f)
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "🎉 Target tercapai!",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reminder Settings Card
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
// Quick Add Buttons
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Notifications,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
QuickAddButton(
|
||||
amount = 250,
|
||||
onClick = { viewModel.addWater(250) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
"Pengingat Minum",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
QuickAddButton(
|
||||
amount = 500,
|
||||
onClick = { viewModel.addWater(500) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Text(
|
||||
"Ingatkan jika 3 jam tidak minum",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.7f)
|
||||
QuickAddButton(
|
||||
amount = 1000,
|
||||
onClick = { viewModel.addWater(1000) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Switch(
|
||||
checked = reminderEnabled,
|
||||
onCheckedChange = { enabled ->
|
||||
if (enabled) {
|
||||
// Check & request notification permission
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
reminderManager.scheduleReminder()
|
||||
reminderEnabled = true
|
||||
}
|
||||
} else {
|
||||
reminderManager.cancelReminder()
|
||||
reminderEnabled = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (reminderEnabled) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f),
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Info,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
"Aktif jam 06:00 - 22:00",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Container selection
|
||||
item {
|
||||
Text(
|
||||
text = "Pilih Wadah:",
|
||||
text = "Riwayat Hari Ini",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
// Row 1
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
viewModel.presetContainers.take(2).forEach { container ->
|
||||
ContainerCard(
|
||||
container = container,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { viewModel.addWaterFromContainer(container) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Row 2
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
viewModel.presetContainers.drop(2).take(2).forEach { container ->
|
||||
ContainerCard(
|
||||
container = container,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { viewModel.addWaterFromContainer(container) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Row 3 - Last item centered
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
ContainerCard(
|
||||
container = viewModel.presetContainers[4],
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { viewModel.addWaterFromContainer(viewModel.presetContainers[4]) }
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom amount
|
||||
item {
|
||||
OutlinedCard(
|
||||
onClick = { showCustomDialog = true },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.Settings, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
"Input Jumlah Custom",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// History
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
"Riwayat Hari Ini",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
"${todayRecords.size} kali minum",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (todayRecords.isEmpty()) {
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("💧", style = MaterialTheme.typography.displayMedium)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
"Belum ada riwayat hari ini",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
"Yuk mulai minum air!",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items(todayRecords.sortedByDescending { it.timestamp }) { record ->
|
||||
items(todayRecords) { record ->
|
||||
WaterRecordItem(
|
||||
record = record,
|
||||
container = viewModel.getContainerByAmount(record.amount),
|
||||
onDelete = { viewModel.deleteWater(record) }
|
||||
amount = record.amount,
|
||||
timestamp = record.timestamp,
|
||||
onDelete = { viewModel.deleteWaterRecord(record) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Success animation
|
||||
AnimatedVisibility(
|
||||
visible = showSuccessAnimation,
|
||||
enter = scaleIn(initialScale = 0.3f) + fadeIn(),
|
||||
exit = scaleOut(targetScale = 1.5f) + fadeOut(),
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
) {
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = Color.White,
|
||||
shadowElevation = 8.dp,
|
||||
modifier = Modifier.size(100.dp)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Text("💧", style = MaterialTheme.typography.displayLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showCustomDialog) {
|
||||
CustomAmountDialog(
|
||||
onDismiss = { showCustomDialog = false },
|
||||
if (showDialog) {
|
||||
AddWaterDialog(
|
||||
onDismiss = { showDialog = false },
|
||||
onConfirm = { amount ->
|
||||
viewModel.addWater(amount)
|
||||
showCustomDialog = false
|
||||
showDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContainerCard(
|
||||
container: com.example.stepdrink.viewmodel.WaterContainer,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit
|
||||
fun QuickAddButton(
|
||||
amount: Int,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Card(
|
||||
OutlinedButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier.height(120.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color(container.color).copy(alpha = 0.1f)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(2.dp)
|
||||
modifier = modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = container.emoji,
|
||||
style = MaterialTheme.typography.displaySmall
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = container.name,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(container.color)
|
||||
)
|
||||
Text(
|
||||
text = "${container.amount}ml",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(container.color).copy(alpha = 0.8f)
|
||||
)
|
||||
Icon(Icons.Default.WaterDrop, contentDescription = null)
|
||||
Text("${amount}ml")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WaterRecordItem(
|
||||
record: com.example.stepdrink.data.local.entity.WaterRecord,
|
||||
container: com.example.stepdrink.viewmodel.WaterContainer?,
|
||||
amount: Int,
|
||||
timestamp: Long,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
|
||||
val time = timeFormat.format(Date(timestamp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(1.dp)
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@ -484,51 +199,31 @@ fun WaterRecordItem(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = container?.emoji ?: "💧",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
Icon(
|
||||
imageVector = Icons.Default.WaterDrop,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
text = container?.name ?: "Custom",
|
||||
text = "${amount}ml",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "${record.amount}ml",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text("•", style = MaterialTheme.typography.bodySmall)
|
||||
Text(
|
||||
text = SimpleDateFormat("HH:mm", Locale.getDefault())
|
||||
.format(Date(record.timestamp)),
|
||||
text = time,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Hapus",
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
@ -538,7 +233,7 @@ fun WaterRecordItem(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CustomAmountDialog(
|
||||
fun AddWaterDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (Int) -> Unit
|
||||
) {
|
||||
@ -546,24 +241,21 @@ fun CustomAmountDialog(
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
icon = { Icon(Icons.Default.WaterDrop, contentDescription = null) },
|
||||
title = { Text("Input Jumlah Air") },
|
||||
title = { Text("Tambah Air Minum") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = amount,
|
||||
onValueChange = { amount = it.filter { char -> char.isDigit() } },
|
||||
label = { Text("Jumlah (ml)") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
suffix = { Text("ml") }
|
||||
singleLine = true
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
TextButton(
|
||||
onClick = {
|
||||
val value = amount.toIntOrNull()
|
||||
if (value != null && value > 0) {
|
||||
onConfirm(value)
|
||||
val amountInt = amount.toIntOrNull()
|
||||
if (amountInt != null && amountInt > 0) {
|
||||
onConfirm(amountInt)
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
@ -1,214 +1,46 @@
|
||||
package com.example.stepdrink.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
// Color palette dari icon
|
||||
val TealPrimary = Color(0xFF7BC8BC) // Mint/Teal utama
|
||||
val TealLight = Color(0xFFA8E6CF) // Light mint
|
||||
val TealDark = Color(0xFF4A9B8E) // Dark teal
|
||||
val BluePrimary = Color(0xFF5DADE2) // Blue dari gradient
|
||||
val NavyAccent = Color(0xFF2C3E50) // Dark blue accent
|
||||
val OrangeAccent = Color(0xFFF39C12) // Orange highlight
|
||||
val Background = Color(0xFFF5F9F8) // Very light teal background
|
||||
|
||||
// Light Theme Colors
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = TealPrimary,
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = TealLight,
|
||||
onPrimaryContainer = NavyAccent,
|
||||
|
||||
secondary = BluePrimary,
|
||||
onSecondary = Color.White,
|
||||
secondaryContainer = Color(0xFFE3F2FD),
|
||||
onSecondaryContainer = NavyAccent,
|
||||
|
||||
tertiary = OrangeAccent,
|
||||
onTertiary = Color.White,
|
||||
tertiaryContainer = Color(0xFFFFE6CC),
|
||||
onTertiaryContainer = Color(0xFF8B5A00),
|
||||
|
||||
background = Background,
|
||||
onBackground = NavyAccent,
|
||||
|
||||
surface = Color.White,
|
||||
onSurface = NavyAccent,
|
||||
surfaceVariant = Color(0xFFE8F5F3),
|
||||
onSurfaceVariant = Color(0xFF5A6A6C),
|
||||
|
||||
error = Color(0xFFE74C3C),
|
||||
onError = Color.White,
|
||||
errorContainer = Color(0xFFFFDAD6),
|
||||
onErrorContainer = Color(0xFF93000A),
|
||||
|
||||
outline = Color(0xFFB0BEC5),
|
||||
outlineVariant = Color(0xFFCFD8DC)
|
||||
)
|
||||
|
||||
// Dark Theme Colors
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = TealPrimary,
|
||||
onPrimary = NavyAccent,
|
||||
primaryContainer = TealDark,
|
||||
onPrimaryContainer = TealLight,
|
||||
|
||||
secondary = BluePrimary,
|
||||
onSecondary = NavyAccent,
|
||||
secondaryContainer = Color(0xFF1E3A5F),
|
||||
onSecondaryContainer = Color(0xFFB3D9FF),
|
||||
|
||||
tertiary = OrangeAccent,
|
||||
onTertiary = NavyAccent,
|
||||
tertiaryContainer = Color(0xFF8B5A00),
|
||||
onTertiaryContainer = Color(0xFFFFD9A3),
|
||||
|
||||
background = Color(0xFF1A2329),
|
||||
onBackground = Color(0xFFE1E8E8),
|
||||
|
||||
surface = Color(0xFF1F2A30),
|
||||
onSurface = Color(0xFFE1E8E8),
|
||||
surfaceVariant = Color(0xFF2C3E50),
|
||||
onSurfaceVariant = Color(0xFFB8C5C8),
|
||||
|
||||
error = Color(0xFFFFB4AB),
|
||||
onError = Color(0xFF690005),
|
||||
errorContainer = Color(0xFF93000A),
|
||||
onErrorContainer = Color(0xFFFFDAD6),
|
||||
|
||||
outline = Color(0xFF8B9A9D),
|
||||
outlineVariant = Color(0xFF3F5054)
|
||||
primary = androidx.compose.ui.graphics.Color(0xFF90CAF9),
|
||||
secondary = androidx.compose.ui.graphics.Color(0xFF81C784),
|
||||
tertiary = androidx.compose.ui.graphics.Color(0xFF64B5F6)
|
||||
)
|
||||
|
||||
// Typography untuk Material3
|
||||
private val AppTypography = androidx.compose.material3.Typography(
|
||||
displayLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 57.sp,
|
||||
lineHeight = 64.sp,
|
||||
letterSpacing = (-0.25).sp
|
||||
),
|
||||
displayMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 45.sp,
|
||||
lineHeight = 52.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
displaySmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 36.sp,
|
||||
lineHeight = 44.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 40.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 36.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 32.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.15.sp
|
||||
),
|
||||
titleSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp
|
||||
),
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.25.sp
|
||||
),
|
||||
bodySmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.4.sp
|
||||
),
|
||||
labelLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp
|
||||
),
|
||||
labelMedium = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = androidx.compose.ui.graphics.Color(0xFF1976D2),
|
||||
secondary = androidx.compose.ui.graphics.Color(0xFF388E3C),
|
||||
tertiary = androidx.compose.ui.graphics.Color(0xFF0288D1)
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun StepDrinkTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = false, // Disable dynamic color untuk konsistensi
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
|
||||
val view = LocalView.current
|
||||
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
window.statusBarColor = colorScheme.primary.toArgb()
|
||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = AppTypography, // Gunakan AppTypography yang sudah didefinisikan
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@ -1,168 +0,0 @@
|
||||
package com.example.stepdrink.util
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.work.*
|
||||
import com.example.stepdrink.MainActivity
|
||||
import com.example.stepdrink.R
|
||||
import com.example.stepdrink.data.local.database.AppDatabase
|
||||
import com.example.stepdrink.data.repository.WaterRepository
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* WaterReminderManager - Mengelola reminder untuk minum air
|
||||
*
|
||||
* Fitur:
|
||||
* - Cek otomatis setiap 30 menit
|
||||
* - Kirim notifikasi jika sudah 3 jam tidak minum
|
||||
* - Hanya aktif jam 6 pagi - 10 malam
|
||||
* - Motivational messages yang bervariasi
|
||||
*/
|
||||
class WaterReminderManager( val context: Context) {
|
||||
|
||||
companion object {
|
||||
const val WORK_NAME = "WaterReminderWork"
|
||||
const val CHANNEL_ID = "water_reminder_channel"
|
||||
const val NOTIFICATION_ID = 1001
|
||||
|
||||
// Reminder settings
|
||||
const val CHECK_INTERVAL_MINUTES = 30L // Cek setiap 30 menit
|
||||
const val REMIND_AFTER_HOURS = 3 // Remind setelah 3 jam tidak minum
|
||||
const val ACTIVE_START_HOUR = 6 // Mulai jam 6 pagi
|
||||
const val ACTIVE_END_HOUR = 22 // Sampai jam 10 malam
|
||||
}
|
||||
|
||||
fun scheduleReminder() {
|
||||
createNotificationChannel()
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiresBatteryNotLow(true) // Jangan ganggu kalau battery low
|
||||
.build()
|
||||
|
||||
val reminderWork = PeriodicWorkRequestBuilder<WaterReminderWorker>(
|
||||
CHECK_INTERVAL_MINUTES, TimeUnit.MINUTES
|
||||
)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.KEEP, // Keep existing jika sudah ada
|
||||
reminderWork
|
||||
)
|
||||
}
|
||||
|
||||
fun cancelReminder() {
|
||||
WorkManager.getInstance(context).cancelUniqueWork(WORK_NAME)
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Pengingat Minum Air",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
description = "Notifikasi pengingat untuk minum air"
|
||||
enableVibration(true)
|
||||
}
|
||||
|
||||
val notificationManager = context.getSystemService(NotificationManager::class.java)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Worker yang menjalankan logic reminder
|
||||
*/
|
||||
class WaterReminderWorker(
|
||||
context: Context,
|
||||
params: WorkerParameters
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
return try {
|
||||
checkAndSendReminder()
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkAndSendReminder() {
|
||||
// Cek apakah dalam jam aktif
|
||||
val currentHour = java.util.Calendar.getInstance().get(java.util.Calendar.HOUR_OF_DAY)
|
||||
if (currentHour < WaterReminderManager.ACTIVE_START_HOUR ||
|
||||
currentHour >= WaterReminderManager.ACTIVE_END_HOUR) {
|
||||
return // Jangan ganggu di luar jam aktif
|
||||
}
|
||||
|
||||
// Get last drink time dari database
|
||||
val database = AppDatabase.getDatabase(applicationContext)
|
||||
val repository = WaterRepository(database.waterDao())
|
||||
val todayRecords = repository.getWaterByDate(DateUtils.getCurrentDate()).first()
|
||||
|
||||
if (todayRecords.isEmpty()) {
|
||||
// Belum minum sama sekali hari ini
|
||||
if (currentHour >= 8) { // Kalau udah jam 8 pagi
|
||||
sendReminderNotification(
|
||||
"Yuk Mulai Minum Air! 💧",
|
||||
"Kamu belum minum air hari ini. Tubuhmu butuh hidrasi!"
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Cek waktu terakhir minum
|
||||
val lastDrinkTime = todayRecords.maxOf { it.timestamp }
|
||||
val hoursSinceLastDrink = (System.currentTimeMillis() - lastDrinkTime) / (1000 * 60 * 60)
|
||||
|
||||
if (hoursSinceLastDrink >= WaterReminderManager.REMIND_AFTER_HOURS) {
|
||||
// Sudah 3 jam tidak minum!
|
||||
val message = getMotivationalMessage(hoursSinceLastDrink.toInt())
|
||||
sendReminderNotification("Waktunya Minum Air! 💧", message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMotivationalMessage(hoursSince: Int): String {
|
||||
return when {
|
||||
hoursSince >= 5 -> "Sudah ${hoursSince} jam tidak minum! Tubuhmu sangat butuh air sekarang! 🚨"
|
||||
hoursSince >= 4 -> "Sudah ${hoursSince} jam tidak minum. Yuk hidrasi sekarang! ⏰"
|
||||
else -> "Sudah ${hoursSince} jam tidak minum. Jangan lupa minum air ya! 💙"
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendReminderNotification(title: String, message: String) {
|
||||
val intent = Intent(applicationContext, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
putExtra("open_water_screen", true) // Optional: buka langsung water screen
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
applicationContext,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(applicationContext, WaterReminderManager.CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_water_notification)
|
||||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setVibrate(longArrayOf(0, 500, 200, 500)) // Vibration pattern
|
||||
.build()
|
||||
|
||||
val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.notify(WaterReminderManager.NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
@ -3,79 +3,47 @@ package com.example.stepdrink.viewmodel
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.stepdrink.data.local.PreferencesManager
|
||||
import com.example.stepdrink.data.local.database.AppDatabase
|
||||
import com.example.stepdrink.data.local.PreferencesManager
|
||||
import com.example.stepdrink.data.local.entity.WaterRecord
|
||||
import com.example.stepdrink.data.repository.WaterRepository
|
||||
import com.example.stepdrink.util.DateUtils
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
// Data class untuk preset containers
|
||||
data class WaterContainer(
|
||||
val emoji: String,
|
||||
val name: String,
|
||||
val amount: Int,
|
||||
val color: Long = 0xFF2196F3
|
||||
)
|
||||
|
||||
class WaterViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val repository: WaterRepository
|
||||
private val repository: WaterRepository = WaterRepository(
|
||||
AppDatabase.getDatabase(application).waterDao()
|
||||
)
|
||||
|
||||
private val preferencesManager = PreferencesManager(application)
|
||||
|
||||
val dailyGoal: StateFlow<Int> = preferencesManager.waterGoal
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 2000)
|
||||
|
||||
val todayWaterRecords: StateFlow<List<WaterRecord>>
|
||||
val todayTotalWater: StateFlow<Int>
|
||||
|
||||
// SIMPLE CONTAINERS - Kategori yang clear & sederhana
|
||||
val presetContainers = listOf(
|
||||
WaterContainer("🥛", "Gelas Kecil", 200, 0xFF64B5F6), // Small glass
|
||||
WaterContainer("💧", "Gelas Sedang", 250, 0xFF42A5F5), // Medium glass (STANDARD)
|
||||
WaterContainer("🌊", "Gelas Besar", 350, 0xFF2196F3), // Large glass
|
||||
WaterContainer("🚰", "Botol Aqua", 600, 0xFF1976D2), // Aqua bottle (MOST COMMON)
|
||||
WaterContainer("🫗", "Tumbler", 500, 0xFF1565C0), // Tumbler
|
||||
)
|
||||
|
||||
init {
|
||||
val database = AppDatabase.getDatabase(application)
|
||||
repository = WaterRepository(database.waterDao())
|
||||
|
||||
todayWaterRecords = repository.getWaterByDate(DateUtils.getCurrentDate())
|
||||
val todayWaterRecords: StateFlow<List<WaterRecord>> =
|
||||
repository.getWaterRecordsByDate(DateUtils.getCurrentDate())
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||
|
||||
todayTotalWater = todayWaterRecords.map { records ->
|
||||
records.sumOf { it.amount }
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0)
|
||||
}
|
||||
val todayTotalWater: StateFlow<Int> =
|
||||
repository.getTotalWaterByDate(DateUtils.getCurrentDate())
|
||||
.map { it ?: 0 }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0)
|
||||
|
||||
val last7DaysWater: StateFlow<List<WaterRecord>> =
|
||||
repository.getLast7DaysWater()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||
|
||||
fun addWater(amount: Int) {
|
||||
viewModelScope.launch {
|
||||
repository.insertWater(DateUtils.getCurrentDate(), amount)
|
||||
repository.addWaterRecord(DateUtils.getCurrentDate(), amount)
|
||||
}
|
||||
}
|
||||
|
||||
fun addWaterFromContainer(container: WaterContainer) {
|
||||
addWater(container.amount)
|
||||
}
|
||||
|
||||
fun deleteWater(record: WaterRecord) {
|
||||
fun deleteWaterRecord(record: WaterRecord) {
|
||||
viewModelScope.launch {
|
||||
repository.deleteWater(record)
|
||||
repository.deleteWaterRecord(record)
|
||||
}
|
||||
}
|
||||
|
||||
fun getProgressPercentage(): Float {
|
||||
return (todayTotalWater.value.toFloat() / dailyGoal.value.toFloat()).coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
fun getLastDrinkTime(): Long? {
|
||||
return todayWaterRecords.value.maxByOrNull { it.timestamp }?.timestamp
|
||||
}
|
||||
|
||||
fun getContainerByAmount(amount: Int): WaterContainer? {
|
||||
return presetContainers.find { it.amount == amount }
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 472 B |
|
Before Width: | Height: | Size: 339 B |
|
Before Width: | Height: | Size: 655 B |
|
Before Width: | Height: | Size: 1023 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 75 KiB |
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 982 B |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 7.6 KiB |