Course work: implement full performance CRUD

This commit is contained in:
abazov73 2023-12-27 01:21:02 +04:00
parent a687ff002f
commit 2eca8db087
14 changed files with 606 additions and 14 deletions

View File

@ -67,7 +67,8 @@ dependencies {
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material3:material3:1.1.2")
implementation("androidx.compose.material:material:1.4.3")
// Room
val room_version = "2.5.2"

View File

@ -60,6 +60,22 @@ interface MyServerService {
@Body credentials: Credentials
): Token
@POST("api/performances")
suspend fun createPerformance(
@Body performance: PerformanceRemote,
)
@PUT("api/performances/{id}")
suspend fun updatePerformance(
@Path("id") id: Int,
@Body student: PerformanceRemote,
)
@DELETE("api/performances/{id}")
suspend fun deletePerformance(
@Path("id") id: Int,
)
companion object {
private const val BASE_URL = "http://10.0.2.2:8000/"

View File

@ -40,4 +40,14 @@ suspend fun PerformanceRemote.toPerformanceWithPeople(service: MyServerService):
service.getPerson(director_id).toPerson(),
actorsList.toList()
)
}
}
fun Performance.toPerformanceRemote(): PerformanceRemote = PerformanceRemote(
performance_uid!!,
title,
description,
authorId!!,
directorId!!,
imageURL,
previewImageURL
)

View File

@ -6,6 +6,7 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.example.mobile_labs.api.MyServerService
import com.example.mobile_labs.api.models.toPerformance
import com.example.mobile_labs.api.models.toPerformanceRemote
import com.example.mobile_labs.api.models.toPerformanceWithPeople
import com.example.mobile_labs.api.people.RestPersonRepository
import com.example.mobile_labs.common.AppDataContainer
@ -50,4 +51,16 @@ class RestPerformanceRepository(
override suspend fun getPerformance(uid: Int): PerformanceWithPeople =
service.getPerformance(uid).toPerformanceWithPeople(service)
override suspend fun insertPerformance(performance: Performance) {
service.createPerformance(performance.toPerformanceRemote())
}
override suspend fun updatePerformance(performance: Performance) {
service.updatePerformance(performance.performance_uid!!, performance.toPerformanceRemote())
}
override suspend fun deletePerformance(performance: Performance) {
service.deletePerformance(performance.performance_uid !!)
}
}

View File

@ -64,6 +64,6 @@ class AppDataContainer(val context: Context) : AppContainer {
companion object {
const val TIMEOUT = 5000L
const val LIMIT = 3
const val LIMIT = 20
}
}

View File

@ -8,7 +8,9 @@ import androidx.lifecycle.viewmodel.viewModelFactory
import com.example.mobile_labs.TheatreApplication
import com.example.mobile_labs.ui.event.list.EventListViewModel
import com.example.mobile_labs.ui.login.LoginViewModel
import com.example.mobile_labs.ui.performance.list.AdminPerformanceListViewModel
import com.example.mobile_labs.ui.performance.list.PerformanceListViewModel
import com.example.mobile_labs.ui.performance.view.AdminPerformanceViewModel
import com.example.mobile_labs.ui.performance.view.PerformanceViewModel
import com.example.mobile_labs.ui.person.list.PeopleListViewModel
@ -29,6 +31,12 @@ object AppViewModelProvider {
initializer {
LoginViewModel()
}
initializer {
AdminPerformanceListViewModel(theatreApplication().container.performanceRestRepository)
}
initializer {
AdminPerformanceViewModel(this.createSavedStateHandle(), theatreApplication().container.performanceRestRepository)
}
}
}

View File

@ -8,4 +8,7 @@ import kotlinx.coroutines.flow.Flow
interface PerformanceRepository {
fun getAllPerformances(): Flow<PagingData<Performance>>
suspend fun getPerformance(uid: Int): PerformanceWithPeople
suspend fun insertPerformance(performance: Performance)
suspend fun updatePerformance(performance: Performance)
suspend fun deletePerformance(performance: Performance)
}

View File

@ -24,11 +24,11 @@ class OfflinePerformanceRepository(private val performanceDao: PerformanceDao) :
).flow
override suspend fun getPerformance(uid: Int): PerformanceWithPeople = performanceDao.getByUid(uid).first();
suspend fun insertPerformance(performance: Performance) = performanceDao.insert(performance);
override suspend fun insertPerformance(performance: Performance) = performanceDao.insert(performance);
suspend fun updatePerformance(performance: Performance) = performanceDao.update(performance);
override suspend fun updatePerformance(performance: Performance) = performanceDao.update(performance);
suspend fun deletePerformance(performance: Performance) = performanceDao.delete(performance);
override suspend fun deletePerformance(performance: Performance) = performanceDao.delete(performance);
suspend fun clearPerformances() = performanceDao.deleteAll()

View File

@ -1,6 +1,7 @@
package com.example.mobile_labs.ui.navigation
import android.content.res.Configuration
import android.util.Log
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
@ -37,7 +38,9 @@ import com.example.mobile_labs.api.MyServerService
import com.example.mobile_labs.ui.about.About
import com.example.mobile_labs.ui.event.list.EventList
import com.example.mobile_labs.ui.login.Login
import com.example.mobile_labs.ui.performance.list.AdminPerformanceList
import com.example.mobile_labs.ui.performance.list.PerformanceList
import com.example.mobile_labs.ui.performance.view.AdminPerformanceView
import com.example.mobile_labs.ui.performance.view.PerformanceView
import com.example.mobile_labs.ui.person.list.PeopleList
@ -79,8 +82,26 @@ fun Navbar(
modifier: Modifier = Modifier
) {
NavigationBar(modifier) {
Screen.bottomBarItems.forEach { screen ->
if (MyServerService.getToken().isBlank() || screen.route !== "login") {
if (MyServerService.getToken().isBlank()) {
Screen.bottomBarItems.forEach { screen ->
NavigationBarItem(
icon = { Icon(screen.icon, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = false
}
launchSingleTop = true
restoreState = false
}
}
)
}
}
else {
Screen.adminBottomBarItems.forEach { screen ->
NavigationBarItem(
icon = { Icon(screen.icon, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
@ -113,7 +134,12 @@ fun Navhost(
) {
composable(Screen.Schedule.route) { EventList(navController) }
composable(Screen.Repertoire.route) {
PerformanceList(navController)
if (MyServerService.getToken().isNotBlank()) {
AdminPerformanceList(navController)
}
else {
PerformanceList(navController)
}
}
composable(Screen.PeopleList.route) { PeopleList() }
composable(Screen.About.route) { About() }
@ -124,7 +150,12 @@ fun Navhost(
Screen.PerformanceView.route,
arguments = listOf(navArgument("id") { type = NavType.IntType })
) { backStackEntry ->
backStackEntry.arguments?.let { PerformanceView() }
if (MyServerService.getToken().isNotBlank()) {
AdminPerformanceView(navController)
}
else {
backStackEntry.arguments?.let { PerformanceView() }
}
}
}
}

View File

@ -38,16 +38,16 @@ enum class Screen(
);
companion object {
val bottomBarItems = if (MyServerService.getToken().isBlank()) listOf(
val bottomBarItems = listOf(
Schedule,
Repertoire,
PeopleList,
About,
Login
) else listOf(
Schedule,
)
val adminBottomBarItems = listOf(
Repertoire,
PeopleList,
About,
)

View File

@ -0,0 +1,261 @@
package com.example.mobile_labs.ui.performance.list
import android.content.res.Configuration
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.spring
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DismissDirection
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.DismissState
import androidx.compose.material3.DismissValue
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import coil.compose.AsyncImage
import com.example.mobile_labs.R
import com.example.mobile_labs.api.MyServerService
import com.example.mobile_labs.database.performance.model.Performance
import com.example.mobile_labs.common.AppViewModelProvider
import com.example.mobile_labs.ui.navigation.Screen
import com.example.mobile_labs.ui.theme.Mobile_LabsTheme
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@Composable
fun AdminPerformanceList(
navController: NavController,
viewModel: AdminPerformanceListViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val coroutineScope = rememberCoroutineScope()
val performanceListUiState = viewModel.performanceListUiState.collectAsLazyPagingItems()
Scaffold(
topBar = {},
floatingActionButton = {
FloatingActionButton(
onClick = {
val route = Screen.PerformanceView.route.replace("{id}", 0.toString())
navController.navigate(route)
},
) {
Icon(Icons.Filled.Add, "Добавить")
}
}
) { innerPadding ->
PerformanceList(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
performanceList = performanceListUiState,
onClick = { uid: Int ->
val route = Screen.PerformanceView.route.replace("{id}", uid.toString())
navController.navigate(route)
},
onSwipe = { performance: Performance ->
coroutineScope.launch {
viewModel.deleteStudent(performance)
}
}
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DismissBackground(dismissState: DismissState) {
val color = when (dismissState.dismissDirection) {
DismissDirection.StartToEnd -> Color.Transparent
DismissDirection.EndToStart -> Color(0xFFFF1744)
null -> Color.Transparent
}
val direction = dismissState.dismissDirection
Row(
modifier = Modifier
.fillMaxSize()
.background(color)
.padding(12.dp, 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
if (direction == DismissDirection.EndToStart) {
Icon(
Icons.Default.Delete,
contentDescription = "delete",
tint = Color.White
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SwipeToDelete(
dismissState: DismissState,
performance: Performance,
onClick: (uid: Int) -> Unit
) {
SwipeToDismiss(
modifier = Modifier.zIndex(1f),
state = dismissState,
directions = setOf(
DismissDirection.EndToStart
),
background = {
DismissBackground(dismissState)
},
dismissContent = {
PerformanceListItem(performance = performance,
modifier = Modifier
.padding(vertical = 7.dp)
.clickable { onClick(performance.performance_uid!!) })
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PerformanceList(
modifier: Modifier = Modifier,
performanceList: LazyPagingItems<Performance>,
onClick: (uid: Int) -> Unit,
onSwipe: (performance: Performance) -> Unit
) {
Column(
modifier = modifier
) {
if (performanceList.itemCount == 0) {
Text(
text = stringResource(R.string.performance_missing_description),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge
)
} else {
LazyColumn(modifier = Modifier.padding(all = 10.dp)) {
items(
count = performanceList.itemCount,
key = performanceList.itemKey(),
contentType = performanceList.itemContentType()
) { index ->
val performance = performanceList[index]
performance?.let {
var show by remember { mutableStateOf(true) }
val dismissState = rememberDismissState(
confirmValueChange = {
if (it == DismissValue.DismissedToStart ||
it == DismissValue.DismissedToEnd
) {
show = false
true
} else false
}, positionalThreshold = { 200.dp.toPx() }
)
AnimatedVisibility(
show, exit = fadeOut(spring())
) {
SwipeToDelete(
dismissState = dismissState,
performance = performance,
onClick = onClick
)
}
LaunchedEffect(show) {
if (!show) {
delay(800)
onSwipe(performance)
}
}
}
}
}
}
}
}
@Composable
private fun PerformanceListItem(
performance: Performance,
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = modifier.padding(all = 10.dp)
) {
AsyncImage(model = performance.imageURL, contentDescription = "")
Button(
modifier = Modifier
.fillMaxWidth()
.padding(all = 10.dp),
onClick = { }) {
Text(performance.title)
}
}
}
}
@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun AdminSchedulePreview() {
Mobile_LabsTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
PerformanceList(
performanceList = MutableStateFlow(
PagingData.empty<Performance>()
).collectAsLazyPagingItems(),
onClick = {},
onSwipe = {}
)
}
}
}

View File

@ -0,0 +1,17 @@
package com.example.mobile_labs.ui.performance.list
import androidx.lifecycle.ViewModel
import androidx.paging.PagingData
import com.example.mobile_labs.common.PerformanceRepository
import com.example.mobile_labs.database.performance.model.Performance
import kotlinx.coroutines.flow.Flow
class AdminPerformanceListViewModel(
private val performanceRepository: PerformanceRepository
) : ViewModel() {
val performanceListUiState: Flow<PagingData<Performance>> = performanceRepository.getAllPerformances()
suspend fun deleteStudent(performance: Performance) {
performanceRepository.deletePerformance(performance)
}
}

View File

@ -0,0 +1,129 @@
package com.example.mobile_labs.ui.performance.view
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import coil.compose.AsyncImage
import com.example.mobile_labs.R
import com.example.mobile_labs.common.AppViewModelProvider
import com.example.mobile_labs.ui.theme.Mobile_LabsTheme
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AdminPerformanceView(
navController: NavController,
viewModel: AdminPerformanceViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val coroutineScope = rememberCoroutineScope()
PerformanceEdit(
performanceUiState = viewModel.performanceUiState,
onClick = {
coroutineScope.launch {
viewModel.savePerformance()
navController.popBackStack()
}
},
onUpdate = viewModel::updateUiState,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PerformanceEdit(
performanceUiState: AdminPerformanceUiState,
onClick: () -> Unit,
onUpdate: (AdminPerformanceDetails) -> Unit,
) {
Column(
Modifier
.fillMaxWidth()
.padding(all = 10.dp)
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = performanceUiState.performanceDetails.title,
onValueChange = { onUpdate(performanceUiState.performanceDetails.copy(title = it)) },
label = {Text("Название")},
singleLine = true
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = performanceUiState.performanceDetails.description,
onValueChange = { onUpdate(performanceUiState.performanceDetails.copy(description = it)) },
label = {Text("Описание")},
singleLine = true
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = performanceUiState.performanceDetails.imageURL,
onValueChange = { onUpdate(performanceUiState.performanceDetails.copy(imageURL = it)) },
label = { Text("URL изображения") },
singleLine = true,
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = performanceUiState.performanceDetails.previewImageUrl,
onValueChange = { onUpdate(performanceUiState.performanceDetails.copy(previewImageUrl = it)) },
label = {Text("URL превью")},
singleLine = true
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = performanceUiState.performanceDetails.authorId.toString(),
onValueChange = { onUpdate(performanceUiState.performanceDetails.copy(authorId = it.toInt())) },
label = {Text("Автор")},
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone)
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = performanceUiState.performanceDetails.directorId.toString(),
onValueChange = { onUpdate(performanceUiState.performanceDetails.copy(directorId = it.toInt())) },
label = {Text("Режиссер")},
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone)
)
Button(
onClick = onClick,
enabled = performanceUiState.isEntryValid,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Сохранить")
}
}
}
@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun AdminPerformanceViewPreview() {
Mobile_LabsTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
PerformanceView()
}
}
}

View File

@ -0,0 +1,103 @@
package com.example.mobile_labs.ui.performance.view
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.mobile_labs.database.performance.model.PerformanceWithPeople
import com.example.mobile_labs.common.PerformanceRepository
import com.example.mobile_labs.database.performance.model.Performance
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class AdminPerformanceViewModel(
savedStateHandle: SavedStateHandle,
private val performanceRepository: PerformanceRepository
) : ViewModel() {
var performanceUiState by mutableStateOf(AdminPerformanceUiState())
private set
private val performanceUid: Int = checkNotNull(savedStateHandle["id"])
init {
viewModelScope.launch {
if (performanceUid > 0) {
performanceUiState = performanceRepository.getPerformance(performanceUid)
.toUiStateAdmin()
}
}
}
fun updateUiState(performanceDetails: AdminPerformanceDetails) {
performanceUiState = AdminPerformanceUiState(
performanceDetails = performanceDetails,
isEntryValid = validateInput(performanceDetails)
)
}
suspend fun savePerformance() {
if (validateInput()) {
if (performanceUid > 0) {
performanceRepository.updatePerformance(
performanceUiState.performanceDetails.toPerformance(performanceUid)
)
} else {
performanceRepository.insertPerformance(
performanceUiState.performanceDetails.toPerformance()
)
}
}
}
private fun validateInput(uiState: AdminPerformanceDetails = performanceUiState.performanceDetails): Boolean {
return with(uiState) {
title.isNotBlank()
&& description.isNotBlank()
}
}
}
data class AdminPerformanceUiState(
val performanceDetails: AdminPerformanceDetails = AdminPerformanceDetails(),
val isEntryValid: Boolean = false
)
data class AdminPerformanceDetails(
val title: String = "",
val authorName: String = "",
val actorsList: String = "",
val imageURL: String = "",
val description: String = "",
val authorId: Int = 0,
val directorId: Int = 0,
val previewImageUrl: String = "",
)
fun PerformanceWithPeople.toAdminDetails(): AdminPerformanceDetails = AdminPerformanceDetails(
title = performance.title,
authorName = String.format("%s %s", author.last_name, author.first_name),
actorsList = buildString { for (actor in actors) append(actor.last_name + " " + actor.first_name + "\n") },
imageURL = performance.imageURL,
description = performance.description,
authorId = performance.authorId!!,
directorId = performance.directorId!!,
previewImageUrl = performance.previewImageURL,
)
fun PerformanceWithPeople.toUiStateAdmin(): AdminPerformanceUiState = AdminPerformanceUiState(
performanceDetails = this.toAdminDetails(),
)
fun AdminPerformanceDetails.toPerformance(uid: Int = 0): Performance = Performance(
performance_uid = uid,
title = title,
authorId = authorId,
directorId = directorId,
imageURL = imageURL,
description = description,
previewImageURL = previewImageUrl
)