Task module
This commit is contained in:
@@ -2,6 +2,7 @@ plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -49,5 +50,11 @@ dependencies {
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.retrofit.kotlinx.serialization.converter)
|
||||
implementation(libs.okhttp.logging.interceptor)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
android:icon="@android:drawable/sym_def_app_icon"
|
||||
android:theme="@style/Theme.Haushalt">
|
||||
android:theme="@style/Theme.Haushalt"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
|
||||
@@ -16,10 +16,36 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import de.haushalt.app.ui.task.TaskAllScreen
|
||||
import de.haushalt.app.ui.task.TaskCreateScreen
|
||||
import de.haushalt.app.ui.task.TaskEditScreen
|
||||
import de.haushalt.app.ui.task.TaskScreen
|
||||
|
||||
private val trailMap: Map<String, List<Pair<String, String>>> = mapOf(
|
||||
"start" to listOf("start" to "Haushalt"),
|
||||
"tasks" to listOf("start" to "Haushalt", "tasks" to "Tasks"),
|
||||
"tasks/all" to listOf(
|
||||
"start" to "Haushalt",
|
||||
"tasks" to "Tasks",
|
||||
"tasks/all" to "All",
|
||||
),
|
||||
"tasks/create" to listOf(
|
||||
"start" to "Haushalt",
|
||||
"tasks" to "Tasks",
|
||||
"tasks/create" to "Create",
|
||||
),
|
||||
"tasks/{id}" to listOf(
|
||||
"start" to "Haushalt",
|
||||
"tasks" to "Tasks",
|
||||
"tasks/{id}" to "Edit",
|
||||
),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun MainScreen() {
|
||||
@@ -35,17 +61,33 @@ fun MainScreen() {
|
||||
startDestination = "start",
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
) {
|
||||
composable("start") { StartScreen() }
|
||||
composable("start") {
|
||||
StartScreen(onOpenTasks = { navController.navigate("tasks") })
|
||||
}
|
||||
composable("tasks") {
|
||||
TaskScreen(navController = navController)
|
||||
}
|
||||
composable("tasks/all") {
|
||||
TaskAllScreen(navController = navController)
|
||||
}
|
||||
composable("tasks/create") {
|
||||
TaskCreateScreen(navController = navController)
|
||||
}
|
||||
composable(
|
||||
route = "tasks/{id}",
|
||||
arguments = listOf(navArgument("id") { type = NavType.IntType })
|
||||
) { entry ->
|
||||
val id = entry.arguments?.getInt("id") ?: return@composable
|
||||
TaskEditScreen(navController = navController, taskId = id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Breadcrumb(currentRoute: String?, navController: NavController) {
|
||||
// Trail: (route, label). Erweitert sich pro neuer Destination in späteren Modulen.
|
||||
val trail = listOf(
|
||||
"start" to "Haushalt",
|
||||
)
|
||||
val trail = trailMap[currentRoute] ?: listOf("start" to "Haushalt")
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.surfaceContainer
|
||||
|
||||
@@ -1,7 +1,44 @@
|
||||
package de.haushalt.app
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun StartScreen() {
|
||||
fun StartScreen(onOpenTasks: () -> Unit = {}) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 160.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
item {
|
||||
Card(
|
||||
onClick = onOpenTasks,
|
||||
modifier = Modifier
|
||||
.padding(8.dp)
|
||||
.aspectRatio(1f),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "Tasks",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
app/app/src/main/java/de/haushalt/app/data/ApiClient.kt
Normal file
30
app/app/src/main/java/de/haushalt/app/data/ApiClient.kt
Normal file
@@ -0,0 +1,30 @@
|
||||
package de.haushalt.app.data
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
|
||||
object ApiClient {
|
||||
private const val BASE_URL = "http://192.168.178.34:8080/api/"
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
}
|
||||
|
||||
private val okHttp: OkHttpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(
|
||||
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }
|
||||
)
|
||||
.build()
|
||||
|
||||
val taskApi: TaskApi = Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(okHttp)
|
||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
.create(TaskApi::class.java)
|
||||
}
|
||||
26
app/app/src/main/java/de/haushalt/app/data/Task.kt
Normal file
26
app/app/src/main/java/de/haushalt/app/data/Task.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package de.haushalt.app.data
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class TaskStatus {
|
||||
active,
|
||||
done,
|
||||
inactive,
|
||||
past,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Task(
|
||||
val id: Int? = null,
|
||||
val name: String,
|
||||
val date: String? = null,
|
||||
val status: TaskStatus = TaskStatus.active,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskRequest(
|
||||
val name: String,
|
||||
val date: String? = null,
|
||||
val status: TaskStatus = TaskStatus.active,
|
||||
)
|
||||
33
app/app/src/main/java/de/haushalt/app/data/TaskApi.kt
Normal file
33
app/app/src/main/java/de/haushalt/app/data/TaskApi.kt
Normal file
@@ -0,0 +1,33 @@
|
||||
package de.haushalt.app.data
|
||||
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.PATCH
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface TaskApi {
|
||||
@GET("tasks")
|
||||
suspend fun list(@Query("filter") filter: String? = null): List<Task>
|
||||
|
||||
@GET("tasks/{id}")
|
||||
suspend fun get(@Path("id") id: Int): Task
|
||||
|
||||
@POST("tasks")
|
||||
suspend fun create(@Body body: TaskRequest): Task
|
||||
|
||||
@PUT("tasks/{id}")
|
||||
suspend fun update(@Path("id") id: Int, @Body body: TaskRequest): Task
|
||||
|
||||
@DELETE("tasks/{id}")
|
||||
suspend fun delete(@Path("id") id: Int)
|
||||
|
||||
@PATCH("tasks/{id}/toggle")
|
||||
suspend fun toggle(@Path("id") id: Int): Task
|
||||
|
||||
@GET("tasks/statuses")
|
||||
suspend fun statuses(): List<TaskStatus>
|
||||
}
|
||||
12
app/app/src/main/java/de/haushalt/app/ui/task/DateFormat.kt
Normal file
12
app/app/src/main/java/de/haushalt/app/ui/task/DateFormat.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package de.haushalt.app.ui.task
|
||||
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
private val germanFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
|
||||
|
||||
fun formatDate(iso: String): String = try {
|
||||
LocalDate.parse(iso.take(10)).format(germanFormatter)
|
||||
} catch (e: Exception) {
|
||||
iso
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package de.haushalt.app.ui.task
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.DatePicker
|
||||
import androidx.compose.material3.DatePickerDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberDatePickerState
|
||||
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.Modifier
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DatePickerField(
|
||||
value: String,
|
||||
onChange: (String) -> Unit,
|
||||
label: String = "Datum",
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val display = if (value.isBlank()) "" else try {
|
||||
formatDate(value)
|
||||
} catch (e: Exception) {
|
||||
value
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { showDialog = true }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = display,
|
||||
onValueChange = {},
|
||||
enabled = false,
|
||||
readOnly = true,
|
||||
label = { Text(label) },
|
||||
placeholder = { Text("tt.mm.jjjj") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
disabledTextColor = MaterialTheme.colorScheme.onSurface,
|
||||
disabledBorderColor = MaterialTheme.colorScheme.outline,
|
||||
disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (showDialog) {
|
||||
val initialMillis = value.takeIf { it.isNotBlank() }?.let {
|
||||
try {
|
||||
LocalDate.parse(it.take(10))
|
||||
.atStartOfDay(ZoneOffset.UTC)
|
||||
.toInstant()
|
||||
.toEpochMilli()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
val state = rememberDatePickerState(initialSelectedDateMillis = initialMillis)
|
||||
|
||||
DatePickerDialog(
|
||||
onDismissRequest = { showDialog = false },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
state.selectedDateMillis?.let { millis ->
|
||||
val date = Instant.ofEpochMilli(millis)
|
||||
.atZone(ZoneOffset.UTC)
|
||||
.toLocalDate()
|
||||
onChange(date.format(DateTimeFormatter.ISO_LOCAL_DATE))
|
||||
}
|
||||
showDialog = false
|
||||
}) { Text("OK") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDialog = false }) { Text("Abbrechen") }
|
||||
},
|
||||
) {
|
||||
DatePicker(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package de.haushalt.app.ui.task
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
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.Modifier
|
||||
import de.haushalt.app.data.TaskStatus
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun StatusDropdown(
|
||||
current: TaskStatus,
|
||||
selectable: List<TaskStatus>,
|
||||
onChange: (TaskStatus) -> Unit,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = current.name,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Status") },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier
|
||||
.menuAnchor(MenuAnchorType.PrimaryNotEditable, enabled = true)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
) {
|
||||
selectable.forEach { status ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(status.name) },
|
||||
onClick = {
|
||||
onChange(status)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
160
app/app/src/main/java/de/haushalt/app/ui/task/TaskAllScreen.kt
Normal file
160
app/app/src/main/java/de/haushalt/app/ui/task/TaskAllScreen.kt
Normal file
@@ -0,0 +1,160 @@
|
||||
package de.haushalt.app.ui.task
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.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.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
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.draw.alpha
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import de.haushalt.app.data.Task
|
||||
import de.haushalt.app.data.TaskStatus
|
||||
|
||||
@Composable
|
||||
fun TaskAllScreen(
|
||||
navController: NavController,
|
||||
viewModel: TaskAllViewModel = viewModel(),
|
||||
) {
|
||||
var deleteCandidate by remember { mutableStateOf<Int?>(null) }
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
viewModel.refresh()
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
IconButton(onClick = { navController.navigate("tasks/create") }) {
|
||||
Icon(Icons.Filled.Add, contentDescription = "Neuer Task")
|
||||
}
|
||||
IconButton(onClick = { viewModel.refresh() }) {
|
||||
Icon(Icons.Filled.Refresh, contentDescription = "Neu laden")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.padding(4.dp))
|
||||
|
||||
when {
|
||||
viewModel.isLoading && viewModel.tasks.isEmpty() -> Text("Lädt…")
|
||||
viewModel.error != null -> Text(
|
||||
viewModel.error ?: "",
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
viewModel.tasks.isEmpty() -> Text("Keine Tasks.")
|
||||
else -> LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(viewModel.tasks, key = { it.id ?: 0 }) { task ->
|
||||
TaskAllRow(
|
||||
task = task,
|
||||
onClick = { task.id?.let(viewModel::toggle) },
|
||||
onEdit = { task.id?.let { navController.navigate("tasks/$it") } },
|
||||
onDelete = { deleteCandidate = task.id },
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deleteCandidate != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { deleteCandidate = null },
|
||||
title = { Text("Task löschen?") },
|
||||
text = { Text("Dieser Task wird dauerhaft entfernt.") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
deleteCandidate?.let(viewModel::delete)
|
||||
deleteCandidate = null
|
||||
}) { Text("Löschen") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { deleteCandidate = null }) { Text("Abbrechen") }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TaskAllRow(
|
||||
task: Task,
|
||||
onClick: () -> Unit,
|
||||
onEdit: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.alpha(if (task.status == TaskStatus.past) 0.5f else 1f)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(vertical = 8.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = task.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textDecoration = if (task.status == TaskStatus.done) TextDecoration.LineThrough else null,
|
||||
fontStyle = if (task.status == TaskStatus.inactive) FontStyle.Italic else null,
|
||||
color = when (task.status) {
|
||||
TaskStatus.active -> MaterialTheme.colorScheme.onSurface
|
||||
TaskStatus.done -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
TaskStatus.inactive -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
TaskStatus.past -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
task.date?.let {
|
||||
Text(
|
||||
text = formatDate(it),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onEdit) {
|
||||
Icon(Icons.Filled.Edit, contentDescription = "Bearbeiten")
|
||||
}
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(Icons.Filled.Delete, contentDescription = "Löschen")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package de.haushalt.app.ui.task
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.haushalt.app.data.ApiClient
|
||||
import de.haushalt.app.data.Task
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TaskAllViewModel : ViewModel() {
|
||||
var tasks by mutableStateOf<List<Task>>(emptyList())
|
||||
private set
|
||||
var isLoading by mutableStateOf(false)
|
||||
private set
|
||||
var error by mutableStateOf<String?>(null)
|
||||
private set
|
||||
|
||||
init {
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
viewModelScope.launch {
|
||||
isLoading = true
|
||||
error = null
|
||||
try {
|
||||
tasks = ApiClient.taskApi.list()
|
||||
} catch (e: Exception) {
|
||||
error = e.message
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggle(id: Int) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val updated = ApiClient.taskApi.toggle(id)
|
||||
tasks = tasks.map { if (it.id == id) updated else it }
|
||||
} catch (e: Exception) {
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(id: Int) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
ApiClient.taskApi.delete(id)
|
||||
tasks = tasks.filter { it.id != id }
|
||||
} catch (e: Exception) {
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package de.haushalt.app.ui.task
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
|
||||
@Composable
|
||||
fun TaskCreateScreen(
|
||||
navController: NavController,
|
||||
viewModel: TaskCreateViewModel = viewModel(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = viewModel.name,
|
||||
onValueChange = { viewModel.name = it },
|
||||
label = { Text("Name") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
DatePickerField(
|
||||
value = viewModel.date,
|
||||
onChange = { viewModel.date = it },
|
||||
)
|
||||
|
||||
StatusDropdown(
|
||||
current = viewModel.status,
|
||||
selectable = viewModel.availableStatuses,
|
||||
onChange = { viewModel.status = it },
|
||||
)
|
||||
|
||||
viewModel.error?.let {
|
||||
Text(it, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
enabled = !viewModel.isSubmitting && viewModel.name.isNotBlank(),
|
||||
onClick = { viewModel.save { navController.popBackStack() } },
|
||||
) {
|
||||
Text("Speichern")
|
||||
}
|
||||
OutlinedButton(onClick = { navController.popBackStack() }) {
|
||||
Text("Abbrechen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package de.haushalt.app.ui.task
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.haushalt.app.data.ApiClient
|
||||
import de.haushalt.app.data.TaskRequest
|
||||
import de.haushalt.app.data.TaskStatus
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TaskCreateViewModel : ViewModel() {
|
||||
var name by mutableStateOf("")
|
||||
var date by mutableStateOf("")
|
||||
var status by mutableStateOf(TaskStatus.active)
|
||||
var availableStatuses by mutableStateOf<List<TaskStatus>>(emptyList())
|
||||
private set
|
||||
var isSubmitting by mutableStateOf(false)
|
||||
private set
|
||||
var error by mutableStateOf<String?>(null)
|
||||
private set
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
availableStatuses = ApiClient.taskApi.statuses()
|
||||
} catch (e: Exception) {
|
||||
availableStatuses = emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun save(onSuccess: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
isSubmitting = true
|
||||
error = null
|
||||
try {
|
||||
ApiClient.taskApi.create(
|
||||
TaskRequest(
|
||||
name = name,
|
||||
date = date.ifBlank { null },
|
||||
status = status,
|
||||
)
|
||||
)
|
||||
onSuccess()
|
||||
} catch (e: Exception) {
|
||||
error = e.message
|
||||
} finally {
|
||||
isSubmitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package de.haushalt.app.ui.task
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
|
||||
@Composable
|
||||
fun TaskEditScreen(
|
||||
navController: NavController,
|
||||
taskId: Int,
|
||||
viewModel: TaskEditViewModel = viewModel(),
|
||||
) {
|
||||
LaunchedEffect(taskId) {
|
||||
viewModel.load(taskId)
|
||||
}
|
||||
|
||||
if (viewModel.isLoading) {
|
||||
Text("Lädt…", modifier = Modifier.padding(16.dp))
|
||||
return
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = viewModel.name,
|
||||
onValueChange = { viewModel.name = it },
|
||||
label = { Text("Name") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
DatePickerField(
|
||||
value = viewModel.date,
|
||||
onChange = { viewModel.date = it },
|
||||
)
|
||||
|
||||
StatusDropdown(
|
||||
current = viewModel.status,
|
||||
selectable = viewModel.availableStatuses,
|
||||
onChange = { viewModel.status = it },
|
||||
)
|
||||
|
||||
viewModel.error?.let {
|
||||
Text(it, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
enabled = !viewModel.isSubmitting && viewModel.name.isNotBlank(),
|
||||
onClick = { viewModel.update(taskId) { navController.popBackStack() } },
|
||||
) {
|
||||
Text("Aktualisieren")
|
||||
}
|
||||
OutlinedButton(onClick = { viewModel.reset() }) {
|
||||
Text("Zurücksetzen")
|
||||
}
|
||||
OutlinedButton(onClick = { navController.popBackStack() }) {
|
||||
Text("Abbrechen")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package de.haushalt.app.ui.task
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.haushalt.app.data.ApiClient
|
||||
import de.haushalt.app.data.Task
|
||||
import de.haushalt.app.data.TaskRequest
|
||||
import de.haushalt.app.data.TaskStatus
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TaskEditViewModel : ViewModel() {
|
||||
private var original: Task? = null
|
||||
|
||||
var name by mutableStateOf("")
|
||||
var date by mutableStateOf("")
|
||||
var status by mutableStateOf(TaskStatus.active)
|
||||
var availableStatuses by mutableStateOf<List<TaskStatus>>(emptyList())
|
||||
private set
|
||||
var isLoading by mutableStateOf(false)
|
||||
private set
|
||||
var isSubmitting by mutableStateOf(false)
|
||||
private set
|
||||
var error by mutableStateOf<String?>(null)
|
||||
private set
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
availableStatuses = ApiClient.taskApi.statuses()
|
||||
} catch (e: Exception) {
|
||||
availableStatuses = emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun load(id: Int) {
|
||||
viewModelScope.launch {
|
||||
isLoading = true
|
||||
error = null
|
||||
try {
|
||||
val task = ApiClient.taskApi.get(id)
|
||||
original = task
|
||||
applyTask(task)
|
||||
} catch (e: Exception) {
|
||||
error = e.message
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun update(id: Int, onSuccess: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
isSubmitting = true
|
||||
error = null
|
||||
try {
|
||||
ApiClient.taskApi.update(
|
||||
id,
|
||||
TaskRequest(
|
||||
name = name,
|
||||
date = date.ifBlank { null },
|
||||
status = status,
|
||||
)
|
||||
)
|
||||
onSuccess()
|
||||
} catch (e: Exception) {
|
||||
error = e.message
|
||||
} finally {
|
||||
isSubmitting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
original?.let { applyTask(it) }
|
||||
}
|
||||
|
||||
private fun applyTask(task: Task) {
|
||||
name = task.name
|
||||
date = task.date?.take(10) ?: ""
|
||||
status = task.status
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package de.haushalt.app.ui.task
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.haushalt.app.data.ApiClient
|
||||
import de.haushalt.app.data.Task
|
||||
import de.haushalt.app.data.TaskStatus
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TaskListViewModel : ViewModel() {
|
||||
var tasks by mutableStateOf<List<Task>>(emptyList())
|
||||
private set
|
||||
var showDone by mutableStateOf(false)
|
||||
var isLoading by mutableStateOf(false)
|
||||
private set
|
||||
var error by mutableStateOf<String?>(null)
|
||||
private set
|
||||
|
||||
val visibleTasks: List<Task>
|
||||
get() = if (showDone) tasks else tasks.filter { it.status != TaskStatus.done }
|
||||
|
||||
val groupedTasks: Map<String, List<Task>>
|
||||
get() = visibleTasks.groupBy { task -> task.date?.let { formatDate(it) } ?: "" }
|
||||
|
||||
init {
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
viewModelScope.launch {
|
||||
isLoading = true
|
||||
error = null
|
||||
try {
|
||||
tasks = ApiClient.taskApi.list("current")
|
||||
} catch (e: Exception) {
|
||||
error = e.message
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggle(id: Int) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val updated = ApiClient.taskApi.toggle(id)
|
||||
tasks = tasks.map { if (it.id == id) updated else it }
|
||||
} catch (e: Exception) {
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleShowDone() {
|
||||
showDone = !showDone
|
||||
}
|
||||
}
|
||||
163
app/app/src/main/java/de/haushalt/app/ui/task/TaskScreen.kt
Normal file
163
app/app/src/main/java/de/haushalt/app/ui/task/TaskScreen.kt
Normal file
@@ -0,0 +1,163 @@
|
||||
package de.haushalt.app.ui.task
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.List
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import de.haushalt.app.data.Task
|
||||
import de.haushalt.app.data.TaskStatus
|
||||
|
||||
@Composable
|
||||
fun TaskScreen(
|
||||
navController: NavController,
|
||||
viewModel: TaskListViewModel = viewModel(),
|
||||
) {
|
||||
val tasks = viewModel.visibleTasks
|
||||
val showDone by androidx.compose.runtime.rememberUpdatedState(viewModel.showDone)
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
viewModel.refresh()
|
||||
}
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
IconButton(onClick = { navController.navigate("tasks/create") }) {
|
||||
Icon(Icons.Filled.Add, contentDescription = "Neuer Task")
|
||||
}
|
||||
IconButton(onClick = { navController.navigate("tasks/all") }) {
|
||||
Icon(Icons.AutoMirrored.Filled.List, contentDescription = "Alle Tasks")
|
||||
}
|
||||
IconButton(onClick = { viewModel.toggleShowDone() }) {
|
||||
Icon(
|
||||
imageVector = if (showDone) Icons.Filled.Visibility else Icons.Filled.VisibilityOff,
|
||||
contentDescription = "Erledigte ein-/ausblenden",
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { viewModel.refresh() }) {
|
||||
Icon(Icons.Filled.Refresh, contentDescription = "Neu laden")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.padding(4.dp))
|
||||
|
||||
when {
|
||||
viewModel.isLoading && tasks.isEmpty() -> Text("Lädt…")
|
||||
viewModel.error != null -> Text(viewModel.error ?: "", color = MaterialTheme.colorScheme.error)
|
||||
tasks.isEmpty() -> Text("Keine Tasks.")
|
||||
else -> LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(top = 12.dp, bottom = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
) {
|
||||
viewModel.groupedTasks.forEach { (key, groupTasks) ->
|
||||
item(key = key.ifEmpty { "no-date" }) {
|
||||
TaskGroupCard(
|
||||
title = key.ifEmpty { null },
|
||||
tasks = groupTasks,
|
||||
onToggle = viewModel::toggle,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TaskRow(task: Task, onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(vertical = 12.dp, horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = task.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textDecoration = if (task.status == TaskStatus.done) TextDecoration.LineThrough else null,
|
||||
color = if (task.status == TaskStatus.done) MaterialTheme.colorScheme.onSurfaceVariant
|
||||
else MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TaskGroupCard(
|
||||
title: String?,
|
||||
tasks: List<Task>,
|
||||
onToggle: (Int) -> Unit,
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
OutlinedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column {
|
||||
tasks.forEachIndexed { index, task ->
|
||||
TaskRow(task = task, onClick = { task.id?.let(onToggle) })
|
||||
if (index < tasks.lastIndex) HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (title != null) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.offset(y = (-10).dp),
|
||||
shape = RoundedCornerShape(6.dp),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,10 @@ lifecycleRuntimeKtx = "2.8.7"
|
||||
activityCompose = "1.9.3"
|
||||
composeBom = "2024.12.01"
|
||||
navigationCompose = "2.8.5"
|
||||
retrofit = "2.11.0"
|
||||
okhttp = "4.12.0"
|
||||
kotlinxSerialization = "1.7.3"
|
||||
lifecycleViewModelCompose = "2.8.7"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -16,9 +20,16 @@ androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewModelCompose" }
|
||||
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||
retrofit-kotlinx-serialization-converter = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" }
|
||||
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
|
||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
|
||||
Reference in New Issue
Block a user