diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6161d07..07fe8ef 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,4 +1,5 @@ plugins { + id("com.google.devtools.ksp") alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) @@ -37,6 +38,13 @@ android { } dependencies { + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + ksp(libs.androidx.room.compiler) + + // Für Coroutines Support + implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) diff --git a/app/src/main/java/de/webfussel/soulecho/MainActivity.kt b/app/src/main/java/de/webfussel/soulecho/MainActivity.kt index 71e4ddc..32b1a76 100644 --- a/app/src/main/java/de/webfussel/soulecho/MainActivity.kt +++ b/app/src/main/java/de/webfussel/soulecho/MainActivity.kt @@ -4,30 +4,36 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* +import androidx.lifecycle.viewmodel.compose.viewModel +import de.webfussel.soulecho.data.database.MoodDatabase +import de.webfussel.soulecho.data.repository.MoodRepository import de.webfussel.soulecho.mood.MoodSection import de.webfussel.soulecho.mood.MoodWithInfo import de.webfussel.soulecho.mood.PossibleMood import de.webfussel.soulecho.navigation.Drawer import de.webfussel.soulecho.ui.theme.SoulEchoTheme +import de.webfussel.soulecho.viewmodel.MoodViewModel +import de.webfussel.soulecho.viewmodel.MoodViewModelFactory +import kotlinx.coroutines.launch val LocalMoodState = compositionLocalOf { error("No MoodState provided") } @Composable -fun SoulEchoApp() { +fun SoulEchoApp(moodViewModel: MoodViewModel) { var currentMood by remember { mutableStateOf(MoodWithInfo(mood = PossibleMood.HAPPY, info = "")) } + val coroutineScope = rememberCoroutineScope() CompositionLocalProvider(LocalMoodState provides currentMood) { SoulEchoTheme () { Drawer () { MoodSection( - onMoodChange = { currentMood = it } + onMoodChange = { + currentMood = it + coroutineScope.launch { + moodViewModel.saveMood(it) + } + } ) } } @@ -35,11 +41,21 @@ fun SoulEchoApp() { } class MainActivity : ComponentActivity() { + private lateinit var moodRepository: MoodRepository + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + val database = MoodDatabase.getDatabase(this) + moodRepository = MoodRepository(database.moodDao()) + enableEdgeToEdge() setContent { - SoulEchoApp() + val moodViewModel: MoodViewModel = viewModel( + factory = MoodViewModelFactory(moodRepository) + ) + + SoulEchoApp(moodViewModel) } } } \ No newline at end of file diff --git a/app/src/main/java/de/webfussel/soulecho/data/converter/Converters.kt b/app/src/main/java/de/webfussel/soulecho/data/converter/Converters.kt new file mode 100644 index 0000000..528b7e6 --- /dev/null +++ b/app/src/main/java/de/webfussel/soulecho/data/converter/Converters.kt @@ -0,0 +1,29 @@ +package de.webfussel.soulecho.data.converter + +import androidx.room.TypeConverter +import de.webfussel.soulecho.mood.PossibleMood +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class Converters { + + @TypeConverter + fun fromPossibleMood(mood: PossibleMood): String { + return mood.name + } + + @TypeConverter + fun toPossibleMood(moodString: String): PossibleMood { + return PossibleMood.valueOf(moodString) + } + + @TypeConverter + fun fromLocalDateTime(dateTime: LocalDateTime): String { + return dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + } + + @TypeConverter + fun toLocalDateTime(dateTimeString: String): LocalDateTime { + return LocalDateTime.parse(dateTimeString, DateTimeFormatter.ISO_LOCAL_DATE_TIME) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/webfussel/soulecho/data/dao/MoodDao.kt b/app/src/main/java/de/webfussel/soulecho/data/dao/MoodDao.kt new file mode 100644 index 0000000..456fc46 --- /dev/null +++ b/app/src/main/java/de/webfussel/soulecho/data/dao/MoodDao.kt @@ -0,0 +1,34 @@ +package de.webfussel.soulecho.data.dao + +import androidx.room.* +import de.webfussel.soulecho.data.entity.MoodEntity +import de.webfussel.soulecho.mood.PossibleMood +import kotlinx.coroutines.flow.Flow + +@Dao +interface MoodDao { + + @Insert + suspend fun insertMood(mood: MoodEntity): Long + + @Query("SELECT * FROM moods ORDER BY timestamp DESC") + fun getAllMoods(): Flow> + + @Query("SELECT * FROM moods WHERE id = :id") + suspend fun getMoodById(id: Long): MoodEntity? + + @Query("SELECT * FROM moods WHERE mood = :mood ORDER BY timestamp DESC") + fun getMoodsByType(mood: PossibleMood): Flow> + + @Query("SELECT * FROM moods WHERE DATE(timestamp) = DATE('now', 'localtime') ORDER BY timestamp DESC") + fun getTodaysMoods(): Flow> + + @Update + suspend fun updateMood(mood: MoodEntity) + + @Delete + suspend fun deleteMood(mood: MoodEntity) + + @Query("DELETE FROM moods") + suspend fun deleteAllMoods() +} \ No newline at end of file diff --git a/app/src/main/java/de/webfussel/soulecho/data/database/MoodDatabase.kt b/app/src/main/java/de/webfussel/soulecho/data/database/MoodDatabase.kt new file mode 100644 index 0000000..8811554 --- /dev/null +++ b/app/src/main/java/de/webfussel/soulecho/data/database/MoodDatabase.kt @@ -0,0 +1,38 @@ +package de.webfussel.soulecho.data.database + +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import android.content.Context +import de.webfussel.soulecho.data.converter.Converters +import de.webfussel.soulecho.data.dao.MoodDao +import de.webfussel.soulecho.data.entity.MoodEntity + +@Database( + entities = [MoodEntity::class], + version = 1, + exportSchema = false +) +@TypeConverters(Converters::class) +abstract class MoodDatabase : RoomDatabase() { + + abstract fun moodDao(): MoodDao + + companion object { + @Volatile + private var INSTANCE: MoodDatabase? = null + + fun getDatabase(context: Context): MoodDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + MoodDatabase::class.java, + "mood_database" + ).build() + INSTANCE = instance + instance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/webfussel/soulecho/data/entity/MoodEntity.kt b/app/src/main/java/de/webfussel/soulecho/data/entity/MoodEntity.kt new file mode 100644 index 0000000..e4a7f48 --- /dev/null +++ b/app/src/main/java/de/webfussel/soulecho/data/entity/MoodEntity.kt @@ -0,0 +1,15 @@ +package de.webfussel.soulecho.data.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey +import de.webfussel.soulecho.mood.PossibleMood +import java.time.LocalDateTime + +@Entity(tableName = "moods") +data class MoodEntity( + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + val mood: PossibleMood, + val info: String, + val timestamp: LocalDateTime = LocalDateTime.now() +) \ No newline at end of file diff --git a/app/src/main/java/de/webfussel/soulecho/data/repository/MoodRepository.kt b/app/src/main/java/de/webfussel/soulecho/data/repository/MoodRepository.kt new file mode 100644 index 0000000..8aa6834 --- /dev/null +++ b/app/src/main/java/de/webfussel/soulecho/data/repository/MoodRepository.kt @@ -0,0 +1,51 @@ +package de.webfussel.soulecho.data.repository + +import de.webfussel.soulecho.data.dao.MoodDao +import de.webfussel.soulecho.data.entity.MoodEntity +import de.webfussel.soulecho.mood.MoodWithInfo +import de.webfussel.soulecho.mood.PossibleMood +import kotlinx.coroutines.flow.Flow + +class MoodRepository(private val moodDao: MoodDao) { + + suspend fun saveMood(moodWithInfo: MoodWithInfo): Long { + val moodEntity = MoodEntity( + mood = moodWithInfo.mood, + info = moodWithInfo.info + ) + return moodDao.insertMood(moodEntity) + } + + fun getAllMoods(): Flow> { + return moodDao.getAllMoods() + } + + fun getMoodsByType(mood: PossibleMood): Flow> { + return moodDao.getMoodsByType(mood) + } + + fun getTodaysMoods(): Flow> { + return moodDao.getTodaysMoods() + } + + suspend fun getMoodById(id: Long): MoodEntity? { + return moodDao.getMoodById(id) + } + + suspend fun updateMood(moodEntity: MoodEntity) { + moodDao.updateMood(moodEntity) + } + + suspend fun deleteMood(moodEntity: MoodEntity) { + moodDao.deleteMood(moodEntity) + } + + suspend fun deleteAllMoods() { + moodDao.deleteAllMoods() + } + + // Hilfsmethode um MoodEntity zu MoodWithInfo zu konvertieren + fun MoodEntity.toMoodWithInfo(): MoodWithInfo { + return MoodWithInfo(mood = this.mood, info = this.info) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/webfussel/soulecho/mood/Choice.kt b/app/src/main/java/de/webfussel/soulecho/mood/Choice.kt index a872c08..eaf0882 100644 --- a/app/src/main/java/de/webfussel/soulecho/mood/Choice.kt +++ b/app/src/main/java/de/webfussel/soulecho/mood/Choice.kt @@ -1,11 +1,9 @@ package de.webfussel.soulecho.mood -import android.inputmethodservice.Keyboard import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -22,8 +20,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf diff --git a/app/src/main/java/de/webfussel/soulecho/mood/Section.kt b/app/src/main/java/de/webfussel/soulecho/mood/Section.kt index 35abb39..03a0df1 100644 --- a/app/src/main/java/de/webfussel/soulecho/mood/Section.kt +++ b/app/src/main/java/de/webfussel/soulecho/mood/Section.kt @@ -51,24 +51,46 @@ fun MoodSection( Text(mood.label, fontWeight = FontWeight.Bold, fontSize = 32.sp) Text(currentMood.info) } - FloatingActionButton( - containerColor = theme.primary, - onClick = { - dialogOpen = true - }, - modifier = Modifier - .align(Alignment.BottomEnd) - .size(100.dp) - .padding(24.dp), - - ) { - Icon( - painter = painterResource(R.drawable.face_smile_plus), - contentDescription = "Change Mood", + Column ( + horizontalAlignment = Alignment.End, + modifier = Modifier.align(Alignment.BottomEnd), + ) { + FloatingActionButton( + containerColor = theme.primary, + onClick = { + dialogOpen = true + }, modifier = Modifier - .size(32.dp) - .padding(start = 4.dp) - ) + .size(64.dp) + .padding(end = 24.dp, bottom = 24.dp), + + ) { + Icon( + painter = painterResource(R.drawable.pen), + contentDescription = "Change Mood", + modifier = Modifier + .size(32.dp) + .padding(6.dp) + ) + } + FloatingActionButton( + containerColor = theme.primary, + onClick = { + dialogOpen = true + }, + modifier = Modifier + .size(80.dp) + .padding(end = 24.dp, bottom = 24.dp), + + ) { + Icon( + painter = painterResource(R.drawable.face_smile_plus), + contentDescription = "Change Mood", + modifier = Modifier + .size(32.dp) + .padding(start = 4.dp) + ) + } } when { dialogOpen -> MoodChoice( diff --git a/app/src/main/java/de/webfussel/soulecho/viewmodel/MoodViewModel.kt b/app/src/main/java/de/webfussel/soulecho/viewmodel/MoodViewModel.kt new file mode 100644 index 0000000..18dd63e --- /dev/null +++ b/app/src/main/java/de/webfussel/soulecho/viewmodel/MoodViewModel.kt @@ -0,0 +1,57 @@ +package de.webfussel.soulecho.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import de.webfussel.soulecho.data.entity.MoodEntity +import de.webfussel.soulecho.data.repository.MoodRepository +import de.webfussel.soulecho.mood.MoodWithInfo +import de.webfussel.soulecho.mood.PossibleMood +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +class MoodViewModel(private val repository: MoodRepository) : ViewModel() { + + val allMoods: Flow> = repository.getAllMoods() + val todaysMoods: Flow> = repository.getTodaysMoods() + + suspend fun saveMood(moodWithInfo: MoodWithInfo): Long { + return repository.saveMood(moodWithInfo) + } + + fun getMoodsByType(mood: PossibleMood): Flow> { + return repository.getMoodsByType(mood) + } + + suspend fun getMoodById(id: Long): MoodEntity? { + return repository.getMoodById(id) + } + + fun updateMood(moodEntity: MoodEntity) { + viewModelScope.launch { + repository.updateMood(moodEntity) + } + } + + fun deleteMood(moodEntity: MoodEntity) { + viewModelScope.launch { + repository.deleteMood(moodEntity) + } + } + + fun deleteAllMoods() { + viewModelScope.launch { + repository.deleteAllMoods() + } + } +} + +class MoodViewModelFactory(private val repository: MoodRepository) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(MoodViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return MoodViewModel(repository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bars_staggered.xml b/app/src/main/res/drawable/bars_staggered.xml index e842729..b3d680d 100644 --- a/app/src/main/res/drawable/bars_staggered.xml +++ b/app/src/main/res/drawable/bars_staggered.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/calendar_days.xml b/app/src/main/res/drawable/calendar_days.xml index da29040..9fa9030 100644 --- a/app/src/main/res/drawable/calendar_days.xml +++ b/app/src/main/res/drawable/calendar_days.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/clock_rotate_left.xml b/app/src/main/res/drawable/clock_rotate_left.xml index 1124611..02de015 100644 --- a/app/src/main/res/drawable/clock_rotate_left.xml +++ b/app/src/main/res/drawable/clock_rotate_left.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/face_anxious_sweat.xml b/app/src/main/res/drawable/face_anxious_sweat.xml index 57be584..9bca1eb 100644 --- a/app/src/main/res/drawable/face_anxious_sweat.xml +++ b/app/src/main/res/drawable/face_anxious_sweat.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/face_confounded.xml b/app/src/main/res/drawable/face_confounded.xml index 5935700..d4ab2cb 100644 --- a/app/src/main/res/drawable/face_confounded.xml +++ b/app/src/main/res/drawable/face_confounded.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/face_disappointed.xml b/app/src/main/res/drawable/face_disappointed.xml index 3ae086a..eb3bd4b 100644 --- a/app/src/main/res/drawable/face_disappointed.xml +++ b/app/src/main/res/drawable/face_disappointed.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/face_dotted.xml b/app/src/main/res/drawable/face_dotted.xml index 24c3570..e544ef6 100644 --- a/app/src/main/res/drawable/face_dotted.xml +++ b/app/src/main/res/drawable/face_dotted.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/face_frown.xml b/app/src/main/res/drawable/face_frown.xml index 9067f96..a21ae92 100644 --- a/app/src/main/res/drawable/face_frown.xml +++ b/app/src/main/res/drawable/face_frown.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/face_meh.xml b/app/src/main/res/drawable/face_meh.xml index 2fc84ef..bb0909b 100644 --- a/app/src/main/res/drawable/face_meh.xml +++ b/app/src/main/res/drawable/face_meh.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/face_pouting.xml b/app/src/main/res/drawable/face_pouting.xml index 5837af5..a396c31 100644 --- a/app/src/main/res/drawable/face_pouting.xml +++ b/app/src/main/res/drawable/face_pouting.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/face_relieved.xml b/app/src/main/res/drawable/face_relieved.xml index 2a42e20..4753e10 100644 --- a/app/src/main/res/drawable/face_relieved.xml +++ b/app/src/main/res/drawable/face_relieved.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/face_scream.xml b/app/src/main/res/drawable/face_scream.xml index 3a2beb4..7caf523 100644 --- a/app/src/main/res/drawable/face_scream.xml +++ b/app/src/main/res/drawable/face_scream.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/face_smile.xml b/app/src/main/res/drawable/face_smile.xml index ad10318..4606834 100644 --- a/app/src/main/res/drawable/face_smile.xml +++ b/app/src/main/res/drawable/face_smile.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/face_smile_plus.xml b/app/src/main/res/drawable/face_smile_plus.xml index dbf9dbb..1cb65ce 100644 --- a/app/src/main/res/drawable/face_smile_plus.xml +++ b/app/src/main/res/drawable/face_smile_plus.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/face_smile_relaxed.xml b/app/src/main/res/drawable/face_smile_relaxed.xml index 93d5478..62012db 100644 --- a/app/src/main/res/drawable/face_smile_relaxed.xml +++ b/app/src/main/res/drawable/face_smile_relaxed.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/gear_code.xml b/app/src/main/res/drawable/gear_code.xml index a809f09..7ed70d4 100644 --- a/app/src/main/res/drawable/gear_code.xml +++ b/app/src/main/res/drawable/gear_code.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/gears.xml b/app/src/main/res/drawable/gears.xml index 17aa734..79bfc5d 100644 --- a/app/src/main/res/drawable/gears.xml +++ b/app/src/main/res/drawable/gears.xml @@ -1,7 +1,7 @@ - + - + diff --git a/app/src/main/res/drawable/pen.xml b/app/src/main/res/drawable/pen.xml new file mode 100644 index 0000000..53d4e49 --- /dev/null +++ b/app/src/main/res/drawable/pen.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/drawable/square_info.xml b/app/src/main/res/drawable/square_info.xml index 4f32e92..b23d647 100644 --- a/app/src/main/res/drawable/square_info.xml +++ b/app/src/main/res/drawable/square_info.xml @@ -1,7 +1,7 @@ - + - + diff --git a/build.gradle.kts b/build.gradle.kts index 952b930..f3bc0ed 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { + id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c9e697e..9e2cc10 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,12 +5,19 @@ coreKtx = "1.10.1" junit = "4.13.2" junitVersion = "1.1.5" espressoCore = "3.5.1" +kotlinxCoroutinesAndroid = "1.7.3" lifecycleRuntimeKtx = "2.6.1" activityCompose = "1.8.0" composeBom = "2024.09.00" +lifecycleViewmodelCompose = "2.9.2" +roomRuntime = "2.7.2" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomRuntime" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomRuntime" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } @@ -24,6 +31,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }