diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 31a863c..5b13f76 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -81,7 +81,7 @@ dependencies { implementation(libs.kotlinx.coroutines.android) // LiveData - implementation (libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.livedata.ktx) // Coroutine Lifecycle Scopes implementation(libs.androidx.lifecycle.viewmodel.ktx) @@ -99,7 +99,7 @@ dependencies { implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) implementation(libs.androidx.material3.window.size) - + implementation(libs.androidx.material.icons.extended) // Testing testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) diff --git a/app/src/main/java/com/braiso_22/cozycave/MainActivity.kt b/app/src/main/java/com/braiso_22/cozycave/MainActivity.kt index aea6aed..5e20627 100644 --- a/app/src/main/java/com/braiso_22/cozycave/MainActivity.kt +++ b/app/src/main/java/com/braiso_22/cozycave/MainActivity.kt @@ -9,7 +9,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.ui.Modifier -import com.braiso_22.cozycave.feature_task.ui.tasks.TasksScreen import com.braiso_22.cozycave.router.AppNavigation import com.braiso_22.cozycave.ui.theme.CozyCaveTheme import dagger.hilt.android.AndroidEntryPoint diff --git a/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/date_selector/DateSelector.kt b/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/date_selector/DateSelector.kt new file mode 100644 index 0000000..cf862bd --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/date_selector/DateSelector.kt @@ -0,0 +1,73 @@ +package com.braiso_22.cozycave.date_time_selector.presentation.date_selector + +import androidx.compose.foundation.clickable +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.braiso_22.cozycave.date_time_selector.presentation.date_selector.comps.DatePickerDialogWrapper +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +@Composable +fun DateSelector( + state: String, + setState: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val date = try { + LocalDate.parse(state, DateTimeFormatter.ofPattern("dd/MM/yyyy")) + } catch (e: Exception) { + LocalDate.now() + } + + DateSelectorComponent( + date = date, + setDate = { + setState( + it.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")) + ) + }, + modifier = modifier + ) +} + +@Composable +private fun DateSelectorComponent( + date: LocalDate, + setDate: (LocalDate) -> Unit, + modifier: Modifier = Modifier, +) { + var isDateOpen by remember { + mutableStateOf(false) + } + DatePickerDialogWrapper( + isVisible = isDateOpen, + setVisible = { isDateOpen = it }, + state = date.toEpochDay().times(86_400_000), + setState = { + setDate( + try { + LocalDate.ofEpochDay(it.div(86_400_000)) + } catch (e: Exception) { + LocalDate.now() + } + ) + } + ) + Text( + text = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")), + modifier = modifier.clickable { + isDateOpen = true + } + ) +} + +@Preview +@Composable +private fun DateSelectorPreview() { + DateSelector( + state = "", + setState = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/date_selector/comps/DatePickerDialogWrapper.kt b/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/date_selector/comps/DatePickerDialogWrapper.kt new file mode 100644 index 0000000..6e5fdfc --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/date_selector/comps/DatePickerDialogWrapper.kt @@ -0,0 +1,47 @@ +package com.braiso_22.cozycave.date_time_selector.presentation.date_selector.comps + +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.braiso_22.cozycave.R +import kotlin.math.absoluteValue + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DatePickerDialogWrapper( + isVisible: Boolean, + setVisible: (Boolean) -> Unit, + state: Long, + setState: (Long) -> Unit, + modifier: Modifier = Modifier, +) { + val datePickerState = rememberDatePickerState( + initialSelectedDateMillis = state + ) + + if (isVisible) { + DatePickerDialog( + onDismissRequest = { setVisible(false) }, + confirmButton = { + TextButton( + onClick = { + setVisible(false) + setState(datePickerState.selectedDateMillis?.absoluteValue ?: 0) + } + ) { + Text(stringResource(R.string.accept)) + } + + }, + dismissButton = { + TextButton(onClick = { setVisible(false) }) { + Text(stringResource(R.string.cancel)) + } + }, + modifier = modifier + ) { + DatePicker(state = datePickerState) + } + } +} diff --git a/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/time_selector/TimeSelector.kt b/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/time_selector/TimeSelector.kt new file mode 100644 index 0000000..e3c306c --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/time_selector/TimeSelector.kt @@ -0,0 +1,79 @@ +package com.braiso_22.cozycave.date_time_selector.presentation.time_selector + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.braiso_22.cozycave.date_time_selector.presentation.time_selector.comps.TimePickerDialogWrapper +import java.time.LocalTime +import java.time.format.DateTimeFormatter + +@Composable +fun TimeSelector( + state: String, + setState: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val time = try { + LocalTime.parse(state) + } catch (e: Exception) { + LocalTime.now() + } + + DateTimeSelectorContent( + time = time, + setTime = { + setState( + it.format(DateTimeFormatter.ofPattern("HH:mm")) + ) + }, + modifier = modifier + ) +} + +@Composable +private fun DateTimeSelectorContent( + time: LocalTime, + setTime: (LocalTime) -> Unit, + modifier: Modifier = Modifier, +) { + var isTimeOpen by remember { + mutableStateOf(false) + } + + TimePickerDialogWrapper( + isVisible = isTimeOpen, + setVisible = { isTimeOpen = it }, + state = TimeState(time.hour, time.minute), + setState = { + setTime(LocalTime.of(it.hour, it.minute)) + } + ) + + Text( + text = time.format(DateTimeFormatter.ofPattern("HH:mm")), + modifier = modifier.clickable { + isTimeOpen = true + } + ) +} + +@Preview +@Composable +private fun DateTimeSelectorPreview() { + Surface { + TimeSelector( + state = "", + setState = { + + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/time_selector/TimeState.kt b/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/time_selector/TimeState.kt new file mode 100644 index 0000000..2c4c25b --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/time_selector/TimeState.kt @@ -0,0 +1,6 @@ +package com.braiso_22.cozycave.date_time_selector.presentation.time_selector + +data class TimeState( + val hour: Int, + val minute: Int, +) diff --git a/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/time_selector/comps/TimePickerDialog.kt b/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/time_selector/comps/TimePickerDialog.kt new file mode 100644 index 0000000..c711ad8 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/time_selector/comps/TimePickerDialog.kt @@ -0,0 +1,91 @@ +package com.braiso_22.cozycave.date_time_selector.presentation.time_selector.comps + + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Keyboard +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.PreviewScreenSizes +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TimePickerDialog( + state: TimePickerState, + onDismissRequest: () -> Unit, + confirmButton: @Composable () -> Unit, + modifier: Modifier = Modifier, + dismissButton: @Composable (() -> Unit)? = null, +) { + var isTimePicker by remember { + mutableStateOf(true) + } + + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + usePlatformDefaultWidth = false, + ) + ) { + Surface( + shape = MaterialTheme.shapes.extraLarge, + modifier = Modifier + .width(IntrinsicSize.Min) + .height(IntrinsicSize.Min) + .background( + shape = MaterialTheme.shapes.extraLarge, + color = MaterialTheme.colorScheme.surface + ) + ) { + Column(modifier.padding(8.dp)) { + Text("Select a time", Modifier.padding(8.dp)) + if (isTimePicker) { + TimePicker(state = state) + } else { + TimeInput(state = state) + } + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + modifier = Modifier.fillMaxWidth() + ) { + IconButton(onClick = { isTimePicker = !isTimePicker }) { + Icon( + imageVector = Icons.Outlined.Keyboard, + "Change to write mode" + ) + } + if (dismissButton != null) { + dismissButton() + } + confirmButton() + } + } + } + } + +} + +@OptIn(ExperimentalMaterial3Api::class) +@PreviewScreenSizes +@Composable +private fun TimePickerPreview() { + Box(Modifier.fillMaxSize()) { + TimePickerDialog( + state = rememberTimePickerState(), + onDismissRequest = {}, + confirmButton = { + TextButton(onClick = { /*TODO*/ }) { + Text("Accept") + } + }, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/time_selector/comps/TimePickerDialogWrapper.kt b/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/time_selector/comps/TimePickerDialogWrapper.kt new file mode 100644 index 0000000..839862e --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/date_time_selector/presentation/time_selector/comps/TimePickerDialogWrapper.kt @@ -0,0 +1,51 @@ +package com.braiso_22.cozycave.date_time_selector.presentation.time_selector.comps + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberTimePickerState +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.braiso_22.cozycave.R +import com.braiso_22.cozycave.date_time_selector.presentation.time_selector.TimeState + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TimePickerDialogWrapper( + isVisible: Boolean, + setVisible: (Boolean) -> Unit, + state: TimeState, + setState: (TimeState) -> Unit, + modifier: Modifier = Modifier, +) { + val timePickerState = rememberTimePickerState( + is24Hour = true, + initialMinute = state.minute, + initialHour = state.hour, + ) + if (isVisible) { + TimePickerDialog( + state = timePickerState, + onDismissRequest = { setVisible(false) }, + confirmButton = { + TextButton( + onClick = { + setVisible(false) + setState( + TimeState(timePickerState.hour, timePickerState.minute) + ) + } + ) { + Text(stringResource(R.string.accept)) + } + }, + dismissButton = { + TextButton(onClick = { setVisible(false) }) { + Text(stringResource(R.string.cancel)) + } + }, + modifier = modifier + ) + } +} diff --git a/app/src/main/java/com/braiso_22/cozycave/di/AppDatabase.kt b/app/src/main/java/com/braiso_22/cozycave/di/AppDatabase.kt new file mode 100644 index 0000000..5b33555 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/di/AppDatabase.kt @@ -0,0 +1,25 @@ +package com.braiso_22.cozycave.di + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.braiso_22.cozycave.feature_execution.data.local.dao.ExecutionDao +import com.braiso_22.cozycave.feature_execution.data.local.entities.LocalExecution +import com.braiso_22.cozycave.feature_task.data.local.dao.TaskDao +import com.braiso_22.cozycave.feature_task.data.local.entities.LocalTask + +@Database( + entities = [ + LocalTask::class, + LocalExecution::class, + ], + version = 1, + exportSchema = false +) +abstract class AppDatabase : RoomDatabase() { + abstract val taskDao: TaskDao + abstract val executionDao: ExecutionDao + + companion object { + const val DATABASE_NAME = "cozycave_db" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/di/AppModule.kt b/app/src/main/java/com/braiso_22/cozycave/di/AppModule.kt index 1e072f5..0c91005 100644 --- a/app/src/main/java/com/braiso_22/cozycave/di/AppModule.kt +++ b/app/src/main/java/com/braiso_22/cozycave/di/AppModule.kt @@ -2,13 +2,16 @@ package com.braiso_22.cozycave.di import android.app.Application import androidx.room.Room +import com.braiso_22.cozycave.feature_execution.data.local.ExecutionRepositoryImpl +import com.braiso_22.cozycave.feature_execution.data.local.dao.ExecutionDao +import com.braiso_22.cozycave.feature_execution.domain.ExecutionRepository +import com.braiso_22.cozycave.feature_execution.domain.use_case.AddExecutionUseCase +import com.braiso_22.cozycave.feature_execution.domain.use_case.GetExecutionByIdUseCase +import com.braiso_22.cozycave.feature_execution.domain.use_case.GetExecutionsByRelatedIdUseCase import com.braiso_22.cozycave.feature_task.data.TaskRepositoryImpl -import com.braiso_22.cozycave.feature_task.data.local.db.TaskDatabase +import com.braiso_22.cozycave.feature_task.data.local.dao.TaskDao import com.braiso_22.cozycave.feature_task.domain.TaskRepository -import com.braiso_22.cozycave.feature_task.domain.use_case.AddTaskUseCase -import com.braiso_22.cozycave.feature_task.domain.use_case.DeleteTaskUseCase -import com.braiso_22.cozycave.feature_task.domain.use_case.GetTaskByIdUseCase -import com.braiso_22.cozycave.feature_task.domain.use_case.GetTasksUseCase +import com.braiso_22.cozycave.feature_task.domain.use_case.* import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,20 +23,23 @@ import javax.inject.Singleton object AppModule { @Provides @Singleton - fun provideTaskDatabase(app: Application): TaskDatabase { + fun provideTaskDatabase(app: Application): AppDatabase { return Room.databaseBuilder( app, - TaskDatabase::class.java, - TaskDatabase.DATABASE_NAME, + AppDatabase::class.java, + AppDatabase.DATABASE_NAME, ).build() } + @Provides + fun provideTaskDao(database: AppDatabase): TaskDao = database.taskDao + @Provides @Singleton fun provideTaskRepository( - taskDatabase: TaskDatabase, + dao: TaskDao, ): TaskRepository { - return TaskRepositoryImpl(taskDatabase.taskDao) + return TaskRepositoryImpl(dao) } @Provides @@ -67,4 +73,43 @@ object AppModule { ): GetTaskByIdUseCase { return GetTaskByIdUseCase(taskRepository) } -} \ No newline at end of file + + @Provides + fun provideExecutionDao( + appDatabase: AppDatabase, + ): ExecutionDao = appDatabase.executionDao + + @Provides + @Singleton + fun provideExecutionRepository( + dao: ExecutionDao, + ): ExecutionRepository = ExecutionRepositoryImpl(dao) + + @Provides + @Singleton + fun provideAddExecutionUseCase( + repository: ExecutionRepository, + ): AddExecutionUseCase = AddExecutionUseCase(repository) + + @Provides + @Singleton + fun provideGetExecutionsByRelatedIdUseCase( + repository: ExecutionRepository, + ): GetExecutionsByRelatedIdUseCase = GetExecutionsByRelatedIdUseCase(repository) + + @Provides + @Singleton + fun provideGetExecutionByIdUseCase( + repository: ExecutionRepository, + ): GetExecutionByIdUseCase = GetExecutionByIdUseCase(repository) + + @Provides + @Singleton + fun provideGetTaskWithExecutionsUseCase( + getTaskByIdUseCase: GetTaskByIdUseCase, + getExecutionsByRelatedIdUseCase: GetExecutionsByRelatedIdUseCase, + ): GetTaskWithExecutionsUseCase = GetTaskWithExecutionsUseCase( + getTaskByIdUseCase, + getExecutionsByRelatedIdUseCase + ) +} diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_execution/data/local/ExecutionRepositoryFake.kt b/app/src/main/java/com/braiso_22/cozycave/feature_execution/data/local/ExecutionRepositoryFake.kt new file mode 100644 index 0000000..d2b4bbd --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_execution/data/local/ExecutionRepositoryFake.kt @@ -0,0 +1,32 @@ +package com.braiso_22.cozycave.feature_execution.data.local + +import androidx.compose.runtime.mutableStateListOf +import com.braiso_22.cozycave.feature_execution.domain.Execution +import com.braiso_22.cozycave.feature_execution.domain.ExecutionRepository +import kotlinx.coroutines.flow.* + +/** + * Fake implementation of the [ExecutionRepository] to be used in tests. + * It stores the [Execution]s in memory. + */ +class ExecutionRepositoryFake : ExecutionRepository { + + private val _executions = MutableStateFlow>(listOf()) + private val executions = _executions.asStateFlow() + + override suspend fun addExecution(execution: Execution) { + _executions.update { list -> + list + execution + } + } + + override fun getExecutionsByRelatedId(id: Int): Flow> { + return executions.map { list -> + list.filter { it.relatedId == id } + } + } + + override suspend fun getExecutionById(id: Int): Execution { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_execution/data/local/ExecutionRepositoryImpl.kt b/app/src/main/java/com/braiso_22/cozycave/feature_execution/data/local/ExecutionRepositoryImpl.kt new file mode 100644 index 0000000..bc97213 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_execution/data/local/ExecutionRepositoryImpl.kt @@ -0,0 +1,40 @@ +package com.braiso_22.cozycave.feature_execution.data.local + +import com.braiso_22.cozycave.feature_execution.data.local.dao.ExecutionDao +import com.braiso_22.cozycave.feature_execution.data.local.entities.LocalExecution +import com.braiso_22.cozycave.feature_execution.data.local.entities.asExecution +import com.braiso_22.cozycave.feature_execution.data.local.entities.toLocal +import com.braiso_22.cozycave.feature_execution.domain.Execution +import com.braiso_22.cozycave.feature_execution.domain.ExecutionRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import java.time.LocalDateTime + +/** + * [Execution]s Repository implementation using the offline first approach. + * @param dao [ExecutionDao] to access the Local database. + */ +class ExecutionRepositoryImpl(private val dao: ExecutionDao) : ExecutionRepository { + override suspend fun addExecution(execution: Execution) { + withContext(Dispatchers.IO) { + dao.addExecution(execution.toLocal()) + } + } + + override fun getExecutionsByRelatedId(id: Int): Flow> { + return dao.getExecutionsByRelatedId(id).map { + it.map(LocalExecution::asExecution) + } + } + + override suspend fun getExecutionById(id: Int): Execution { + val localExecution: LocalExecution? = dao.getExecutionById(id) + return localExecution?.asExecution() ?: Execution( + startDateTime = LocalDateTime.now(), + endDateTime = LocalDateTime.now(), + relatedId = 0 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_execution/data/local/dao/ExecutionDao.kt b/app/src/main/java/com/braiso_22/cozycave/feature_execution/data/local/dao/ExecutionDao.kt new file mode 100644 index 0000000..1ca780c --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_execution/data/local/dao/ExecutionDao.kt @@ -0,0 +1,23 @@ +package com.braiso_22.cozycave.feature_execution.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.braiso_22.cozycave.feature_execution.data.local.entities.LocalExecution +import kotlinx.coroutines.flow.Flow + +/** + * Dao for the [LocalExecution] entity. + */ +@Dao +interface ExecutionDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun addExecution(execution: LocalExecution) + + @Query("SELECT * FROM LocalExecution WHERE relatedId = :id") + fun getExecutionsByRelatedId(id: Int): Flow> + + @Query("SELECT * FROM LocalExecution WHERE id = :id LIMIT 1") + suspend fun getExecutionById(id: Int): LocalExecution? +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_execution/data/local/entities/LocalExecution.kt b/app/src/main/java/com/braiso_22/cozycave/feature_execution/data/local/entities/LocalExecution.kt new file mode 100644 index 0000000..954d71e --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_execution/data/local/entities/LocalExecution.kt @@ -0,0 +1,42 @@ +package com.braiso_22.cozycave.feature_execution.data.local.entities + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.braiso_22.cozycave.feature_execution.domain.Execution +import java.time.LocalDateTime + +/** + * Entity for the [Execution] table in room database. + */ +@Entity +data class LocalExecution( + @PrimaryKey(autoGenerate = true) + val id: Int, + @ColumnInfo(defaultValue = "CURRENT_TIMESTAMP") + val startDateTime: String, + @ColumnInfo(defaultValue = "CURRENT_TIMESTAMP") + val endDateTime: String, + val relatedId: Int, + val finished: Boolean, +) + +fun LocalExecution.asExecution(): Execution { + return Execution( + id = id, + startDateTime = LocalDateTime.parse(startDateTime), + endDateTime = LocalDateTime.parse(endDateTime), + relatedId = relatedId, + finished = finished + ) +} + +fun Execution.toLocal(): LocalExecution { + return LocalExecution( + id = id, + startDateTime = startDateTime.toString(), + endDateTime = endDateTime.toString(), + relatedId = relatedId, + finished = finished, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_execution/domain/Execution.kt b/app/src/main/java/com/braiso_22/cozycave/feature_execution/domain/Execution.kt new file mode 100644 index 0000000..70ab00c --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_execution/domain/Execution.kt @@ -0,0 +1,15 @@ +package com.braiso_22.cozycave.feature_execution.domain + +import android.content.IntentSender.OnFinished +import java.time.LocalDateTime + +/** + * Represents an execution of a feature. + */ +data class Execution( + val id: Int = 0, + val startDateTime: LocalDateTime = LocalDateTime.now(), + val endDateTime: LocalDateTime = LocalDateTime.now(), + val relatedId: Int, + val finished: Boolean = false, +) diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_execution/domain/ExecutionRepository.kt b/app/src/main/java/com/braiso_22/cozycave/feature_execution/domain/ExecutionRepository.kt new file mode 100644 index 0000000..6c3ac88 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_execution/domain/ExecutionRepository.kt @@ -0,0 +1,24 @@ +package com.braiso_22.cozycave.feature_execution.domain + +import kotlinx.coroutines.flow.Flow + +/** + * Repository for [Execution]s. + * It is used to abstract the data source from the domain layer. + */ +interface ExecutionRepository { + /** + * Adds an [Execution] to the data source. + */ + suspend fun addExecution(execution: Execution) + + /** + * Returns a [Flow] of all the [Execution]s related to a specific id. + */ + fun getExecutionsByRelatedId(id: Int): Flow> + + /** + * Returns an [Execution] by its id. + */ + suspend fun getExecutionById(id: Int): Execution +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_execution/domain/use_case/AddExecutionUseCase.kt b/app/src/main/java/com/braiso_22/cozycave/feature_execution/domain/use_case/AddExecutionUseCase.kt new file mode 100644 index 0000000..3e4bf8e --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_execution/domain/use_case/AddExecutionUseCase.kt @@ -0,0 +1,13 @@ +package com.braiso_22.cozycave.feature_execution.domain.use_case + +import com.braiso_22.cozycave.feature_execution.domain.Execution +import com.braiso_22.cozycave.feature_execution.domain.ExecutionRepository + +/** + * Use case to add an [Execution] to the data source. + */ +class AddExecutionUseCase(private val repository: ExecutionRepository) { + suspend operator fun invoke(execution: Execution) { + repository.addExecution(execution) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_execution/domain/use_case/GetExecutionByIdUseCase.kt b/app/src/main/java/com/braiso_22/cozycave/feature_execution/domain/use_case/GetExecutionByIdUseCase.kt new file mode 100644 index 0000000..3e1f5b7 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_execution/domain/use_case/GetExecutionByIdUseCase.kt @@ -0,0 +1,10 @@ +package com.braiso_22.cozycave.feature_execution.domain.use_case + +import com.braiso_22.cozycave.feature_execution.domain.ExecutionRepository + + +class GetExecutionByIdUseCase( + private val executionRepository: ExecutionRepository, +) { + suspend operator fun invoke(id: Int) = executionRepository.getExecutionById(id) +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_execution/domain/use_case/GetExecutionsByRelatedIdUseCase.kt b/app/src/main/java/com/braiso_22/cozycave/feature_execution/domain/use_case/GetExecutionsByRelatedIdUseCase.kt new file mode 100644 index 0000000..01772f6 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_execution/domain/use_case/GetExecutionsByRelatedIdUseCase.kt @@ -0,0 +1,12 @@ +package com.braiso_22.cozycave.feature_execution.domain.use_case + +import com.braiso_22.cozycave.feature_execution.domain.ExecutionRepository +import com.braiso_22.cozycave.feature_execution.domain.Execution + +/** + * Use case to get all the [Execution]s related to a specific id. + + */ +class GetExecutionsByRelatedIdUseCase(private val repository: ExecutionRepository) { + operator fun invoke(id: Int) = repository.getExecutionsByRelatedId(id) +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_execution/presentation/add_edit_execution/AddEditExecutionEvent.kt b/app/src/main/java/com/braiso_22/cozycave/feature_execution/presentation/add_edit_execution/AddEditExecutionEvent.kt new file mode 100644 index 0000000..65060ee --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_execution/presentation/add_edit_execution/AddEditExecutionEvent.kt @@ -0,0 +1,9 @@ +package com.braiso_22.cozycave.feature_execution.presentation.add_edit_execution + +import com.braiso_22.cozycave.feature_execution.presentation.add_edit_execution.state.AddEditExecutionUiState + +sealed class AddEditExecutionEvent { + data class ChangeState(val state: AddEditExecutionUiState) : AddEditExecutionEvent() + data object Save : AddEditExecutionEvent() + data object Cancel : AddEditExecutionEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_execution/presentation/add_edit_execution/AddEditExecutionScreen.kt b/app/src/main/java/com/braiso_22/cozycave/feature_execution/presentation/add_edit_execution/AddEditExecutionScreen.kt new file mode 100644 index 0000000..001ea66 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_execution/presentation/add_edit_execution/AddEditExecutionScreen.kt @@ -0,0 +1,224 @@ +package com.braiso_22.cozycave.feature_execution.presentation.add_edit_execution + +import android.widget.Space +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.outlined.WatchLater +import androidx.compose.material3.* +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.braiso_22.cozycave.R +import com.braiso_22.cozycave.feature_execution.presentation.add_edit_execution.components.DateTimeRow +import com.braiso_22.cozycave.feature_execution.presentation.add_edit_execution.state.AddEditExecutionUiState +import kotlinx.coroutines.flow.collectLatest + +@Composable +fun AddEditExecutionScreen( + windowSizeClass: WindowSizeClass, + onMessage: (String) -> Unit, + onClickBack: () -> Unit, + modifier: Modifier = Modifier, + viewModel: AddEditExecutionViewModel = hiltViewModel(), +) { + val state = viewModel.state.value + val context = LocalContext.current + + LaunchedEffect(key1 = true) { + viewModel.eventFlow.collectLatest { event -> + when (event) { + AddEditExecutionViewModel.UiEvent.CloseScreen -> { + onClickBack() + } + + is AddEditExecutionViewModel.UiEvent.SavedCloseScreen -> { + onMessage(context.getString(event.message)) + onClickBack() + } + + is AddEditExecutionViewModel.UiEvent.ShowSnackbar -> { + onMessage(context.getString(event.message)) + } + } + } + } + + AddEditExecutionScreenContent( + state = state, + setState = { + viewModel.onEvent( + AddEditExecutionEvent.ChangeState(it) + ) + }, + onClickBack = onClickBack, + onAddExecution = { + viewModel.onEvent(AddEditExecutionEvent.Save) + }, + windowSizeClass = windowSizeClass, + modifier = modifier, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddEditExecutionScreenContent( + state: AddEditExecutionUiState, + setState: (AddEditExecutionUiState) -> Unit, + onClickBack: () -> Unit, + onAddExecution: () -> Unit, + windowSizeClass: WindowSizeClass, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.add_execution)) }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary + ), + navigationIcon = { + IconButton(onClick = onClickBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Go back" + ) + } + } + ) + }, + ) { padding -> + Column( + modifier = Modifier.padding(padding) + ) { + Column( + modifier = Modifier.padding(8.dp) + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { + setState( + state.copy( + isFinished = !state.isFinished + ) + ) + } + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(imageVector = Icons.Outlined.WatchLater, "") + Spacer(modifier = Modifier.padding(8.dp)) + Text("Already completed") + } + Switch( + checked = state.isFinished, + onCheckedChange = { + setState( + state.copy( + isFinished = !state.isFinished + ) + ) + } + ) + } + Spacer(modifier = Modifier.padding(8.dp)) + DateTimeRow( + date = state.startDate, + setDate = { + setState( + state.copy( + startDate = it + ) + ) + }, + time = state.startTime, + setTime = { + setState( + state.copy( + startTime = it + ) + ) + }, + modifier = Modifier + .fillMaxWidth() + ) + if (state.isFinished) { + Spacer(modifier = Modifier.padding(8.dp)) + DateTimeRow( + date = state.endDate, + setDate = { + setState( + state.copy( + endDate = it + ) + ) + }, + time = state.endTime, + setTime = { + setState( + state.copy( + endTime = it + ) + ) + }, + modifier = Modifier.fillMaxWidth() + ) + } + Button( + onClick = onAddExecution, + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) { + Text(text = stringResource(id = R.string.save)) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@Composable +fun AddEditExecutionScreenContentPreview(dpSize: DpSize) { + val state = remember { + mutableStateOf( + AddEditExecutionUiState(), + ) + } + AddEditExecutionScreenContent( + state = state.value, + setState = {}, + windowSizeClass = WindowSizeClass.calculateFromSize(dpSize), + onClickBack = {}, + onAddExecution = {}, + modifier = Modifier.fillMaxSize() + ) +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Composable +fun AddEditExecutionScreenContentVerticalPreview() { + AddEditExecutionScreenContentPreview(DpSize(360.dp, 720.dp)) +} + +@Preview(showBackground = true, widthDp = 720, heightDp = 360) +@Composable +fun AddEditExecutionScreenContentHorizontalPreview() { + AddEditExecutionScreenContentPreview(DpSize(720.dp, 360.dp)) +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_execution/presentation/add_edit_execution/AddEditExecutionViewModel.kt b/app/src/main/java/com/braiso_22/cozycave/feature_execution/presentation/add_edit_execution/AddEditExecutionViewModel.kt new file mode 100644 index 0000000..5fbffbf --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_execution/presentation/add_edit_execution/AddEditExecutionViewModel.kt @@ -0,0 +1,97 @@ +package com.braiso_22.cozycave.feature_execution.presentation.add_edit_execution + + +import android.util.Log +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.braiso_22.cozycave.R +import com.braiso_22.cozycave.feature_execution.domain.use_case.AddExecutionUseCase +import com.braiso_22.cozycave.feature_execution.domain.use_case.GetExecutionByIdUseCase +import com.braiso_22.cozycave.feature_execution.domain.use_case.GetExecutionsByRelatedIdUseCase +import com.braiso_22.cozycave.feature_execution.presentation.add_edit_execution.state.AddEditExecutionUiState +import com.braiso_22.cozycave.feature_execution.presentation.add_edit_execution.state.asExecution +import com.braiso_22.cozycave.feature_execution.presentation.add_edit_execution.state.toUiState +import com.braiso_22.cozycave.feature_task.presentation.add_edit_task.state.toUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +const val TAG = "AddEditExecutionViewModel" + +@HiltViewModel +class AddEditExecutionViewModel @Inject constructor( + private val getExecutionByIdUseCase: GetExecutionByIdUseCase, + private val addExecutionUseCase: AddExecutionUseCase, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + + private val _state = mutableStateOf(AddEditExecutionUiState()) + val state: State = _state + + private val _eventFlow = MutableSharedFlow() + val eventFlow = _eventFlow.asSharedFlow() + + init { + Log.i(TAG, "$TAG created") + savedStateHandle.get("executionId").let { executionId -> + viewModelScope.launch { + val execution = getExecutionByIdUseCase( + executionId ?: return@launch + ) + _state.value = execution.toUiState() + savedStateHandle.get("edit").let { isEdit -> + _state.value = _state.value.copy( + isEditExecution = isEdit ?: return@launch + ) + } + savedStateHandle.get("relatedId").let { id -> + _state.value = _state.value.copy(relatedId = id ?: return@launch) + } + + } + } + } + + fun onEvent(changeState: AddEditExecutionEvent) { + when (changeState) { + is AddEditExecutionEvent.ChangeState -> { + _state.value = changeState.state + } + + AddEditExecutionEvent.Save -> { + Log.i(TAG, "Clicked on save button") + if (!state.value.isFinished) { + _state.value = _state.value.copy( + endTime = _state.value.startTime, + endDate = _state.value.startDate + ) + } + viewModelScope.launch { + addExecutionUseCase( + _state.value.asExecution() + ) + _eventFlow.emit(UiEvent.SavedCloseScreen(R.string.execution_saved_correctly)) + } + } + + AddEditExecutionEvent.Cancel -> { + viewModelScope.launch { + _eventFlow.emit(UiEvent.CloseScreen) + } + Log.i(TAG, "AddEditExecution creation cancelled") + } + } + } + + sealed class UiEvent { + data class ShowSnackbar(val message: Int) : UiEvent() + data object CloseScreen : UiEvent() + data class SavedCloseScreen(val message: Int) : UiEvent() + } +} + diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_execution/presentation/add_edit_execution/components/DateTimeRow.kt b/app/src/main/java/com/braiso_22/cozycave/feature_execution/presentation/add_edit_execution/components/DateTimeRow.kt new file mode 100644 index 0000000..15f6247 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_execution/presentation/add_edit_execution/components/DateTimeRow.kt @@ -0,0 +1,58 @@ +package com.braiso_22.cozycave.feature_execution.presentation.add_edit_execution.components + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.braiso_22.cozycave.date_time_selector.presentation.date_selector.DateSelector +import com.braiso_22.cozycave.date_time_selector.presentation.time_selector.TimeSelector + +@Composable +fun DateTimeRow( + date: String, + setDate: (String) -> Unit, + time: String, + setTime: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, + ) { + Row { + Spacer(Modifier.padding(horizontal = 20.dp)) + DateSelector( + state = date, + setState = setDate, + modifier = Modifier + ) + } + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + TimeSelector( + state = time, + setState = setTime, + modifier = Modifier + ) + Spacer(modifier = Modifier.padding(4.dp)) + } + } +} + +@Preview +@Composable +private fun DateTimeRowPreview() { + DateTimeRow( + date = "", + setDate = {}, + time = "", + setTime = {}, + modifier = Modifier.fillMaxWidth() + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_execution/presentation/add_edit_execution/state/AddEditExecutionUiState.kt b/app/src/main/java/com/braiso_22/cozycave/feature_execution/presentation/add_edit_execution/state/AddEditExecutionUiState.kt new file mode 100644 index 0000000..9a94a96 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_execution/presentation/add_edit_execution/state/AddEditExecutionUiState.kt @@ -0,0 +1,53 @@ +package com.braiso_22.cozycave.feature_execution.presentation.add_edit_execution.state + +import com.braiso_22.cozycave.feature_execution.domain.Execution +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.format.DateTimeFormatter + +data class AddEditExecutionUiState( + val startDate: String = "", + val startTime: String = "", + val endDate: String = "", + val endTime: String = "", + val relatedId: Int = 0, + val isEditExecution: Boolean = false, + val isFinished: Boolean = false, +) + +fun Execution.toUiState() = AddEditExecutionUiState( + startDate = startDateTime.toFormatedDate(), + startTime = startDateTime.toFormatedTime(), + endDate = endDateTime.toFormatedDate(), + endTime = endDateTime.toFormatedTime(), + relatedId = relatedId, + isFinished = finished, +) + +fun LocalDateTime.toFormatedDate(): String { + return this.format( + DateTimeFormatter.ofPattern("dd/MM/yyyy") + ) +} + +fun LocalDateTime.toFormatedTime(): String { + return this.format( + DateTimeFormatter.ofPattern("HH:mm") + ) +} + +fun AddEditExecutionUiState.asExecution(): Execution { + return Execution( + startDateTime = LocalDateTime.of( + LocalDate.parse(startDate, DateTimeFormatter.ofPattern("dd/MM/yyyy")), + LocalTime.parse(startTime) + ), + endDateTime = LocalDateTime.of( + LocalDate.parse(endDate, DateTimeFormatter.ofPattern("dd/MM/yyyy")), + LocalTime.parse(endTime) + ), + relatedId = relatedId, + finished = isFinished, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/data/TaskRepositoryImpl.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/data/TaskRepositoryImpl.kt index 24d31f9..5bdaa38 100644 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/data/TaskRepositoryImpl.kt +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/data/TaskRepositoryImpl.kt @@ -3,7 +3,7 @@ package com.braiso_22.cozycave.feature_task.data import com.braiso_22.cozycave.feature_task.data.local.dao.TaskDao import com.braiso_22.cozycave.feature_task.data.local.entities.LocalTask import com.braiso_22.cozycave.feature_task.data.local.entities.asTask -import com.braiso_22.cozycave.feature_task.data.local.entities.localTaskfromTask +import com.braiso_22.cozycave.feature_task.data.local.entities.toLocal import com.braiso_22.cozycave.feature_task.domain.Task import com.braiso_22.cozycave.feature_task.domain.TaskRepository import kotlinx.coroutines.flow.Flow @@ -23,12 +23,13 @@ class TaskRepositoryImpl(private val taskDao: TaskDao) : TaskRepository { } } - override suspend fun getTaskById(id: Int): Task? { - TODO("Not yet implemented") + override suspend fun getTaskById(id: Int): Task { + val task = taskDao.getTask(id) + return task?.asTask() ?: Task() } override suspend fun insertTask(task: Task) { - taskDao.insertTask(localTaskfromTask(task)) + taskDao.insertTask(task.toLocal()) } override suspend fun deleteTask(task: Task) { diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/data/local/dao/TaskDao.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/data/local/dao/TaskDao.kt index 9ac16bf..e8bb4df 100644 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/data/local/dao/TaskDao.kt +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/data/local/dao/TaskDao.kt @@ -1,9 +1,6 @@ package com.braiso_22.cozycave.feature_task.data.local.dao -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Query -import androidx.room.Upsert +import androidx.room.* import com.braiso_22.cozycave.feature_task.data.local.entities.LocalTask import kotlinx.coroutines.flow.Flow @@ -18,7 +15,7 @@ interface TaskDao { @Query("SELECT * FROM LocalTask WHERE id = :id") suspend fun getTask(id: Int): LocalTask? - @Upsert + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertTask(task: LocalTask) @Delete diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/data/local/db/TaskDatabase.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/data/local/db/TaskDatabase.kt deleted file mode 100644 index 747b920..0000000 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/data/local/db/TaskDatabase.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.braiso_22.cozycave.feature_task.data.local.db - -import androidx.room.Database -import androidx.room.RoomDatabase -import com.braiso_22.cozycave.feature_task.data.local.dao.TaskDao -import com.braiso_22.cozycave.feature_task.data.local.entities.LocalTask - -/** - * Database for the Task entity. - */ -@Database( - entities = [LocalTask::class], - version = 1, - exportSchema = false, -) -abstract class TaskDatabase : RoomDatabase() { - abstract val taskDao: TaskDao - - companion object { - const val DATABASE_NAME = "task_db" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/data/local/entities/LocalTask.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/data/local/entities/LocalTask.kt index c68e3f3..89b67d5 100644 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/data/local/entities/LocalTask.kt +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/data/local/entities/LocalTask.kt @@ -9,12 +9,10 @@ import com.braiso_22.cozycave.feature_task.domain.Task */ @Entity data class LocalTask( - @PrimaryKey val id: Int? = null, + @PrimaryKey(autoGenerate = true) + val id: Int, val name: String, - val description: String? = null, - val frequency: String, - val initialDate: Long, - val days: String? = null, + val description: String, ) /** @@ -25,23 +23,17 @@ fun LocalTask.asTask(): Task { id = id, name = name, description = description, - frequency = frequency, - initialDate = initialDate, - days = days, ) } /** * Extension function to convert a [Task] into a [LocalTask]. */ -fun localTaskfromTask(task: Task): LocalTask { +fun Task.toLocal(): LocalTask { return LocalTask( - id = task.id, - name = task.name, - description = task.description, - frequency = task.frequency, - initialDate = task.initialDate, - days = task.days, + id = id, + name = name, + description = description, ) } diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/data/network/model/NetworkTask.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/data/network/model/NetworkTask.kt index 47d7248..be48f48 100644 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/data/network/model/NetworkTask.kt +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/data/network/model/NetworkTask.kt @@ -9,11 +9,9 @@ import com.braiso_22.cozycave.feature_task.data.local.entities.LocalTask */ @Serializable data class NetworkTask( - val id: Int? = null, - val name: String, - val description: String? = null, - val frequency: String, - val initialDate: Long, + val id: Int = 0, + val name: String = "", + val description: String = "", ) /** @@ -24,7 +22,5 @@ fun NetworkTask.asLocalTask(): LocalTask { id = id, name = name, description = description, - frequency = frequency, - initialDate = initialDate, ) } \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/Task.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/Task.kt index 11b6a94..78948d8 100644 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/Task.kt +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/Task.kt @@ -3,16 +3,13 @@ package com.braiso_22.cozycave.feature_task.domain /** * External domain layer representation of a Task. */ -data class Task ( - val id: Int? = null, - val name: String, - val description: String? = null, - val frequency: String, - val initialDate: Long, - val days: String? = null, +data class Task( + val id: Int = 0, + val name: String = "", + val description: String = "", ) /** * Exception thrown when a task is not valid. */ -class InvalidTaskException(message: String) : Exception(message) \ No newline at end of file +class InvalidTaskException(message: String) : Exception(message) diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/TaskRepository.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/TaskRepository.kt index 1fc50db..ef03f49 100644 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/TaskRepository.kt +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/TaskRepository.kt @@ -14,7 +14,7 @@ interface TaskRepository { /** * Returns a [Task] by its id. */ - suspend fun getTaskById(id: Int): Task? + suspend fun getTaskById(id: Int): Task /** * Inserts a [Task] into the database. */ diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/TaskWithExecutions.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/TaskWithExecutions.kt new file mode 100644 index 0000000..1a4bfc6 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/TaskWithExecutions.kt @@ -0,0 +1,8 @@ +package com.braiso_22.cozycave.feature_task.domain + +import com.braiso_22.cozycave.feature_execution.domain.Execution + +data class TaskWithExecutions( + val task: Task, + val executions: List, +) \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/use_case/AddTaskUseCase.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/use_case/AddTaskUseCase.kt index a42ba31..74d9e9b 100644 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/use_case/AddTaskUseCase.kt +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/use_case/AddTaskUseCase.kt @@ -8,14 +8,8 @@ import com.braiso_22.cozycave.feature_task.domain.InvalidTaskException * Use case to add a new [Task]. */ class AddTaskUseCase( - private val taskRepository: TaskRepository + private val taskRepository: TaskRepository, ) { - @Throws(InvalidTaskException::class) - suspend operator fun invoke(task: Task) { - if(task.name.isBlank()) { - throw InvalidTaskException("The name of the task can't be empty.") - } - taskRepository.insertTask(task) - } + suspend operator fun invoke(task: Task) = taskRepository.insertTask(task) } diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/use_case/GetTaskByIdUseCase.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/use_case/GetTaskByIdUseCase.kt index abe9ee4..4023562 100644 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/use_case/GetTaskByIdUseCase.kt +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/use_case/GetTaskByIdUseCase.kt @@ -5,9 +5,12 @@ import com.braiso_22.cozycave.feature_task.domain.TaskRepository /** * Use case for getting a [Task] by its id. + * @param taskRepository [TaskRepository] to access the [Task]s. + * @throws Exception if the [Task] is not found. */ + class GetTaskByIdUseCase( - private val taskRepository: TaskRepository + private val taskRepository: TaskRepository, ) { - suspend operator fun invoke(id: Int) = taskRepository.getTaskById(id) + suspend operator fun invoke(id: Int): Task = taskRepository.getTaskById(id) } \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/use_case/GetTaskWithExecutionsUseCase.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/use_case/GetTaskWithExecutionsUseCase.kt new file mode 100644 index 0000000..fd41d69 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/domain/use_case/GetTaskWithExecutionsUseCase.kt @@ -0,0 +1,18 @@ +package com.braiso_22.cozycave.feature_task.domain.use_case + +import com.braiso_22.cozycave.feature_execution.domain.use_case.GetExecutionsByRelatedIdUseCase +import com.braiso_22.cozycave.feature_task.domain.TaskWithExecutions +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetTaskWithExecutionsUseCase( + private val getTaskByIdUseCase: GetTaskByIdUseCase, + private val getExecutionsByRelatedIdUseCase: GetExecutionsByRelatedIdUseCase, +) { + suspend operator fun invoke(taskId: Int): Flow { + val task = getTaskByIdUseCase(taskId) + return getExecutionsByRelatedIdUseCase(id = task.id).map { + TaskWithExecutions(task, it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/add_edit_task/AddEditTaskEvent.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/add_edit_task/AddEditTaskEvent.kt new file mode 100644 index 0000000..499e145 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/add_edit_task/AddEditTaskEvent.kt @@ -0,0 +1,9 @@ +package com.braiso_22.cozycave.feature_task.presentation.add_edit_task + + +sealed class AddEditTaskEvent { + data class EnteredName(val name: String) : AddEditTaskEvent() + data class EnteredDescription(val description: String) : AddEditTaskEvent() + data object SaveTask : AddEditTaskEvent() + data object OnBack : AddEditTaskEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/add_edit_task/AddEditTaskScreen.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/add_edit_task/AddEditTaskScreen.kt new file mode 100644 index 0000000..bb02a46 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/add_edit_task/AddEditTaskScreen.kt @@ -0,0 +1,189 @@ +package com.braiso_22.cozycave.feature_task.presentation.add_edit_task + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.braiso_22.cozycave.R +import com.braiso_22.cozycave.feature_task.presentation.add_edit_task.state.AddEditTaskUiState +import kotlinx.coroutines.flow.collectLatest +import com.braiso_22.cozycave.feature_task.presentation.add_edit_task.AddEditTaskViewModel.UiEvent + +@Composable +fun AddEditTaskScreen( + onBack: () -> Unit, + onUiMessage: (String) -> Unit, + modifier: Modifier = Modifier, + viewModel: AddEditTaskViewModel = hiltViewModel(), +) { + val state = viewModel.state.value + LaunchedEffect(key1 = Unit) { + viewModel.eventFlow.collectLatest { + when (it) { + is UiEvent.GoBack -> { + onBack() + } + + is UiEvent.ShowSnackbar -> onUiMessage(it.message) + } + } + } + + AddEditTask( + state = state, + setName = { + viewModel.onEvent( + AddEditTaskEvent.EnteredName(it) + ) + }, + setDescription = { + viewModel.onEvent( + AddEditTaskEvent.EnteredDescription(it) + ) + }, + onSave = { + viewModel.onEvent( + AddEditTaskEvent.SaveTask + ) + }, + onBack = onBack, + modifier = modifier, + ) +} + +@Preview(showBackground = true) +@Composable +fun AddEditTaskPreview() { + var state = remember { AddEditTaskUiState() } + AddEditTask( + state = state, + setName = { state = state.copy(name = it) }, + setDescription = { state = state.copy(description = it) }, + onBack = {}, + onSave = {}, + modifier = Modifier + .padding(8.dp) + .fillMaxWidth(), + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddEditTask( + state: AddEditTaskUiState, + setName: (String) -> Unit, + setDescription: (String) -> Unit, + onBack: () -> Unit, + onSave: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { + Text( + stringResource( + if (state.isEditTask) { + R.string.edit_task + } else { + R.string.add_new_task + } + ) + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary + ), + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } + }, + ) + }, + ) { + Column( + modifier = Modifier.padding(it), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + DataTextFieldColum( + state = state, + setName = setName, + setDescription = setDescription, + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + ) + Button( + onClick = onSave, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) { + Text(text = stringResource(R.string.save)) + } + } + } +} + + +@Composable +fun DataTextFieldColum( + modifier: Modifier = Modifier, + state: AddEditTaskUiState, + setName: (String) -> Unit, + setDescription: (String) -> Unit, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = state.name, + onValueChange = { + setName(it) + }, + label = { + Text(text = "Name") + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = state.description, + onValueChange = { setDescription(it) }, + label = { + Text(text = "Description") + }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + ) + } +} + +@Composable +fun enabledStyles(): TextFieldColors { + return OutlinedTextFieldDefaults.colors( + disabledTextColor = MaterialTheme.colorScheme.onSurface, + disabledBorderColor = MaterialTheme.colorScheme.outline, + disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant, + // For Icons + disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant, + ) +} diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/add_edit_task/AddEditTaskViewModel.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/add_edit_task/AddEditTaskViewModel.kt new file mode 100644 index 0000000..d6423cf --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/add_edit_task/AddEditTaskViewModel.kt @@ -0,0 +1,86 @@ +package com.braiso_22.cozycave.feature_task.presentation.add_edit_task + + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.braiso_22.cozycave.feature_task.domain.Task +import com.braiso_22.cozycave.feature_task.domain.use_case.AddTaskUseCase +import com.braiso_22.cozycave.feature_task.domain.use_case.GetTaskByIdUseCase +import com.braiso_22.cozycave.feature_task.presentation.add_edit_task.AddEditTaskEvent.* +import com.braiso_22.cozycave.feature_task.presentation.add_edit_task.state.AddEditTaskUiState +import com.braiso_22.cozycave.feature_task.presentation.add_edit_task.state.toTask +import com.braiso_22.cozycave.feature_task.presentation.add_edit_task.state.toUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class AddEditTaskViewModel @Inject constructor( + private val addTaskUseCase: AddTaskUseCase, + private val getTaskByIdUseCase: GetTaskByIdUseCase, + private val savedStateHandle: SavedStateHandle, +) : ViewModel() { + private val _state = mutableStateOf(AddEditTaskUiState()) + val state: State = _state + + private var _eventFlow = MutableSharedFlow() + val eventFlow = _eventFlow.asSharedFlow() + + private var currentTaskId: Int? = null + + init { + savedStateHandle.get("taskId").let { taskId -> + viewModelScope.launch { + val task = getTaskByIdUseCase(taskId ?: return@launch) + currentTaskId = task.id + _state.value = task.toUiState() + + savedStateHandle.get("edit").let { isEdit -> + if (isEdit != null) { + _state.value = _state.value.copy(isEditTask = isEdit) + } + } + } + } + } + + + fun onEvent(event: AddEditTaskEvent) { + when (event) { + is EnteredName -> { + _state.value = state.value.copy(name = event.name) + } + + is EnteredDescription -> { + _state.value = state.value.copy(description = event.description) + } + + is SaveTask -> { + viewModelScope.launch { + addTaskUseCase( + state.value.toTask() + ) + _eventFlow.emit(UiEvent.GoBack) + } + } + + is OnBack -> { + viewModelScope.launch { + _eventFlow.emit(UiEvent.GoBack) + } + } + + } + } + + sealed class UiEvent { + data class ShowSnackbar(val message: String) : UiEvent() + data object GoBack : UiEvent() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/add_edit_task/state/AddEditTaskUiState.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/add_edit_task/state/AddEditTaskUiState.kt new file mode 100644 index 0000000..39626ef --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/add_edit_task/state/AddEditTaskUiState.kt @@ -0,0 +1,26 @@ +package com.braiso_22.cozycave.feature_task.presentation.add_edit_task.state + +import com.braiso_22.cozycave.feature_task.domain.Task + +data class AddEditTaskUiState( + val id: Int = 0, + val name: String = "", + val description: String = "", + val isEditTask: Boolean = false +) + +fun Task.toUiState(): AddEditTaskUiState { + return AddEditTaskUiState( + id = id, + name = name, + description = description, + ) +} + +fun AddEditTaskUiState.toTask(): Task { + return Task( + id = id, + name = name, + description = description, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/TaskDetailScreen.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/TaskDetailScreen.kt new file mode 100644 index 0000000..80ba463 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/TaskDetailScreen.kt @@ -0,0 +1,180 @@ +package com.braiso_22.cozycave.feature_task.presentation.show_task_detail + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.* +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.braiso_22.cozycave.R +import com.braiso_22.cozycave.feature_task.presentation.show_task_detail.components.CompletedExecutionComponent +import com.braiso_22.cozycave.feature_task.presentation.show_task_detail.components.UnCompletedExecutionComponent +import com.braiso_22.cozycave.feature_task.presentation.show_task_detail.state.TaskDetailUiState + +@Composable +fun TaskDetailScreen( + windowSizeClass: WindowSizeClass, + onBack: () -> Unit, + modifier: Modifier = Modifier, + viewModel: TaskDetailViewModel = hiltViewModel(), +) { + val state = viewModel.state.value + TaskDetailScreenContent( + state = state, + windowSizeClass = windowSizeClass, + onBack = onBack, + modifier = modifier, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TaskDetailScreenContent( + state: TaskDetailUiState, + windowSizeClass: WindowSizeClass, + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { + Text( + stringResource(R.string.task_detail) + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary + ), + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } + }, + ) + }, + ) { padding -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + item { + Text( + text = state.name, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(8.dp) + ) + Spacer(modifier = Modifier.padding(8.dp)) + if (state.completedExecutions.isEmpty() && state.unFinishedExecutions.isEmpty()) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = "No executions", + Modifier.padding(8.dp) + ) + } + } + } + + item { + if (state.unFinishedExecutions.isNotEmpty()) { + Text( + text = "Not finished executions", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(8.dp) + ) + Spacer(modifier = Modifier.padding(8.dp)) + + } + } + itemsIndexed(state.unFinishedExecutions) { index, execution -> + UnCompletedExecutionComponent( + state = execution, + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) + if (index != state.unFinishedExecutions.lastIndex) + HorizontalDivider( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 8.dp) + ) + } + + item { + if (state.completedExecutions.isNotEmpty()) { + Text( + text = "Finished executions", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(16.dp) + ) + Spacer(modifier = Modifier.padding(8.dp)) + } + } + + itemsIndexed(state.completedExecutions) { index, execution -> + CompletedExecutionComponent( + state = execution, + modifier = Modifier + .fillMaxSize() + .padding(8.dp) + ) + if (index != state.completedExecutions.lastIndex) + HorizontalDivider( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 8.dp) + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@Composable +private fun TaskDetailScreenContentPreview(dpSize: DpSize) { + val state = remember { + mutableStateOf( + TaskDetailUiState(), + ) + } + TaskDetailScreenContent( + state = state.value, + windowSizeClass = WindowSizeClass.calculateFromSize(dpSize), + onBack = {}, + modifier = Modifier.fillMaxSize() + ) +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Composable +private fun TaskDetailScreenContentVerticalPreview() { + TaskDetailScreenContentPreview(DpSize(360.dp, 720.dp)) +} + +@Preview(showBackground = true, widthDp = 720, heightDp = 360) +@Composable +private fun TaskDetailScreenContentHorizontalPreview() { + TaskDetailScreenContentPreview(DpSize(720.dp, 360.dp)) +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/TaskDetailViewModel.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/TaskDetailViewModel.kt new file mode 100644 index 0000000..4e58ba1 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/TaskDetailViewModel.kt @@ -0,0 +1,43 @@ +package com.braiso_22.cozycave.feature_task.presentation.show_task_detail + +import android.util.Log +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.braiso_22.cozycave.feature_task.domain.use_case.GetTaskWithExecutionsUseCase +import com.braiso_22.cozycave.feature_task.presentation.show_task_detail.state.TaskDetailUiState +import com.braiso_22.cozycave.feature_task.presentation.show_task_detail.state.toUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import javax.inject.Inject + +const val TAG = "TaskDetailViewModel" + +@HiltViewModel +class TaskDetailViewModel @Inject constructor( + private val getTaskWithExecutionsUseCase: GetTaskWithExecutionsUseCase, + private val savedStateHandle: SavedStateHandle, +) : ViewModel() { + + private val _state = mutableStateOf(TaskDetailUiState()) + val state: State = _state + + private val _taskId = mutableIntStateOf(0) + + init { + Log.i(TAG, "$TAG created") + savedStateHandle.get("taskId").let { taskId -> + _taskId.intValue = taskId ?: return@let + } + viewModelScope.launch { + getTaskWithExecutionsUseCase(_taskId.intValue).collectLatest { detail -> + _state.value = detail.toUiState() + Log.i(TAG, "TaskDetail data loaded") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/components/CompletedExecutionComponent.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/components/CompletedExecutionComponent.kt new file mode 100644 index 0000000..e301b67 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/components/CompletedExecutionComponent.kt @@ -0,0 +1,83 @@ +package com.braiso_22.cozycave.feature_task.presentation.show_task_detail.components + +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.icons.filled.AccessTime +import androidx.compose.material.icons.filled.CalendarMonth +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.braiso_22.cozycave.feature_task.presentation.show_task_detail.state.ExecutionUiState + +@Composable +fun CompletedExecutionComponent( + state: ExecutionUiState, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Row( + horizontalArrangement = Arrangement.SpaceAround, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + if (state.endDate == state.startDate) { + Text( + text = state.startDate, + modifier = Modifier.padding(8.dp) + ) + Icon( + imageVector = Icons.Default.CalendarMonth, + contentDescription = null + ) + } else { + Text( + text = state.startDate, + modifier = Modifier.padding(8.dp) + ) + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = "From ${state.startDate} to ${state.endDate}" + ) + Text( + text = state.endDate, + modifier = Modifier.padding(8.dp) + ) + } + if (state.startHour == state.endHour) { + Text(text = state.startHour) + Icon( + imageVector = Icons.Default.AccessTime, + contentDescription = null + ) + } else { + Text(text = state.startHour) + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = "From ${state.startHour} to ${state.endHour}" + ) + Text(text = state.endHour) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ExecutionComponentPreview() { + CompletedExecutionComponent( + state = ExecutionUiState( + startDate = "00/00/0000", + endDate = "00/07/0000", + startHour = "00:00", + endHour = "00:00", + ), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/components/UnCompletedExecutionComponent.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/components/UnCompletedExecutionComponent.kt new file mode 100644 index 0000000..5ee915d --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/components/UnCompletedExecutionComponent.kt @@ -0,0 +1,57 @@ +package com.braiso_22.cozycave.feature_task.presentation.show_task_detail.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.icons.filled.AccessTime +import androidx.compose.material.icons.filled.CalendarMonth +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.braiso_22.cozycave.feature_task.presentation.show_task_detail.state.ExecutionUiState + +@Composable +fun UnCompletedExecutionComponent( + state: ExecutionUiState, + modifier: Modifier = Modifier, +) { + Row( + horizontalArrangement = Arrangement.SpaceAround, + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + ) { + Text(text = state.startDate) + Icon( + imageVector = Icons.Default.CalendarMonth, + contentDescription = null + ) + Text(text = state.startHour) + Icon( + imageVector = Icons.Default.AccessTime, + contentDescription = null + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun UnCompletedExecutionComponentPreview() { + UnCompletedExecutionComponent( + state = ExecutionUiState( + startDate = "22/03/2002", + endDate = "22/03/2002", + startHour = "01:00", + endHour = "12:00", + ), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/state/ExecutionUiState.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/state/ExecutionUiState.kt new file mode 100644 index 0000000..ecde9c9 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/state/ExecutionUiState.kt @@ -0,0 +1,19 @@ +package com.braiso_22.cozycave.feature_task.presentation.show_task_detail.state + +import com.braiso_22.cozycave.feature_execution.domain.Execution +import com.braiso_22.cozycave.feature_execution.presentation.add_edit_execution.state.toFormatedDate +import com.braiso_22.cozycave.feature_execution.presentation.add_edit_execution.state.toFormatedTime + +data class ExecutionUiState( + val startDate: String, + val endDate: String, + val startHour: String, + val endHour: String, +) + +fun Execution.toUiState() = ExecutionUiState( + startDate = this.startDateTime.toFormatedDate(), + startHour = this.startDateTime.toFormatedTime(), + endDate = this.endDateTime.toFormatedDate(), + endHour = this.endDateTime.toFormatedTime(), +) \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/state/TaskDetailUiState.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/state/TaskDetailUiState.kt new file mode 100644 index 0000000..2b062d7 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/show_task_detail/state/TaskDetailUiState.kt @@ -0,0 +1,20 @@ +package com.braiso_22.cozycave.feature_task.presentation.show_task_detail.state + +import com.braiso_22.cozycave.feature_execution.domain.Execution +import com.braiso_22.cozycave.feature_task.domain.TaskWithExecutions + +data class TaskDetailUiState( + val name: String = "Task", + val completedExecutions: List = emptyList(), + val unFinishedExecutions: List = emptyList(), +) + +fun TaskWithExecutions.toUiState(): TaskDetailUiState = TaskDetailUiState( + name = this.task.name, + completedExecutions = this.executions.filter { + it.finished + }.map(Execution::toUiState), + unFinishedExecutions = this.executions.filter { + !it.finished + }.map(Execution::toUiState) +) diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/TaskUiState.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/TaskUiState.kt new file mode 100644 index 0000000..a8c5a3d --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/TaskUiState.kt @@ -0,0 +1,19 @@ +package com.braiso_22.cozycave.feature_task.presentation.tasks + +import com.braiso_22.cozycave.feature_task.domain.Task + +data class TasksUiState(val tasks: List = emptyList()) + +data class TaskUiState( + val id: Int = 0, + val name: String, + val description: String = "", +) + +fun Task.toUiState(): TaskUiState { + return TaskUiState( + id = id, + name = name, + description = description + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/TasksEvent.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/TasksEvent.kt new file mode 100644 index 0000000..c939d74 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/TasksEvent.kt @@ -0,0 +1,6 @@ +package com.braiso_22.cozycave.feature_task.presentation.tasks + +sealed class TasksEvent { + data class Delete(val id: Int) : TasksEvent() + data object UndoDeletion : TasksEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/TasksScreen.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/TasksScreen.kt new file mode 100644 index 0000000..4d9717b --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/TasksScreen.kt @@ -0,0 +1,173 @@ +package com.braiso_22.cozycave.feature_task.presentation.tasks + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.* +import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi +import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.braiso_22.cozycave.R +import com.braiso_22.cozycave.feature_task.presentation.tasks.components.TasksList +import com.braiso_22.cozycave.ui.common.isVertical +import kotlinx.coroutines.flow.collectLatest + +@Composable +fun TasksScreen( + windowSizeClass: WindowSizeClass, + onClickAddTask: () -> Unit, + onSeeDetail: (Int) -> Unit, + onAddExecution: (Int) -> Unit, + onEdit: (Int) -> Unit, + modifier: Modifier = Modifier, + viewModel: TasksViewModel = hiltViewModel(), +) { + val state = viewModel.state.value + val snackbarHostState = remember { + SnackbarHostState() + } + + LaunchedEffect(key1 = true) { + viewModel.eventFlow.collectLatest { + when (it) { + is TasksViewModel.TaskUiEvent.ShowUndoDeletion -> { + val result = snackbarHostState.showSnackbar( + message = "Task deleted", + actionLabel = "Undo" + ) + if (result == SnackbarResult.ActionPerformed) { + viewModel.onEvent(TasksEvent.UndoDeletion) + } + } + } + } + } + + TasksScreenContent( + tasks = state.tasks, + snackbarHostState = snackbarHostState, + windowSizeClass = windowSizeClass, + onClickAddTask = onClickAddTask, + onClickTask = onSeeDetail, + onDeleteTask = { + viewModel.onEvent(TasksEvent.Delete(it)) + }, + onAddExecution = onAddExecution, + onEdit = onEdit, + modifier = modifier, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TasksScreenContent( + tasks: List, + snackbarHostState: SnackbarHostState, + windowSizeClass: WindowSizeClass, + onClickAddTask: () -> Unit, + onAddExecution: (Int) -> Unit, + onEdit: (Int) -> Unit, + onDeleteTask: (Int) -> Unit, + onClickTask: (Int) -> Unit, + modifier: Modifier = Modifier, +) { + if (windowSizeClass.isVertical()) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { Text("Tasks") }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary + ), + actions = { + IconButton(onClick = onClickAddTask) { + Icon( + Icons.Default.Add, + contentDescription = stringResource(R.string.add_new_task) + ) + } + }, + ) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + ) { + TasksList( + tasks = tasks, + onClickEditTask = onEdit, + onClickNewExecution = onAddExecution, + onClickDelete = onDeleteTask, + onClickSeeDetail = onClickTask, + modifier = Modifier.padding(it) + ) + } + } else { + TasksList( + tasks = tasks, + onClickEditTask = onClickTask, + onClickNewExecution = onAddExecution, + onClickDelete = onDeleteTask, + onClickSeeDetail = onEdit, + ) + } +} + +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@Composable +fun TasksScreenContentPreview(dpSize: DpSize) { + val tasks = remember { + mutableStateListOf( + TaskUiState( + id = 1, + name = "Task 1", + description = "Description 1", + ), + TaskUiState( + id = 2, + name = "Task 2", + description = "Description 2", + ), + TaskUiState( + id = 3, + name = "Task 3", + description = "Description 3", + ), + ) + } + TasksScreenContent( + tasks = tasks, + windowSizeClass = WindowSizeClass.calculateFromSize(dpSize), + snackbarHostState = SnackbarHostState(), + onClickAddTask = {}, + onClickTask = {}, + onDeleteTask = {}, + onEdit = {}, + onAddExecution = {}, + modifier = Modifier.fillMaxSize() + ) +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Composable +fun TasksScreenContentVerticalPreview() { + TasksScreenContentPreview(DpSize(360.dp, 720.dp)) +} + +@Preview(showBackground = true, widthDp = 720, heightDp = 360) +@Composable +fun TasksScreenContentHorizontalPreview() { + TasksScreenContentPreview(DpSize(720.dp, 360.dp)) +} diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/TasksViewModel.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/TasksViewModel.kt new file mode 100644 index 0000000..182d812 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/TasksViewModel.kt @@ -0,0 +1,72 @@ +package com.braiso_22.cozycave.feature_task.presentation.tasks + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.braiso_22.cozycave.feature_task.domain.Task +import com.braiso_22.cozycave.feature_task.domain.use_case.AddTaskUseCase +import com.braiso_22.cozycave.feature_task.domain.use_case.DeleteTaskUseCase +import com.braiso_22.cozycave.feature_task.domain.use_case.GetTaskByIdUseCase +import com.braiso_22.cozycave.feature_task.domain.use_case.GetTasksUseCase +import com.braiso_22.cozycave.feature_task.presentation.tasks.TasksEvent.* +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class TasksViewModel @Inject constructor( + private val getTasksUseCase: GetTasksUseCase, + private val getTaskByIdUseCase: GetTaskByIdUseCase, + private val deleteTasksUseCase: DeleteTaskUseCase, + private val addTaskUseCase: AddTaskUseCase, +) : ViewModel() { + private val _state = mutableStateOf(TasksUiState()) + val state: State = _state + + private val _eventFlow = MutableSharedFlow() + val eventFlow = _eventFlow.asSharedFlow() + + private var lastDeletedTask: Task? = null + + private var updateStateJob: Job? = null + + init { + updateState() + } + + private fun updateState() { + updateStateJob?.cancel() + updateStateJob = getTasksUseCase().onEach { taskList -> + _state.value = state.value.copy( + tasks = taskList.map(Task::toUiState) + ) + }.launchIn(viewModelScope) + } + + fun onEvent(event: TasksEvent) { + when (event) { + is Delete -> { + viewModelScope.launch { + val task = getTaskByIdUseCase(event.id) ?: return@launch + lastDeletedTask = task + deleteTasksUseCase(task) + + } + } + + is UndoDeletion -> { + viewModelScope.launch { + addTaskUseCase(lastDeletedTask ?: return@launch) + lastDeletedTask = null + } + } + } + } + + sealed class TaskUiEvent { + data class ShowUndoDeletion(val id: Int) : TaskUiEvent() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/components/TaskItem.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/components/TaskItem.kt new file mode 100644 index 0000000..aee73fa --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/components/TaskItem.kt @@ -0,0 +1,89 @@ +package com.braiso_22.cozycave.feature_task.presentation.tasks.components + +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.braiso_22.cozycave.feature_task.presentation.tasks.TaskUiState + +@Composable +fun TaskItem( + state: TaskUiState, + onClickNewExecution: (Int) -> Unit, + onClickEdit: (Int) -> Unit, + onClickDeleteItem: (Int) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + ) { + Text( + text = state.name, + style = MaterialTheme.typography.titleLarge + ) + Row { + IconButton(onClick = { + onClickDeleteItem(state.id) + }) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Delete this task" + ) + } + IconButton(onClick = { + onClickEdit(state.id) + }) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = "Edit this task" + ) + } + IconButton(onClick = { + onClickNewExecution(state.id) + }) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add new execution" + ) + } + } + } + Text( + text = state.description, + modifier = Modifier + .padding(16.dp) + .fillMaxWidth() + ) + } + +} + +@Preview +@Composable +fun TaskItemPreview() { + Surface { + TaskItem( + TaskUiState( + name = "Task 1", + description = "Description 1", + ), + onClickDeleteItem = {}, + onClickEdit = {}, + onClickNewExecution = {}, + modifier = Modifier + .fillMaxWidth() + ) + } +} diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/components/TasksList.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/components/TasksList.kt new file mode 100644 index 0000000..c57fb33 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/feature_task/presentation/tasks/components/TasksList.kt @@ -0,0 +1,62 @@ +package com.braiso_22.cozycave.feature_task.presentation.tasks.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.braiso_22.cozycave.feature_task.presentation.tasks.TaskUiState + +@Composable +fun TasksList( + tasks: List, + onClickNewExecution: (Int) -> Unit, + onClickEditTask: (Int) -> Unit, + onClickSeeDetail: (Int) -> Unit, + onClickDelete: (Int) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn(modifier = modifier) { + itemsIndexed(tasks) { index, task -> + TaskItem( + state = task, + onClickDeleteItem = onClickDelete, + onClickEdit = onClickEditTask, + onClickNewExecution = onClickNewExecution, + modifier = Modifier + .fillMaxWidth() + .clickable { + onClickSeeDetail(task.id) + } + ) + if (index != tasks.lastIndex) + HorizontalDivider(modifier = Modifier.padding(horizontal = 8.dp)) + } + } +} + +@Preview(showBackground = true) +@Composable +fun TasksListPreview() { + TasksList( + listOf( + TaskUiState( + name = "Task 1", + description = "Description 1", + ), + TaskUiState( + name = "Task 2", + description = "Description 2", + ), + ), + onClickSeeDetail = {}, + onClickNewExecution = {}, + onClickEditTask = {}, + onClickDelete = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/add_edit_task/AddEditTaskEvent.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/add_edit_task/AddEditTaskEvent.kt deleted file mode 100644 index d3d79cc..0000000 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/add_edit_task/AddEditTaskEvent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.braiso_22.cozycave.feature_task.ui.add_edit_task - -import java.time.LocalDate -import java.time.LocalTime - -sealed class AddEditTaskEvent { - data class EnteredName(val name: String) : AddEditTaskEvent() - data class EnteredDescription(val description: String) : AddEditTaskEvent() - data class EnteredDays(val days: String) : AddEditTaskEvent() - data class EnteredDate(val date: LocalDate?) : AddEditTaskEvent() - data class EnteredTime(val time: LocalTime?) : AddEditTaskEvent() - data object SaveTask : AddEditTaskEvent() -} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/add_edit_task/AddEditTaskScreen.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/add_edit_task/AddEditTaskScreen.kt deleted file mode 100644 index c6a4931..0000000 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/add_edit_task/AddEditTaskScreen.kt +++ /dev/null @@ -1,325 +0,0 @@ -package com.braiso_22.cozycave.feature_task.ui.add_edit_task - -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.DateRange -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.braiso_22.cozycave.feature_task.ui.add_edit_task.components.TimePickerDialog -import com.braiso_22.cozycave.feature_task.ui.add_edit_task.components.selectedTime -import java.time.LocalDate -import java.time.LocalTime -import java.time.format.DateTimeFormatter -import kotlin.math.absoluteValue - -@RequiresApi(Build.VERSION_CODES.S) -@Composable -fun AddEditTaskScreen( - modifier: Modifier = Modifier, - viewModel: AddEditTaskViewModel = hiltViewModel() -) { - var state = viewModel.state.value - - Column(modifier) { - Row { - Text(text = "Add/Edit task") - } - AddEditTask( - modifier = Modifier - .padding(8.dp) - .fillMaxWidth(), - state = state, - setName = { - viewModel.onEvent( - AddEditTaskEvent.EnteredName(it) - ) - }, - setDescription = { - viewModel.onEvent( - AddEditTaskEvent.EnteredDescription(it) - ) - }, - setDate = { - viewModel.onEvent( - AddEditTaskEvent.EnteredDate(it) - ) - }, - setTime = { - viewModel.onEvent( - AddEditTaskEvent.EnteredTime(it) - ) - }, - onSave = { - viewModel.onEvent( - AddEditTaskEvent.SaveTask - ) - } - ) - } -} - -@Preview(showBackground = true) -@Composable -fun AddEditTaskPreview() { - var state = remember { AddEditTaskState() } - AddEditTask( - modifier = Modifier - .padding(8.dp) - .fillMaxWidth(), - state = state, - setName = { state = state.copy(name = it) }, - setDescription = { state = state.copy(description = it) }, - setDate = { state = state.copy(date = it) }, - setTime = { state = state.copy(time = it) }, - ) -} - -@Composable -fun AddEditTask( - modifier: Modifier = Modifier, - state: AddEditTaskState, - setName: (String) -> Unit, - setDescription: (String) -> Unit, - setDate: (LocalDate?) -> Unit, - setTime: (LocalTime?) -> Unit, - onSave: () -> Unit = {}, -) { - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - DataTextFieldColum( - modifier = Modifier.fillMaxWidth(), - state = state, - setName = { setName(it) }, - setDescription = { setDescription(it) }, - setDate = { setDate(it) }, - setTime = { setTime(it) }, - ) - ButtonsRow( - modifier = Modifier - .padding(top = 8.dp) - .fillMaxWidth(), - onConfirm = { - onSave() - } - ) - } -} - -@Composable -fun DataTextFieldColum( - modifier: Modifier = Modifier, - state: AddEditTaskState, - setName: (String) -> Unit = {}, - setDescription: (String) -> Unit = {}, - setDate: (LocalDate?) -> Unit = {}, - setTime: (LocalTime?) -> Unit = {}, -) { - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - OutlinedTextField( - modifier = Modifier.padding(8.dp), - value = state.name, - onValueChange = { - setName(it) - }, - label = { - Text(text = "Name") - }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - ) - OutlinedTextField( - modifier = Modifier.padding(8.dp), - value = state.description, - onValueChange = { setDescription(it) }, - label = { - Text(text = "Description") - }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - ) - DatePickerTextField( - modifier = Modifier - .padding(8.dp), - state = state.date, - setState = { setDate(it) } - ) - TimePickerTextField( - modifier = Modifier - .padding(8.dp), - state = state.time, - setState = { setTime(it) } - ) - - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun DatePickerTextField( - modifier: Modifier = Modifier, - state: LocalDate? = remember { null }, - setState: (LocalDate?) -> Unit = {} -) { - var openDateDialog by remember { mutableStateOf(false) } - val datePickerState = rememberDatePickerState( - initialSelectedDateMillis = state?.toEpochDay() - ) - OutlinedTextField( - enabled = false, - modifier = modifier.clickable { - openDateDialog = true - }, - colors = enabledStyles(), - value = state?.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")) ?: "", - onValueChange = { }, - label = { - Text(text = "Initial date") - }, - trailingIcon = { - Icon( - imageVector = Icons.Default.DateRange, - contentDescription = "Date picker icon", - modifier = Modifier - .padding(8.dp) - .height(24.dp) - ) - }, - ) - if (openDateDialog) { - DatePickerDialog( - onDismissRequest = { openDateDialog = false }, - confirmButton = { - Button(onClick = { - setState( - if (datePickerState.selectedDateMillis != null) { - LocalDate.ofEpochDay( - datePickerState.selectedDateMillis!!.absoluteValue / 86400000 - ) - } else { - null - } - ) - openDateDialog = false - }) { - Text(text = "Save") - } - }, - dismissButton = { - Button(onClick = { openDateDialog = false }) { - Text(text = "Cancel") - } - }, - - ) { - DatePicker(state = datePickerState) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun TimePickerTextField( - modifier: Modifier = Modifier, - state: LocalTime? = remember { null }, - setState: (LocalTime?) -> Unit = {} -) { - val timePickerState: TimePickerState = rememberTimePickerState( - is24Hour = true, - initialHour = state?.hour ?: LocalTime.now().hour, - initialMinute = state?.minute ?: LocalTime.now().minute - ) - var openTimeDialog by remember { mutableStateOf(false) } - OutlinedTextField( - enabled = false, - modifier = modifier - .clickable { - openTimeDialog = true - }, - colors = enabledStyles(), - value = state?.toString() ?: "", - onValueChange = { }, - label = { - Text(text = "Initial time") - }, - trailingIcon = { - Icon( - imageVector = Icons.Default.DateRange, - contentDescription = "Date picker icon", - modifier = Modifier - .padding(8.dp) - .height(24.dp) - ) - }, - ) - if (openTimeDialog) { - TimePickerDialog( - title = "Time for the task", - onDismissRequest = { openTimeDialog = false }, - confirmButton = { - Button(onClick = { - val timePickerMilis: Long = timePickerState.selectedTime() - setState( - LocalTime.ofSecondOfDay(timePickerMilis) - ) - openTimeDialog = false - }) { - Text(text = "Save") - } - }, - dismissButton = { - Button(onClick = { openTimeDialog = false }) { - Text(text = "Cancel") - } - }, - ) { - TimePicker(state = timePickerState) - } - } -} - -@Composable -fun ButtonsRow( - modifier: Modifier = Modifier, - onConfirm: () -> Unit = {}, - onCancel: () -> Unit = {} -) { - Row( - modifier = modifier - .padding(top = 8.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceAround - ) { - Button(onClick = { onCancel() }) { - Text(text = "Cancel") - } - Button(onClick = { onConfirm() }) { - Text(text = "Save") - } - } -} - -@Composable -private fun enabledStyles(): TextFieldColors { - return OutlinedTextFieldDefaults.colors( - disabledTextColor = MaterialTheme.colorScheme.onSurface, - disabledBorderColor = MaterialTheme.colorScheme.outline, - disabledLeadingIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - disabledTrailingIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant, - //For Icons - disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant, - ) -} diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/add_edit_task/AddEditTaskState.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/add_edit_task/AddEditTaskState.kt deleted file mode 100644 index d4f4bfa..0000000 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/add_edit_task/AddEditTaskState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.braiso_22.cozycave.feature_task.ui.add_edit_task - -import java.time.LocalDate -import java.time.LocalTime - -data class AddEditTaskState( - val name: String = "", - val description: String = "", - val days: String = "", - val date: LocalDate? = null, - val time: LocalTime? = null, -) diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/add_edit_task/AddEditTaskViewModel.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/add_edit_task/AddEditTaskViewModel.kt deleted file mode 100644 index f0e5e0e..0000000 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/add_edit_task/AddEditTaskViewModel.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.braiso_22.cozycave.feature_task.ui.add_edit_task - - -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.braiso_22.cozycave.feature_task.domain.InvalidTaskException -import com.braiso_22.cozycave.feature_task.domain.Task -import com.braiso_22.cozycave.feature_task.domain.use_case.AddTaskUseCase -import com.braiso_22.cozycave.feature_task.domain.use_case.GetTaskByIdUseCase -import com.braiso_22.cozycave.feature_task.ui.add_edit_task.AddEditTaskEvent.* -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.launch -import java.time.Instant -import java.time.LocalDate -import java.time.LocalTime -import java.time.ZoneId -import javax.inject.Inject - -@RequiresApi(Build.VERSION_CODES.S) -@HiltViewModel -class AddEditTaskViewModel @Inject constructor( - private val addTaskUseCase: AddTaskUseCase, - private val getTaskByIdUseCase: GetTaskByIdUseCase, - savedStateHandle: SavedStateHandle -) : ViewModel() { - private val _state = mutableStateOf(AddEditTaskState()) - val state: State = _state - private var updateStateJob: Job? = null - - private var _eventFlow = MutableSharedFlow() - - private var currentTaskId: Int? = null - - init { - savedStateHandle.get("taskId").let { taskId -> - if (taskId != null) { - viewModelScope.launch { - getTaskByIdUseCase(taskId)?.also { - currentTaskId = it.id - _state.value = AddEditTaskState( - name = it.name, - description = it.description ?: "", - days = it.days ?: "", - date = LocalDate.ofEpochDay(it.initialDate), - // should get the time from a unix timestamp initialDate - time = LocalTime.ofInstant( - Instant.ofEpochMilli(it.initialDate), - ZoneId.systemDefault() - ) - ) - } - } - } - } - } - - - fun onEvent(event: AddEditTaskEvent) { - - when (event) { - is EnteredName -> { - _state.value = state.value.copy(name = event.name) - } - - is EnteredDescription -> { - _state.value = state.value.copy(description = event.description) - } - - is EnteredDays -> { - _state.value = state.value.copy(days = event.days) - } - - is EnteredDate -> { - _state.value = state.value.copy( - date = event.date - ) - } - - is EnteredTime -> { - _state.value = state.value.copy( - time = event.time - ) - } - - is SaveTask -> { - viewModelScope.launch { - try { - addTaskUseCase( - Task( - id = null, - name = state.value.name, - description = state.value.description, - frequency = "Daily", - initialDate = LocalDate.now().toEpochDay(), - days = state.value.days - ) - ) - _eventFlow.emit(UiEvent.SaveTask) - } catch (e: InvalidTaskException) { - _eventFlow.emit( - UiEvent.ShowSnackbar( - message = e.message ?: "Couldn't save the task." - ) - ) - } - } - } - } - } - - sealed class UiEvent { - data class ShowSnackbar(val message: String) : UiEvent() - data object SaveTask : UiEvent() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/add_edit_task/components/TimePickerDialog.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/add_edit_task/components/TimePickerDialog.kt deleted file mode 100644 index 2acb868..0000000 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/add_edit_task/components/TimePickerDialog.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.braiso_22.cozycave.feature_task.ui.add_edit_task.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.KeyboardArrowUp -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TimePicker -import androidx.compose.material3.TimePickerState -import androidx.compose.material3.rememberTimePickerState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties - -/** - * A composable function that displays a time picker dialog. - * This is a workaround while google doesn't implement the DateTimePicker in Material 3 - * - * - * @param title The title of the dialog. Default value is "Select Time". - * @param onDismissRequest A lambda function that is called when the dialog is dismissed. - * @param confirmButton A lambda function that is called when the OK button is clicked. - * @param content A composable lambda function that is called to display the main content of the dialog. - */ -@Composable -fun TimePickerDialog( - modifier: Modifier = Modifier, - title: String = "Select Time", - onDismissRequest: () -> Unit, - confirmButton: @Composable () -> Unit, - dismissButton: @Composable () -> Unit, - content: @Composable () -> Unit, -) { - Dialog( - onDismissRequest = onDismissRequest, - properties = DialogProperties( - usePlatformDefaultWidth = false - ), - ) { - Surface( - shape = MaterialTheme.shapes.extraLarge, - tonalElevation = 6.dp, - modifier = modifier - .width(IntrinsicSize.Min) - .height(IntrinsicSize.Min) - .background( - shape = MaterialTheme.shapes.extraLarge, - color = MaterialTheme.colorScheme.surface - ), - ) { - - Column( - modifier = Modifier.padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 20.dp), - text = title, - style = MaterialTheme.typography.labelMedium - ) - content() - Row( - modifier = Modifier - .height(40.dp) - .fillMaxWidth() - ) { - IconButton(onClick = { }) { - val icon = Icons.Outlined.KeyboardArrowUp - Icon( - icon, - contentDescription = "Switch" - ) - } - Spacer(modifier = Modifier.weight(1f)) - dismissButton() - confirmButton() - } - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Preview -@Composable -fun TimePickerDialogPreview() { - Column { - var openTimeDialog by remember { mutableStateOf(true) } - val timePickerState = rememberTimePickerState() - - TimePickerDialog( - title = "Time for the task", - onDismissRequest = { openTimeDialog = false }, - confirmButton = { openTimeDialog = false }, - dismissButton = { openTimeDialog = false }, - ) { - TimePicker(state = timePickerState) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -fun TimePickerState.selectedTime(): Long { - val hoursMillis = this.hour * 3600 - val minutesMillis = this.minute * 60 - val timePickerMillis: Long = (hoursMillis + minutesMillis).toLong() - return timePickerMillis -} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/tasks/TasksScreen.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/tasks/TasksScreen.kt deleted file mode 100644 index 37e159d..0000000 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/tasks/TasksScreen.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.braiso_22.cozycave.feature_task.ui.tasks - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.* -import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi -import androidx.compose.material3.windowsizeclass.WindowSizeClass -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.braiso_22.cozycave.feature_task.ui.tasks.components.TasksList -import com.braiso_22.cozycave.ui.common.isVertical - -@Composable -fun TasksScreen( - windowSizeClass: WindowSizeClass, - onClickAddTask: () -> Unit, - onClickTask: (Int) -> Unit, - modifier: Modifier = Modifier, - viewModel: TasksViewModel = hiltViewModel(), -) { - val state = viewModel.state.value - TasksScreenContent( - tasks = state.tasks, - windowSizeClass = windowSizeClass, - onClickAddTask = onClickAddTask, - onClickTask = onClickTask, - modifier = modifier, - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun TasksScreenContent( - tasks: List, - windowSizeClass: WindowSizeClass, - onClickAddTask: () -> Unit, - onClickTask: (Int) -> Unit, - modifier: Modifier = Modifier, -) { - if (windowSizeClass.isVertical()) { - Scaffold( - modifier = modifier, - topBar = { - TopAppBar( - title = { Text("Tasks") }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - titleContentColor = MaterialTheme.colorScheme.primary - ), - ) - }, - ) { - - TasksList(tasks = tasks, modifier = Modifier.padding(it)) - - } - } else { - TasksList(tasks = tasks) - } -} - -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) -@Composable -fun TasksScreenContentPreview(dpSize: DpSize) { - val tasks = remember { - mutableStateListOf( - TaskUiState("Task 1", "Description 1"), - TaskUiState("Task 2", "Description 2"), - TaskUiState("Task 3", "Description 3"), - ) - } - TasksScreenContent( - tasks = tasks, - windowSizeClass = WindowSizeClass.calculateFromSize(dpSize), - onClickAddTask = {}, - onClickTask = {}, - modifier = Modifier.fillMaxSize() - ) -} - -@Preview(showBackground = true, widthDp = 360, heightDp = 720) -@Composable -fun TasksScreenContentVerticalPreview() { - TasksScreenContentPreview(DpSize(360.dp, 720.dp)) -} - -@Preview(showBackground = true, widthDp = 720, heightDp = 360) -@Composable -fun TasksScreenContentHorizontalPreview() { - TasksScreenContentPreview(DpSize(720.dp, 360.dp)) -} diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/tasks/TasksUiState.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/tasks/TasksUiState.kt deleted file mode 100644 index 37b0fbb..0000000 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/tasks/TasksUiState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.braiso_22.cozycave.feature_task.ui.tasks - -data class TasksUiState(val tasks: List = emptyList()) - -data class TaskUiState( - val name: String, - val description: String? = null, - val days: String? = null, -) \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/tasks/TasksViewModel.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/tasks/TasksViewModel.kt deleted file mode 100644 index 3c9b50b..0000000 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/tasks/TasksViewModel.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.braiso_22.cozycave.feature_task.ui.tasks - -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.braiso_22.cozycave.feature_task.domain.use_case.DeleteTaskUseCase -import com.braiso_22.cozycave.feature_task.domain.use_case.GetTasksUseCase -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import javax.inject.Inject - -@HiltViewModel -class TasksViewModel @Inject constructor( - private val getTasksUseCase: GetTasksUseCase, - deleteTasksUseCase: DeleteTaskUseCase, -) : ViewModel() { - private val _state = mutableStateOf(TasksUiState()) - val state: State = _state - private var updateStateJob: Job? = null - - init { - updateState() - } - - private fun updateState() { - updateStateJob?.cancel() - updateStateJob = getTasksUseCase().onEach { taskList -> - _state.value = state.value.copy( - tasks = taskList.map { - TaskUiState( - name = it.name, - description = it.description, - days = it.days, - ) - } - ) - }.launchIn(viewModelScope) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/tasks/components/TaskItem.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/tasks/components/TaskItem.kt deleted file mode 100644 index d08853c..0000000 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/tasks/components/TaskItem.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.braiso_22.cozycave.feature_task.ui.tasks.components - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Card -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.braiso_22.cozycave.feature_task.ui.tasks.TaskUiState - -@Composable -fun TaskItem( - taskUiState: TaskUiState, - modifier: Modifier = Modifier, -) { - Card( - modifier = modifier - ) { - Column(modifier = Modifier.padding(8.dp)) { - Text(text = taskUiState.name, style = MaterialTheme.typography.titleLarge) - Spacer(modifier = Modifier.height(8.dp)) - Text(text = taskUiState.description ?: "") - Spacer(modifier = Modifier.height(8.dp)) - Text(text = taskUiState.days ?: "") - } - } -} - -@Preview -@Composable -fun TaskItemPreview() { - TaskItem( - TaskUiState( - name = "Task 1", - description = "Description 1", - days = "Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday" - ), - modifier = Modifier - .padding(8.dp) - .fillMaxWidth() - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/tasks/components/TasksList.kt b/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/tasks/components/TasksList.kt deleted file mode 100644 index 1aae490..0000000 --- a/app/src/main/java/com/braiso_22/cozycave/feature_task/ui/tasks/components/TasksList.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.braiso_22.cozycave.feature_task.ui.tasks.components - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.braiso_22.cozycave.feature_task.ui.tasks.TaskUiState - -@Composable -fun TasksList( - tasks: List, - modifier: Modifier = Modifier, -) { - LazyColumn(modifier = modifier) { - items(tasks) { task -> - TaskItem( - taskUiState = task, - modifier = Modifier.padding(8.dp).fillMaxWidth() - ) - } - } -} - -@Preview(showBackground = true) -@Composable -fun TasksListPreview() { - TasksList( - listOf( - TaskUiState( - name = "Task 1", - description = "Description 1", - days = "Monday, Tuesday, Wednesday, Friday, Saturday" - ), - TaskUiState( - name = "Task 2", - description = "Description 2", - days = "Monday, Wednesday, Thursday, Saturday, Sunday" - ), - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/router/AppNavigation.kt b/app/src/main/java/com/braiso_22/cozycave/router/AppNavigation.kt index fcf18d5..d8b52e5 100644 --- a/app/src/main/java/com/braiso_22/cozycave/router/AppNavigation.kt +++ b/app/src/main/java/com/braiso_22/cozycave/router/AppNavigation.kt @@ -1,13 +1,28 @@ package com.braiso_22.cozycave.router import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.material3.windowsizeclass.WindowSizeClass +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import com.braiso_22.cozycave.feature_task.ui.tasks.TasksScreen +import androidx.navigation.navArgument +import com.braiso_22.cozycave.feature_execution.presentation.add_edit_execution.AddEditExecutionScreen +import com.braiso_22.cozycave.feature_task.presentation.add_edit_task.AddEditTaskScreen +import com.braiso_22.cozycave.feature_task.presentation.show_task_detail.TaskDetailScreen +import com.braiso_22.cozycave.feature_task.presentation.tasks.TasksScreen +import com.braiso_22.cozycave.router.NavigationViewModel.UiEvent +import kotlinx.coroutines.flow.collectLatest /** * A composable function that sets up the navigation in the app @@ -16,26 +31,131 @@ import com.braiso_22.cozycave.feature_task.ui.tasks.TasksScreen fun AppNavigation( windowSizeClass: WindowSizeClass, modifier: Modifier = Modifier, + navigationViewModel: NavigationViewModel = hiltViewModel(), ) { + val navController = rememberNavController() + val snackbarHostState = remember { SnackbarHostState() } - NavHost( - navController = navController, - startDestination = AppScreens.Tasks.route, - modifier = modifier, - ) { - composable(AppScreens.Tasks.route) { - TasksScreen( - windowSizeClass = windowSizeClass, - modifier = Modifier.fillMaxSize(), - onClickAddTask = { - navController.navigate(AppScreens.AddEditTask.route) - }, - onClickTask = { taskId -> - navController.navigate("${AppScreens.AddEditTask.route}/$taskId") + LaunchedEffect(key1 = Unit) { + navigationViewModel.eventFlow.collectLatest { event -> + when (event) { + is NavigationEvent.ShowSnackBar -> { + snackbarHostState.showSnackbar(event.message) } - ) + } } } + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + modifier = modifier + ) { padding -> + NavHost( + navController = navController, + startDestination = AppScreens.Tasks.route, + modifier = Modifier.padding(padding), + ) { + composable(AppScreens.Tasks.route) { + TasksScreen( + windowSizeClass = windowSizeClass, + modifier = Modifier.fillMaxSize(), + onClickAddTask = { + navController.navigate(AppScreens.AddEditTask.route + 0 + "/false") + }, + onSeeDetail = { taskId -> + navController.navigate(AppScreens.TaskDetail.route + taskId) + }, + onAddExecution = { taskId -> + navController.navigate(AppScreens.AddEditExecution.route + taskId + "/true" + "/0") + }, + onEdit = { taskId -> + navController.navigate(AppScreens.AddEditTask.route + taskId + "/true") + }, + ) + } + composable( + route = AppScreens.AddEditTask.route + "{taskId}" + "/{edit}", + arguments = listOf( + navArgument( + name = "taskId", + ) { + type = NavType.IntType + defaultValue = 0 + }, + navArgument( + name = "edit", + ) { + type = NavType.BoolType + defaultValue = false + } + ) + ) { + AddEditTaskScreen( + onBack = { + navController.popBackStack() + }, + onUiMessage = { message -> + navigationViewModel.onEvent( + UiEvent.ShowSnackBar(message) + ) + }, + modifier = Modifier.fillMaxSize(), + ) + } + composable( + route = AppScreens.AddEditExecution.route + "{relatedId}" + "/{edit}" + "/{executionId}", + arguments = listOf( + navArgument( + name = "relatedId", + ) { + type = NavType.IntType + defaultValue = 0 + }, + navArgument( + name = "edit", + ) { + type = NavType.BoolType + defaultValue = false + }, + navArgument( + name = "executionId", + ) { + type = NavType.IntType + defaultValue = 0 + } + ) + ) { + AddEditExecutionScreen( + windowSizeClass = windowSizeClass, + onMessage = { + navigationViewModel.onEvent(UiEvent.ShowSnackBar(it)) + }, + onClickBack = { + navController.popBackStack() + }, + modifier = modifier.fillMaxSize() + ) + } + composable( + route = AppScreens.TaskDetail.route + "{taskId}", + arguments = listOf( + navArgument( + name = "taskId", + ) { + type = NavType.IntType + defaultValue = 0 + }, + ) + ) { + TaskDetailScreen( + windowSizeClass = windowSizeClass, + onBack = { + navController.popBackStack() + }, + modifier = modifier.fillMaxSize() + ) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/router/AppScreens.kt b/app/src/main/java/com/braiso_22/cozycave/router/AppScreens.kt index 61ea25f..bed6fff 100644 --- a/app/src/main/java/com/braiso_22/cozycave/router/AppScreens.kt +++ b/app/src/main/java/com/braiso_22/cozycave/router/AppScreens.kt @@ -4,6 +4,8 @@ package com.braiso_22.cozycave.router * Sealed class that represents the screens in the app */ sealed class AppScreens(val route: String) { - object Tasks : AppScreens("tasks") - object AddEditTask : AppScreens("add_edit_task/{taskId}") + data object Tasks : AppScreens("tasks") + data object AddEditTask : AppScreens("add_edit_task/") + data object AddEditExecution : AppScreens("add_edit_execution/") + data object TaskDetail: AppScreens("taskDetail/") } \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/router/NavigationEvent.kt b/app/src/main/java/com/braiso_22/cozycave/router/NavigationEvent.kt new file mode 100644 index 0000000..350e51a --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/router/NavigationEvent.kt @@ -0,0 +1,5 @@ +package com.braiso_22.cozycave.router + +sealed class NavigationEvent { + data class ShowSnackBar(val message: String) : NavigationEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/braiso_22/cozycave/router/NavigationViewModel.kt b/app/src/main/java/com/braiso_22/cozycave/router/NavigationViewModel.kt new file mode 100644 index 0000000..874b905 --- /dev/null +++ b/app/src/main/java/com/braiso_22/cozycave/router/NavigationViewModel.kt @@ -0,0 +1,26 @@ +package com.braiso_22.cozycave.router + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +class NavigationViewModel : ViewModel() { + + private val _eventFlow = MutableSharedFlow() + val eventFlow = _eventFlow.asSharedFlow() + + fun onEvent(event: UiEvent) = viewModelScope.launch { + when (event) { + is UiEvent.ShowSnackBar -> { + _eventFlow.emit(NavigationEvent.ShowSnackBar(event.message)) + } + } + } + + + sealed class UiEvent { + data class ShowSnackBar(val message: String) : UiEvent() + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8a291b7..b5cda0a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,16 @@ CozyCave + Add new task + Go back + Edit this task + Save + Cancel + End time + End date + Start time + Start date + Accept + Add execution + Execution saved correctly + Task detail \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d328322..aca1020 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +1,13 @@ [versions] -activityCompose = "1.8.2" +activityCompose = "1.9.0" agp = "8.3.2" kotlin = "1.9.20" ksp = "1.9.20-1.0.13" -androidCore = "1.12.0" -composeUi = "1.6.3" -composeMaterial = "1.2.1" +androidCore = "1.13.0" +composeUi = "1.6.6" +composeMaterial = "1.3.0-alpha05" androidTest = "1.1.5" -composeBom = "2024.02.02" +composeBom = "2024.04.01" coroutines = "1.7.3" dagger = "2.51" dokka = "1.9.10" @@ -17,7 +17,7 @@ junit = "4.13.2" kotlinSerializable = "1.6.3" lifecycle = "2.7.0" navigation = "2.7.7" -okhttp = "4.9.1" +okhttp = "4.10.0" retrofit = "2.9.0" roomVersion = "2.6.1" @@ -33,6 +33,7 @@ androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-lived androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "composeUi" } androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "composeMaterial" } androidx-material3-window-size = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "composeMaterial" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" }