diff --git a/README.md b/README.md
index 0b16c38..910fc88 100644
--- a/README.md
+++ b/README.md
@@ -1,187 +1,165 @@
-Step & Drink
-Aplikasi Android untuk tracking langkah harian dan kebutuhan air minum menggunakan Kotlin dan Jetpack Compose.
+# 💧🏃 Step & Drink
-📋 Deskripsi
-Step & Drink adalah aplikasi mobile yang membantu pengguna untuk:
+Aplikasi Android untuk tracking langkah harian dan asupan air minum.
-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
+[](https://kotlinlang.org)
+[](https://developer.android.com/jetpack/compose)
+[](https://developer.android.com)
-Aplikasi ini dibuat sebagai tugas akhir mata kuliah Pemrograman Bergerak dengan fokus pada penggunaan sensor hardware, database lokal, dan multi-halaman navigation.
+---
-✨ Fitur
-1. Home Screen
+## 📱 Tentang
-Dashboard dengan ringkasan aktivitas hari ini
-Card interaktif untuk langkah dan air minum
-Progress bar visual untuk tracking target
-Greeting dengan nama pengguna
+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
-2. Step Tracker
+---
-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
+## ✨ Fitur Utama
-3. Water Tracker
+### Step Counter
+- Real-time tracking dengan hardware sensor
+- Target harian (default 10,000 langkah)
+- History 7 hari
+- Start/Stop control
-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
+### Water Tracker
+Pilihan container cepat:
+- 🥛 Gelas Kecil (200ml)
+- 💧 Gelas Sedang (250ml)
+- 🌊 Gelas Besar (350ml)
+- 🚰 Botol Aqua (600ml)
+- 🫗 Tumbler (500ml)
-4. Profile & Settings
+Plus input custom & history lengkap.
-Edit nama pengguna
-Ubah target langkah harian
-Ubah target air minum harian
-Informasi aplikasi
+### Smart Reminder
+- Notifikasi otomatis setelah 3 jam tidak minum
+- Aktif jam 06:00 - 22:00
+- Battery efficient
+- Bisa di-toggle ON/OFF
+### Profile
+- Input nama & berat badan
+- Set target langkah & air harian
+- Data tersimpan lokal
-🛠️ Teknologi
-Bahasa & Framework
+---
-Kotlin - Bahasa pemrograman
-Jetpack Compose - UI Framework
-Material Design 3 - Design system
+## 🛠️ Teknologi
-Architecture
+**Core:**
+- Kotlin 100%
+- Jetpack Compose
+- Material Design 3
-MVVM (Model-View-ViewModel)
-Repository Pattern
-Clean Architecture
+**Architecture:**
+- MVVM Pattern
+- Room Database
+- DataStore Preferences
+- WorkManager
+- Coroutines & Flow
-Database & Storage
+---
-Room Database - Local database (SQLite)
-DataStore Preferences - Settings storage
+## 🚀 Instalasi
-Components
+```bash
+# Clone repository
+git clone https://github.com/HagaDalpintoGinting/Steps-Drink.git
-Navigation Compose - Multi-halaman navigation
-Sensor Manager - Akses hardware sensor (Step Counter)
-ViewModel - State management
-Kotlin Flow - Reactive data stream
-Coroutines - Asynchronous operations
+# Buka di Android Studio
+# Sync Gradle
+# Run aplikasi
+```
+**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
+---
-🗄️ Database
-step_records
-Menyimpan data langkah harian
+## 📖 Penggunaan
-id - Primary key
-date - Tanggal (yyyy-MM-dd)
-steps - Jumlah langkah
-timestamp - Waktu pencatatan
+### Setup
+1. Buka aplikasi
+2. Masuk ke Profile
+3. Input data pribadi & set target
-water_records
-Menyimpan data minum air
+### Track Langkah
+1. Buka halaman Steps
+2. Tap tombol Start
+3. Mulai berjalan
-id - Primary key
-date - Tanggal (yyyy-MM-dd)
-amount - Jumlah air (ml)
-timestamp - Waktu pencatatan
+### Track Air
+1. Buka halaman Water
+2. Tap container (contoh: 🚰 600ml)
+3. Atau klik "Input Custom" untuk jumlah lain
-Preferences (DataStore)
-Menyimpan pengaturan pengguna
+### Aktifkan Reminder
+1. Buka halaman Water
+2. Toggle "Pengingat Minum" ke ON
+3. Izinkan permission notifikasi
-user_name - Nama pengguna
-step_goal - Target langkah harian
-water_goal - Target air minum harian (ml)
+---
+## ⚙️ Konfigurasi
-🚀 Instalasi
-Requirements
+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
+```
-Android Studio (versi terbaru)
-Minimum SDK: API 26 (Android 8.0)
-Kotlin 1.9.22
+---
-Cara Menjalankan
+## 📝 Catatan
-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
+**Tested on:** Samsung Galaxy A35 5G (Android 14)
+**Limitations:**
+- Step counter butuh device fisik (emulator tidak support)
+- Data tersimpan lokal saja
+- Reminder bergantung pada battery optimization device
-📱 Cara Penggunaan
+**Troubleshooting:**
+- Steps tidak terhitung? Pastikan permission ACTIVITY_RECOGNITION diizinkan
+- Reminder tidak muncul? Check permission POST_NOTIFICATIONS dan pastikan jam aktif
-Tracking Langkah
+---
-Buka Step Tracker
-Klik tombol Play
-Izinkan permission
-Mulai berjalan
-Data otomatis tersimpan
+## 🎓 Project
+Tugas Akhir Pemrograman Bergerak dengan requirement:
+- ✅ Multi-screen
+- ✅ Hardware sensor
+- ✅ Local database
+- ✅ Kotlin & Jetpack Compose
-Catat Air Minum
+---
-Buka Water Tracker
-Klik quick add atau tombol +
-Input jumlah air
-Data tersimpan dengan timestamp
+## 👨💻 Developer
+**Haga Dalpinto Ginting**
-Ubah Target
+GitHub: [@HagaDalpintoGinting](https://github.com/HagaDalpintoGinting)
-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
+Made with ❤️ using Kotlin & 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
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 687caca..d1f5e24 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -51,6 +51,8 @@ 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")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index dc0ece6..d70af82 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,9 @@
+
+
+
>
+ fun getWaterByDate(date: String): Flow>
- @Query("SELECT SUM(amount) FROM water_records WHERE date = :date")
- fun getTotalWaterByDate(date: String): Flow
-
- @Query("SELECT * FROM water_records ORDER BY date DESC LIMIT 7")
+ /**
+ * Get last 7 days water records
+ */
+ @Query("SELECT * FROM water_records ORDER BY date DESC, timestamp DESC LIMIT 50")
fun getLast7DaysWater(): Flow>
- @Delete
- suspend fun deleteWaterRecord(waterRecord: WaterRecord)
+ /**
+ * Get all water records (for backup/export)
+ */
+ @Query("SELECT * FROM water_records ORDER BY date DESC, timestamp DESC")
+ fun getAllWater(): Flow>
+
+ /**
+ * Delete all water records (for reset)
+ */
+ @Query("DELETE FROM water_records")
+ suspend fun deleteAllWater()
}
\ No newline at end of file
diff --git a/app/src/main/java/data/repository/WaterRepository.kt b/app/src/main/java/data/repository/WaterRepository.kt
index 9dd2cc5..1b75e1c 100644
--- a/app/src/main/java/data/repository/WaterRepository.kt
+++ b/app/src/main/java/data/repository/WaterRepository.kt
@@ -6,23 +6,36 @@ import kotlinx.coroutines.flow.Flow
class WaterRepository(private val waterDao: WaterDao) {
- fun getWaterRecordsByDate(date: String): Flow> {
- return waterDao.getWaterRecordsByDate(date)
+ /**
+ * Get all water records for a specific date
+ */
+ fun getWaterByDate(date: String): Flow> {
+ return waterDao.getWaterByDate(date)
}
- fun getTotalWaterByDate(date: String): Flow {
- return waterDao.getTotalWaterByDate(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)
}
+ /**
+ * Delete water record
+ */
+ suspend fun deleteWater(record: WaterRecord) {
+ waterDao.deleteWater(record)
+ }
+
+ /**
+ * Get last 7 days water records
+ */
fun getLast7DaysWater(): Flow> {
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)
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/ui/navigation/NavGraph.kt b/app/src/main/java/ui/navigation/NavGraph.kt
index 79a2a49..7f8c60c 100644
--- a/app/src/main/java/ui/navigation/NavGraph.kt
+++ b/app/src/main/java/ui/navigation/NavGraph.kt
@@ -5,15 +5,20 @@ 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.ui.screen.profile.ProfileScreen
+import com.example.stepdrink.viewmodel.ProfileViewModel
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 NavGraph(
+fun AppNavigation(
navController: NavHostController,
stepViewModel: StepViewModel,
waterViewModel: WaterViewModel,
@@ -21,8 +26,14 @@ fun NavGraph(
) {
NavHost(
navController = navController,
- startDestination = Screen.Home.route
+ startDestination = Screen.Splash.route
) {
+ // Splash Screen
+ composable(Screen.Splash.route) {
+ SplashScreen(navController = navController)
+ }
+
+ // Home Screen
composable(Screen.Home.route) {
HomeScreen(
navController = navController,
@@ -32,6 +43,7 @@ fun NavGraph(
)
}
+ // Steps Screen
composable(Screen.Steps.route) {
StepsScreen(
navController = navController,
@@ -39,17 +51,20 @@ fun NavGraph(
)
}
+ // Water Screen
composable(Screen.Water.route) {
WaterScreen(
navController = navController,
viewModel = waterViewModel
)
}
+
+ // Profile Screen
composable(Screen.Profile.route) {
ProfileScreen(
navController = navController,
- viewModel = profileViewModel )
+ viewModel = profileViewModel
+ )
+ }
}
-}
-
}
\ No newline at end of file
diff --git a/app/src/main/java/ui/navigation/Screen.kt b/app/src/main/java/ui/navigation/Screen.kt
index 14ef829..f4bb82a 100644
--- a/app/src/main/java/ui/navigation/Screen.kt
+++ b/app/src/main/java/ui/navigation/Screen.kt
@@ -1,6 +1,7 @@
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")
diff --git a/app/src/main/java/ui/screen/splash/SplashScreen.kt b/app/src/main/java/ui/screen/splash/SplashScreen.kt
index 60f140d..c0c3586 100644
--- a/app/src/main/java/ui/screen/splash/SplashScreen.kt
+++ b/app/src/main/java/ui/screen/splash/SplashScreen.kt
@@ -1,2 +1,78 @@
-package ui.screen.splash
+package com.example.stepdrink.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
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ui/screen/water/WaterScreen.kt b/app/src/main/java/ui/screen/water/WaterScreen.kt
index 8d31cb0..c75e9db 100644
--- a/app/src/main/java/ui/screen/water/WaterScreen.kt
+++ b/app/src/main/java/ui/screen/water/WaterScreen.kt
@@ -1,17 +1,30 @@
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.*
@@ -22,11 +35,35 @@ fun WaterScreen(
navController: NavController,
viewModel: WaterViewModel
) {
- val todayRecords by viewModel.todayWaterRecords.collectAsState()
- val totalWater by viewModel.todayTotalWater.collectAsState()
- val waterGoal by viewModel.dailyGoal.collectAsState()
+ val context = LocalContext.current
+ val reminderManager = remember { WaterReminderManager(context) }
- var showDialog by remember { mutableStateOf(false) }
+ val todayWater by viewModel.todayTotalWater.collectAsState()
+ val waterGoal by viewModel.dailyGoal.collectAsState()
+ val todayRecords by viewModel.todayWaterRecords.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
+ }
+ }
Scaffold(
topBar = {
@@ -36,160 +73,408 @@ 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 ->
- LazyColumn(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues)
- .padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- item {
- // Water Progress Card
- Card(
- modifier = Modifier.fillMaxWidth(),
- colors = CardDefaults.cardColors(
- containerColor = MaterialTheme.colorScheme.tertiaryContainer
- )
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(24.dp),
- horizontalAlignment = Alignment.CenterHorizontally
+ Box(modifier = Modifier.fillMaxSize()) {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ // Main Progress Card
+ item {
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(containerColor = Color.Transparent),
+ elevation = CardDefaults.cardElevation(4.dp)
) {
- Icon(
- imageVector = Icons.Default.WaterDrop,
- contentDescription = null,
- modifier = Modifier.size(64.dp),
- tint = MaterialTheme.colorScheme.tertiary
- )
-
- Spacer(modifier = Modifier.height(16.dp))
-
- Text(
- text = "${totalWater}ml",
- style = MaterialTheme.typography.displayLarge,
- fontWeight = FontWeight.Bold
- )
-
- Text(
- text = "Air Minum Hari Ini",
- style = MaterialTheme.typography.titleMedium
- )
-
- Spacer(modifier = Modifier.height(16.dp))
-
- LinearProgressIndicator(
- progress = { totalWater.toFloat() / waterGoal.toFloat() },
+ Box(
modifier = Modifier
.fillMaxWidth()
- .height(8.dp),
+ .background(
+ Brush.verticalGradient(
+ listOf(Color(0xFF2196F3), Color(0xFF64B5F6))
+ )
+ )
+ .padding(24.dp)
+ ) {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Icon(
+ Icons.Default.WaterDrop,
+ contentDescription = null,
+ modifier = Modifier.size(64.dp),
+ tint = Color.White
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Text(
+ text = "${todayWater}ml",
+ style = MaterialTheme.typography.displayLarge,
+ fontWeight = FontWeight.Bold,
+ color = Color.White
+ )
+
+ Text(
+ text = "Air Minum Hari Ini",
+ style = MaterialTheme.typography.titleMedium,
+ color = Color.White.copy(alpha = 0.9f)
+ )
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ LinearProgressIndicator(
+ progress = { todayWater.toFloat() / waterGoal.toFloat() },
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(10.dp)
+ .clip(MaterialTheme.shapes.small),
+ color = Color.White,
+ trackColor = Color.White.copy(alpha = 0.3f)
+ )
+
+ Spacer(modifier = Modifier.height(8.dp))
+
+ Text(
+ text = "Target: ${waterGoal}ml",
+ style = MaterialTheme.typography.bodyMedium,
+ color = Color.White.copy(alpha = 0.9f)
+ )
+
+ 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)) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Icon(
+ Icons.Default.Notifications,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSecondaryContainer
+ )
+ Column {
+ Text(
+ "Pengingat Minum",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Text(
+ "Ingatkan jika 3 jam tidak minum",
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.7f)
+ )
+ }
+ }
- Spacer(modifier = Modifier.height(8.dp))
+ 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:",
+ 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(
- text = "Target: ${waterGoal}ml",
- style = MaterialTheme.typography.bodyMedium
+ "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 ->
+ WaterRecordItem(
+ record = record,
+ container = viewModel.getContainerByAmount(record.amount),
+ onDelete = { viewModel.deleteWater(record) }
)
}
}
}
- item {
- // Quick Add Buttons
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(8.dp)
+ // 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)
) {
- QuickAddButton(
- amount = 250,
- onClick = { viewModel.addWater(250) },
- modifier = Modifier.weight(1f)
- )
- QuickAddButton(
- amount = 500,
- onClick = { viewModel.addWater(500) },
- modifier = Modifier.weight(1f)
- )
- QuickAddButton(
- amount = 1000,
- onClick = { viewModel.addWater(1000) },
- modifier = Modifier.weight(1f)
- )
+ Box(contentAlignment = Alignment.Center) {
+ Text("💧", style = MaterialTheme.typography.displayLarge)
+ }
}
}
-
- item {
- Text(
- text = "Riwayat Hari Ini",
- style = MaterialTheme.typography.titleLarge,
- fontWeight = FontWeight.Bold
- )
- }
-
- items(todayRecords) { record ->
- WaterRecordItem(
- amount = record.amount,
- timestamp = record.timestamp,
- onDelete = { viewModel.deleteWaterRecord(record) }
- )
- }
}
}
- if (showDialog) {
- AddWaterDialog(
- onDismiss = { showDialog = false },
+ if (showCustomDialog) {
+ CustomAmountDialog(
+ onDismiss = { showCustomDialog = false },
onConfirm = { amount ->
viewModel.addWater(amount)
- showDialog = false
+ showCustomDialog = false
}
)
}
}
@Composable
-fun QuickAddButton(
- amount: Int,
- onClick: () -> Unit,
- modifier: Modifier = Modifier
+fun ContainerCard(
+ container: com.example.stepdrink.viewmodel.WaterContainer,
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit
) {
- OutlinedButton(
+ Card(
onClick = onClick,
- modifier = modifier
+ modifier = modifier.height(120.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = Color(container.color).copy(alpha = 0.1f)
+ ),
+ elevation = CardDefaults.cardElevation(2.dp)
) {
Column(
- horizontalAlignment = Alignment.CenterHorizontally
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(12.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
) {
- Icon(Icons.Default.WaterDrop, contentDescription = null)
- Text("${amount}ml")
+ 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)
+ )
}
}
}
@Composable
fun WaterRecordItem(
- amount: Int,
- timestamp: Long,
+ record: com.example.stepdrink.data.local.entity.WaterRecord,
+ container: com.example.stepdrink.viewmodel.WaterContainer?,
onDelete: () -> Unit
) {
- val timeFormat = remember { SimpleDateFormat("HH:mm", Locale.getDefault()) }
- val time = timeFormat.format(Date(timestamp))
-
Card(
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier.fillMaxWidth(),
+ elevation = CardDefaults.cardElevation(1.dp)
) {
Row(
modifier = Modifier
@@ -199,31 +484,51 @@ fun WaterRecordItem(
verticalAlignment = Alignment.CenterVertically
) {
Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(12.dp)
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ verticalAlignment = Alignment.CenterVertically
) {
- Icon(
- imageVector = Icons.Default.WaterDrop,
- contentDescription = null,
- tint = MaterialTheme.colorScheme.primary
- )
+ Surface(
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.primaryContainer,
+ modifier = Modifier.size(48.dp)
+ ) {
+ Box(contentAlignment = Alignment.Center) {
+ Text(
+ text = container?.emoji ?: "💧",
+ style = MaterialTheme.typography.titleLarge
+ )
+ }
+ }
Column {
Text(
- text = "${amount}ml",
+ text = container?.name ?: "Custom",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
- Text(
- text = time,
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
+ 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)),
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ }
}
}
IconButton(onClick = onDelete) {
Icon(
- imageVector = Icons.Default.Delete,
+ Icons.Default.Delete,
contentDescription = "Hapus",
tint = MaterialTheme.colorScheme.error
)
@@ -233,7 +538,7 @@ fun WaterRecordItem(
}
@Composable
-fun AddWaterDialog(
+fun CustomAmountDialog(
onDismiss: () -> Unit,
onConfirm: (Int) -> Unit
) {
@@ -241,21 +546,24 @@ fun AddWaterDialog(
AlertDialog(
onDismissRequest = onDismiss,
- title = { Text("Tambah Air Minum") },
+ icon = { Icon(Icons.Default.WaterDrop, contentDescription = null) },
+ title = { Text("Input Jumlah Air") },
text = {
OutlinedTextField(
value = amount,
onValueChange = { amount = it.filter { char -> char.isDigit() } },
label = { Text("Jumlah (ml)") },
- singleLine = true
+ singleLine = true,
+ modifier = Modifier.fillMaxWidth(),
+ suffix = { Text("ml") }
)
},
confirmButton = {
- TextButton(
+ Button(
onClick = {
- val amountInt = amount.toIntOrNull()
- if (amountInt != null && amountInt > 0) {
- onConfirm(amountInt)
+ val value = amount.toIntOrNull()
+ if (value != null && value > 0) {
+ onConfirm(value)
}
}
) {
diff --git a/app/src/main/java/ui/theme/theme.kt b/app/src/main/java/ui/theme/theme.kt
index 86930df..4384ee2 100644
--- a/app/src/main/java/ui/theme/theme.kt
+++ b/app/src/main/java/ui/theme/theme.kt
@@ -1,46 +1,214 @@
package com.example.stepdrink.ui.theme
-import android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.lightColorScheme
+import androidx.compose.material3.*
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.platform.LocalView
-import androidx.core.view.WindowCompat
+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
-private val DarkColorScheme = darkColorScheme(
- primary = androidx.compose.ui.graphics.Color(0xFF90CAF9),
- secondary = androidx.compose.ui.graphics.Color(0xFF81C784),
- tertiary = androidx.compose.ui.graphics.Color(0xFF64B5F6)
+// 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)
)
-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)
+// 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)
+)
+
+// 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
+ )
)
@Composable
fun StepDrinkTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
+ dynamicColor: Boolean = false, // Disable dynamic color untuk konsistensi
content: @Composable () -> Unit
) {
- 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
- }
+ val colorScheme = when {
+ darkTheme -> DarkColorScheme
+ else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
+ typography = AppTypography, // Gunakan AppTypography yang sudah didefinisikan
content = content
)
}
\ No newline at end of file
diff --git a/app/src/main/java/util/WaterReminder.kt b/app/src/main/java/util/WaterReminder.kt
new file mode 100644
index 0000000..a9778bc
--- /dev/null
+++ b/app/src/main/java/util/WaterReminder.kt
@@ -0,0 +1,168 @@
+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(
+ 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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/viewmodel/WaterViewModel.kt b/app/src/main/java/viewmodel/WaterViewModel.kt
index 017bac0..e6fc8db 100644
--- a/app/src/main/java/viewmodel/WaterViewModel.kt
+++ b/app/src/main/java/viewmodel/WaterViewModel.kt
@@ -3,47 +3,79 @@ package com.example.stepdrink.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
-import com.example.stepdrink.data.local.database.AppDatabase
import com.example.stepdrink.data.local.PreferencesManager
+import com.example.stepdrink.data.local.database.AppDatabase
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 = WaterRepository(
- AppDatabase.getDatabase(application).waterDao()
- )
-
+ private val repository: WaterRepository
private val preferencesManager = PreferencesManager(application)
val dailyGoal: StateFlow = preferencesManager.waterGoal
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 2000)
- val todayWaterRecords: StateFlow> =
- repository.getWaterRecordsByDate(DateUtils.getCurrentDate())
+ val todayWaterRecords: StateFlow>
+ val todayTotalWater: StateFlow
+
+ // 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())
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
- val todayTotalWater: StateFlow =
- repository.getTotalWaterByDate(DateUtils.getCurrentDate())
- .map { it ?: 0 }
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0)
-
- val last7DaysWater: StateFlow> =
- repository.getLast7DaysWater()
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
+ todayTotalWater = todayWaterRecords.map { records ->
+ records.sumOf { it.amount }
+ }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0)
+ }
fun addWater(amount: Int) {
viewModelScope.launch {
- repository.addWaterRecord(DateUtils.getCurrentDate(), amount)
+ repository.insertWater(DateUtils.getCurrentDate(), amount)
}
}
- fun deleteWaterRecord(record: WaterRecord) {
+ fun addWaterFromContainer(container: WaterContainer) {
+ addWater(container.amount)
+ }
+
+ fun deleteWater(record: WaterRecord) {
viewModelScope.launch {
- repository.deleteWaterRecord(record)
+ repository.deleteWater(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 }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-hdpi/ic_water_notification.png b/app/src/main/res/drawable-hdpi/ic_water_notification.png
new file mode 100644
index 0000000..6d14329
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_water_notification.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_water_notification.png b/app/src/main/res/drawable-mdpi/ic_water_notification.png
new file mode 100644
index 0000000..ba42936
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_water_notification.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_water_notification.png b/app/src/main/res/drawable-xhdpi/ic_water_notification.png
new file mode 100644
index 0000000..3bbf725
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_water_notification.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_water_notification.png b/app/src/main/res/drawable-xxhdpi/ic_water_notification.png
new file mode 100644
index 0000000..3f55c5f
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_water_notification.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_water_notification.png b/app/src/main/res/drawable-xxxhdpi/ic_water_notification.png
new file mode 100644
index 0000000..41dae26
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_water_notification.png differ
diff --git a/app/src/main/res/drawable/icon_apps_steps_drink.jpeg b/app/src/main/res/drawable/icon_apps_steps_drink.jpeg
new file mode 100644
index 0000000..3651f5f
Binary files /dev/null and b/app/src/main/res/drawable/icon_apps_steps_drink.jpeg differ
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 6f3b755..036d09b 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,6 +1,5 @@
-
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 6f3b755..036d09b 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,6 +1,5 @@
-
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
index c209e78..8f9f52d 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
index b2dfe3d..5f3a584 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
index 4f0f1d6..92bf645 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
index 62b611d..9a11665 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
index 948a307..24a024a 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
index 1b9a695..c1df355 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
index 28d4b77..70406f3 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
index 9287f50..e261f3c 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
index aa7d642..e0cbe12 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
index 9126ae3..735359c 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ