exception handling

This commit is contained in:
dasha 2023-12-18 01:43:48 +04:00
parent 42c21b0ce7
commit c0b94141a3
23 changed files with 493 additions and 75189 deletions

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="SERIAL_NUMBER" />
<value value="KFRSEQ6DTWWWQOE6" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-12-17T14:14:18.887820Z" />
</component>
</project>

View File

@ -1,5 +1,6 @@
package com.example.myapplication package com.example.myapplication
import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@ -20,6 +21,7 @@ class MainComposeActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
application.deleteDatabase("pmy-db") application.deleteDatabase("pmy-db")
appContext = applicationContext
setContent { setContent {
PmudemoTheme(darkTheme = isDarkTheme.value) { PmudemoTheme(darkTheme = isDarkTheme.value) {
LaunchedEffect(key1 = true) { LaunchedEffect(key1 = true) {
@ -40,4 +42,7 @@ class MainComposeActivity : ComponentActivity() {
} }
} }
} }
companion object {
lateinit var appContext: Context
}
} }

View File

@ -15,21 +15,17 @@ fun Authenticator(
viewModel: AuthenticatorViewModel = viewModel(factory = AppViewModelProvider.Factory) viewModel: AuthenticatorViewModel = viewModel(factory = AppViewModelProvider.Factory)
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val login = dataStoreManager.getLogin().collectAsState(initial = "").value val login = dataStoreManager.getLogin().collectAsState(initial = "").value
LiveStore.user.value = viewModel.authUiState.user
fun synchronize() { fun synchronize() {
scope.launch { scope.launch {
if (login == "") { if (login == "") {
LiveStore.user.value = null LiveStore.user.value = null
return@launch return@launch
} }
val overlap = viewModel.findUserByLogin(login) viewModel.findUserByLogin(login)
if (overlap == null) {
dataStoreManager.setLogin("")
return@launch
}
LiveStore.user.value = overlap
} }
} }

View File

@ -1,18 +1,27 @@
package com.example.myapplication.composeui package com.example.myapplication.composeui
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.example.myapplication.database.entities.model.User import com.example.myapplication.database.entities.model.User
import com.example.myapplication.database.entities.repository.UserRepository import com.example.myapplication.database.entities.repository.UserRepository
class AuthenticatorViewModel( class AuthenticatorViewModel(
private val userRepository: UserRepository private val userRepository: UserRepository
) : MyViewModel() { ) : MyViewModel() {
suspend fun findUserByLogin(login: String): User? { var authUiState by mutableStateOf(AuthenticatorUiState())
var user: User? = null private set
suspend fun findUserByLogin(login: String) {
runInScope( runInScope(
actionSuccess = { actionSuccess = {
user = userRepository.getUser(login) authUiState = AuthenticatorUiState(userRepository.getUser(login))
},
actionError = {
authUiState = AuthenticatorUiState()
} }
) )
return user
} }
} }
data class AuthenticatorUiState(val user: User? = null)

View File

@ -60,7 +60,6 @@ fun Cart(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.refreshState() viewModel.refreshState()
} }
Cart( Cart(
cartUiState = cartUiState, cartUiState = cartUiState,
modifier = Modifier modifier = Modifier

View File

@ -3,7 +3,6 @@ package com.example.myapplication.composeui
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.example.myapplication.LiveStore import com.example.myapplication.LiveStore
import com.example.myapplication.database.entities.model.Order import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.model.OrderSessionCrossRef import com.example.myapplication.database.entities.model.OrderSessionCrossRef
@ -21,15 +20,21 @@ class CartViewModel(
private val orderRepository: OrderRepository, private val orderRepository: OrderRepository,
private val orderSessionRepository: OrderSessionRepository, private val orderSessionRepository: OrderSessionRepository,
private val userRepository: UserRepository, private val userRepository: UserRepository,
) : ViewModel() { ) : MyViewModel() {
private var isLoading: Boolean = false private var isLoading: Boolean = false
var cartUiState by mutableStateOf(CartUiState()) var cartUiState by mutableStateOf(CartUiState())
private set private set
suspend fun refreshState() { suspend fun refreshState() {
val userId: Int = LiveStore.user.value?.uid ?: 0 val userId: Int = LiveStore.user.value?.uid ?: 0
val cart = userRepository.getCartByUser(userId) runInScope(
cartUiState = CartUiState(cart) actionSuccess = {
val cart = userRepository.getCartByUser(userId)
cartUiState = CartUiState(cart)
}, actionError = {
cartUiState = CartUiState()
}
)
} }
suspend fun addToOrder(sessions: List<SessionFromCart>) { suspend fun addToOrder(sessions: List<SessionFromCart>) {
@ -39,38 +44,61 @@ class CartViewModel(
val userId: Int = LiveStore.user.value?.uid ?: return val userId: Int = LiveStore.user.value?.uid ?: return
if (sessions.isEmpty()) if (sessions.isEmpty())
return return
val orderId = orderRepository.insertOrder(Order(0, userId, LocalDateTime.now())) runInScope(
sessions.forEach { session -> actionSuccess = {
orderSessionRepository.insertOrderSession( val orderId = orderRepository.insertOrder(Order(0, userId, LocalDateTime.now()))
OrderSessionCrossRef( sessions.forEach { session ->
orderId.toInt(), orderSessionRepository.insertOrderSession(
session.uid, OrderSessionCrossRef(
session.price, orderId.toInt(),
session.count session.uid,
) session.price,
) session.count
} )
userSessionRepository.deleteUserSessions(userId) )
refreshState() }
userSessionRepository.deleteUserSessions(userId)
refreshState()
}
)
} }
suspend fun removeFromCart(session: Session, count: Int = 0) { suspend fun removeFromCart(session: Session, count: Int = 0) {
val userId: Int = LiveStore.user.value?.uid ?: return val userId: Int = LiveStore.user.value?.uid ?: return
userSessionRepository.deleteUserSession(UserSessionCrossRef(userId, session.uid, count)) runInScope(
refreshState() actionSuccess = {
userSessionRepository.deleteUserSession(
UserSessionCrossRef(
userId,
session.uid,
count
)
)
refreshState()
}
)
} }
suspend fun updateFromCart(session: Session, count: Int, availableCount: Int): Boolean { suspend fun updateFromCart(session: Session, count: Int, availableCount: Int) {
val userId: Int = LiveStore.user.value?.uid ?: return false val userId: Int = LiveStore.user.value?.uid ?: return
if (count == 0) { if (count == 0) {
removeFromCart(session, count) removeFromCart(session, count)
return false return
} }
if (count > availableCount) if (count > availableCount)
return false return
userSessionRepository.updateUserSession(UserSessionCrossRef(userId, session.uid, count)) runInScope(
refreshState() actionSuccess = {
return true userSessionRepository.updateUserSession(
UserSessionCrossRef(
userId,
session.uid,
count
)
)
refreshState()
}
)
} }
} }

View File

@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.myapplication.api.ApiStatus
import com.example.myapplication.api.session.ReportRemote import com.example.myapplication.api.session.ReportRemote
import com.example.myapplication.database.entities.composeui.AppViewModelProvider import com.example.myapplication.database.entities.composeui.AppViewModelProvider
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -39,57 +40,68 @@ fun Report(
val dateStateStart = rememberDatePickerState(initialDisplayMode = DisplayMode.Input) val dateStateStart = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
val dateStateEnd = rememberDatePickerState(initialDisplayMode = DisplayMode.Input) val dateStateEnd = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
Column(
modifier = Modifier when (viewModel.apiStatus) {
.verticalScroll(rememberScrollState()) ApiStatus.DONE -> {
.fillMaxWidth() Column(
.padding(all = 10.dp), modifier = Modifier
verticalArrangement = Arrangement.Center, .verticalScroll(rememberScrollState())
horizontalAlignment = Alignment.CenterHorizontally .fillMaxWidth()
) .padding(all = 10.dp),
{ verticalArrangement = Arrangement.Center,
Text( horizontalAlignment = Alignment.CenterHorizontally
text = "Начало периода",
style = MaterialTheme.typography.headlineLarge
)
DatePicker(state = dateStateStart)
val selectedDateStart = dateStateStart.selectedDateMillis
if (selectedDateStart != null) {
viewModel.onUpdate(
viewModel.reportUiState.reportDetails.copy(
startDate =
Date(selectedDateStart)
)
) )
} else { {
viewModel.onUpdate(viewModel.reportUiState.reportDetails.copy(startDate = Date(0))) Text(
} text = "Начало периода",
Text( style = MaterialTheme.typography.headlineLarge
text = "Конец периода",
style = MaterialTheme.typography.headlineLarge
)
DatePicker(state = dateStateEnd)
val selectedDateEnd = dateStateEnd.selectedDateMillis
if (selectedDateEnd != null) {
viewModel.onUpdate(
viewModel.reportUiState.reportDetails.copy(
endDate =
Date(selectedDateEnd)
) )
) DatePicker(state = dateStateStart)
} else { val selectedDateStart = dateStateStart.selectedDateMillis
viewModel.onUpdate(viewModel.reportUiState.reportDetails.copy(endDate = Date(0))) if (selectedDateStart != null) {
viewModel.onUpdate(
viewModel.reportUiState.reportDetails.copy(
startDate =
Date(selectedDateStart)
)
)
} else {
viewModel.onUpdate(viewModel.reportUiState.reportDetails.copy(startDate = Date(0)))
}
Text(
text = "Конец периода",
style = MaterialTheme.typography.headlineLarge
)
DatePicker(state = dateStateEnd)
val selectedDateEnd = dateStateEnd.selectedDateMillis
if (selectedDateEnd != null) {
viewModel.onUpdate(
viewModel.reportUiState.reportDetails.copy(
endDate =
Date(selectedDateEnd)
)
)
} else {
viewModel.onUpdate(viewModel.reportUiState.reportDetails.copy(endDate = Date(0)))
}
Button(
onClick = { coroutineScope.launch { viewModel.getReport() } },
enabled = viewModel.reportUiState.isEntryValid,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Получить отчет")
}
Spacer(modifier = Modifier.height(16.dp))
CardScreen(reportData = viewModel.reportResultUiState.report)
}
} }
Button(
onClick = { coroutineScope.launch { viewModel.getReport() } }, ApiStatus.LOADING -> LoadingPlaceholder()
enabled = viewModel.reportUiState.isEntryValid, else -> ErrorPlaceholder(
shape = MaterialTheme.shapes.small, message = viewModel.apiError,
modifier = Modifier.fillMaxWidth() onBack = { }
) { )
Text(text = "Получить отчет")
}
Spacer(modifier = Modifier.height(16.dp))
CardScreen(reportData = viewModel.reportResultUiState.report)
} }
} }

View File

@ -3,12 +3,11 @@ package com.example.myapplication.composeui
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.example.myapplication.api.session.ReportRemote import com.example.myapplication.api.session.ReportRemote
import com.example.myapplication.api.session.RestSessionRepository import com.example.myapplication.api.session.RestSessionRepository
import java.util.Date import java.util.Date
class ReportViewModel(private val serialRepository: RestSessionRepository) : ViewModel() { class ReportViewModel(private val serialRepository: RestSessionRepository) : MyViewModel() {
var reportUiState by mutableStateOf(ReportUiState()) var reportUiState by mutableStateOf(ReportUiState())
private set private set
@ -32,11 +31,17 @@ class ReportViewModel(private val serialRepository: RestSessionRepository) : Vie
suspend fun getReport() { suspend fun getReport() {
if (validateInput()) { if (validateInput()) {
val temp = serialRepository.getReport( runInScope(
reportUiState.reportDetails.startDate, actionSuccess = {
reportUiState.reportDetails.endDate val temp = serialRepository.getReport(
reportUiState.reportDetails.startDate,
reportUiState.reportDetails.endDate
)
reportResultUiState = ReportResultUiState(temp)
}, actionError = {
reportResultUiState = ReportResultUiState()
}
) )
reportResultUiState = ReportResultUiState(temp)
} }
} }
} }

View File

@ -93,22 +93,21 @@ fun Topbar(
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
if (currentScreen?.route == Screen.CinemaList.route) { Search(
Search( initValue = LiveStore.searchRequest.value ?: "",
initValue = LiveStore.searchRequest.value ?: "", onDone = {
onDone = { navController.navigate(Screen.CinemaList.route)
LiveStore.searchRequest.value = it LiveStore.searchRequest.value = it
}, },
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.height(36.dp) .height(36.dp)
.background( .background(
color = MaterialTheme.colorScheme.onPrimary, color = MaterialTheme.colorScheme.onPrimary,
RoundedCornerShape(18.dp) RoundedCornerShape(18.dp)
) )
.padding(start = 13.dp, top = 8.dp) .padding(start = 13.dp, top = 8.dp)
) )
}
} }
} }
} }
@ -188,7 +187,7 @@ fun Navhost(
Screen.OrderView.route, Screen.OrderView.route,
arguments = listOf(navArgument("id") { type = NavType.IntType }) arguments = listOf(navArgument("id") { type = NavType.IntType })
) { backStackEntry -> ) { backStackEntry ->
backStackEntry.arguments?.let { OrderView(it.getInt("id")) } backStackEntry.arguments?.let { OrderView(navController) }
} }
composable(Screen.Report.route) { Report() } composable(Screen.Report.route) { Report() }
} }

View File

@ -56,7 +56,6 @@ fun CinemaList(
LaunchedEffect(searchPattern.value) { LaunchedEffect(searchPattern.value) {
viewModel.refresh() viewModel.refresh()
} }
Scaffold( Scaffold(
topBar = {}, topBar = {},
floatingActionButton = { floatingActionButton = {
@ -99,6 +98,7 @@ fun CinemaList(
} }
} }
@Composable @Composable
private fun CinemaList( private fun CinemaList(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

View File

@ -21,12 +21,16 @@ class CinemaListViewModel(
fun refresh() { fun refresh() {
val name = "%${LiveStore.searchRequest.value}%" val name = "%${LiveStore.searchRequest.value}%"
val pagingSource = cinemaRepository.getAllCinemas(name) runInScope(actionSuccess = {
cinemaPagingFlow = CinemaPagingFlowState(pagingSource.cachedIn(viewModelScope)) val pagingSource = cinemaRepository.getAllCinemas(name)
cinemaPagingFlow = CinemaPagingFlowState(pagingSource.cachedIn(viewModelScope))
})
} }
suspend fun deleteCinema(cinema: Cinema) { suspend fun deleteCinema(cinema: Cinema) {
cinemaRepository.deleteCinema(cinema) runInScope(actionSuccess = {
cinemaRepository.deleteCinema(cinema)
})
} }
} }

View File

@ -31,6 +31,9 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.example.myapplication.LiveStore import com.example.myapplication.LiveStore
import com.example.myapplication.api.ApiStatus
import com.example.myapplication.composeui.ErrorPlaceholder
import com.example.myapplication.composeui.LoadingPlaceholder
import com.example.myapplication.composeui.navigation.Screen import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.database.entities.model.Cinema import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.UserRole import com.example.myapplication.database.entities.model.UserRole
@ -42,104 +45,112 @@ fun CinemaView(
) { ) {
val cinemaUiState = viewModel.cinemaUiState val cinemaUiState = viewModel.cinemaUiState
val user = LiveStore.user.observeAsState() val user = LiveStore.user.observeAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.refreshState() viewModel.refreshState()
} }
when (viewModel.apiStatus) {
Column( ApiStatus.DONE -> {
modifier = Modifier Column(
.padding(16.dp)
.fillMaxSize(),
) {
val cinema: Cinema? = cinemaUiState.cinemaWithSessions?.cinema
if (cinema != null) {
Box(
modifier = Modifier modifier = Modifier
.background( .padding(16.dp)
color = MaterialTheme.colorScheme.secondary, .fillMaxSize(),
shape = RoundedCornerShape(16.dp)
)
) { ) {
Column( val cinema: Cinema? = cinemaUiState.cinemaWithSessions?.cinema
modifier = Modifier if (cinema != null) {
.fillMaxWidth() Box(
.padding(16.dp) modifier = Modifier
.background(color = MaterialTheme.colorScheme.secondary), .background(
verticalArrangement = Arrangement.spacedBy(8.dp) color = MaterialTheme.colorScheme.secondary,
) { shape = RoundedCornerShape(16.dp)
Row( )
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Text( Column(
text = "${cinema.name}, ${cinema.year}",
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onSecondary
),
modifier = Modifier
.padding(bottom = 8.dp)
)
}
if (cinema.image != null)
Image(
bitmap = BitmapFactory.decodeByteArray(
cinema.image,
0,
cinema.image.size
).asImageBitmap(),
contentDescription = null,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(200.dp) .padding(16.dp)
.padding(4.dp) .background(color = MaterialTheme.colorScheme.secondary),
) verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = "${cinema.name}, ${cinema.year}",
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onSecondary
),
modifier = Modifier
.padding(bottom = 8.dp)
)
}
Text( if (cinema.image != null)
text = cinema.description, Image(
color = MaterialTheme.colorScheme.onSecondary bitmap = BitmapFactory.decodeByteArray(
) cinema.image,
} 0,
} cinema.image.size
} ).asImageBitmap(),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(4.dp)
)
Row( Text(
verticalAlignment = Alignment.CenterVertically text = cinema.description,
) { color = MaterialTheme.colorScheme.onSecondary
Text(
text = "Сеансы",
style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onBackground
),
modifier = Modifier
.weight(1f) // Занимает доступное пространство
.padding(top = 8.dp, bottom = 8.dp)
)
if (user.value?.role == UserRole.ADMIN) {
IconButton(
onClick = {
val route = Screen.SessionEdit.route.replace("{id}", 0.toString())
.replace(
"{cinemaId}",
cinemaUiState.cinemaWithSessions?.cinema?.uid.toString()
) )
navController.navigate(route) }
} }
}
Row(
verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Text(
imageVector = Icons.Filled.Add, text = "Сеансы",
contentDescription = "Добавить сеанс", style = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onBackground
),
modifier = Modifier
.weight(1f) // Занимает доступное пространство
.padding(top = 8.dp, bottom = 8.dp)
) )
if (user.value?.role == UserRole.ADMIN) {
IconButton(
onClick = {
val route = Screen.SessionEdit.route.replace("{id}", 0.toString())
.replace(
"{cinemaId}",
cinemaUiState.cinemaWithSessions?.cinema?.uid.toString()
)
navController.navigate(route)
}
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "Добавить сеанс",
)
}
}
}
if (cinemaUiState.cinemaWithSessions != null) {
SessionList(viewModel, navController)
} }
} }
} }
if (cinemaUiState.cinemaWithSessions != null) {
SessionList(viewModel, navController) ApiStatus.LOADING -> LoadingPlaceholder()
} else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.popBackStack() }
)
} }
} }

View File

@ -4,13 +4,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import com.example.myapplication.composeui.MyViewModel
import com.example.myapplication.database.entities.model.CinemaWithSessions import com.example.myapplication.database.entities.model.CinemaWithSessions
import com.example.myapplication.database.entities.repository.CinemaRepository import com.example.myapplication.database.entities.repository.CinemaRepository
class CinemaViewModel( class CinemaViewModel(
savedStateHandle: SavedStateHandle, private val cinemaRepository: CinemaRepository savedStateHandle: SavedStateHandle, private val cinemaRepository: CinemaRepository
) : ViewModel() { ) : MyViewModel() {
private val cinemaUid: Int = checkNotNull(savedStateHandle["id"]) private val cinemaUid: Int = checkNotNull(savedStateHandle["id"])
var cinemaUiState by mutableStateOf(CinemaUiState()) var cinemaUiState by mutableStateOf(CinemaUiState())
@ -18,7 +18,11 @@ class CinemaViewModel(
suspend fun refreshState() { suspend fun refreshState() {
if (cinemaUid > 0) { if (cinemaUid > 0) {
cinemaUiState = CinemaUiState(cinemaRepository.getCinema(cinemaUid)) runInScope(actionSuccess = {
cinemaUiState = CinemaUiState(cinemaRepository.getCinema(cinemaUid))
}, actionError = {
cinemaUiState = CinemaUiState()
})
} }
} }
} }

View File

@ -1,7 +1,7 @@
package com.example.myapplication.database.entities.composeui package com.example.myapplication.database.entities.composeui
import androidx.lifecycle.ViewModel
import androidx.paging.PagingData import androidx.paging.PagingData
import com.example.myapplication.composeui.MyViewModel
import com.example.myapplication.database.entities.model.Order import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.repository.OrderRepository import com.example.myapplication.database.entities.repository.OrderRepository
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -9,10 +9,12 @@ import kotlinx.coroutines.flow.emptyFlow
class OrderListViewModel( class OrderListViewModel(
private val orderRepository: OrderRepository private val orderRepository: OrderRepository
) : ViewModel() { ) : MyViewModel() {
var orderListUiState: Flow<PagingData<Order>> = emptyFlow() var orderListUiState: Flow<PagingData<Order>> = emptyFlow()
fun refreshState(userId: Int = 0) { fun refreshState(userId: Int = 0) {
orderListUiState = orderRepository.getAllOrders(userId) runInScope(actionSuccess = {
orderListUiState = orderRepository.getAllOrders(userId)
})
} }
} }

View File

@ -1,6 +1,5 @@
package com.example.myapplication.database.entities.composeui package com.example.myapplication.database.entities.composeui
import android.content.res.Configuration
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -15,100 +14,98 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.myapplication.ui.theme.PmudemoTheme import androidx.navigation.NavController
import com.example.myapplication.api.ApiStatus
import com.example.myapplication.composeui.ErrorPlaceholder
import com.example.myapplication.composeui.LoadingPlaceholder
import org.threeten.bp.format.DateTimeFormatter import org.threeten.bp.format.DateTimeFormatter
@Composable @Composable
fun OrderView( fun OrderView(
id: Int, navController: NavController,
viewModel: OrderViewModel = viewModel(factory = AppViewModelProvider.Factory) viewModel: OrderViewModel = viewModel(factory = AppViewModelProvider.Factory)
) { ) {
val coroutineScope = rememberCoroutineScope() val orderUiState = viewModel.orderUiState
val orderUiState by viewModel.orderUiState.collectAsState()
LazyColumn(
modifier = Modifier
.padding(10.dp)
) {
items(orderUiState.sessionList) { session ->
val count = remember { mutableStateOf(session.count) }
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val formattedDate = dateFormatter.format(session.dateTime)
Text( LaunchedEffect(Unit) {
text = formattedDate, viewModel.refreshState()
color = MaterialTheme.colorScheme.onBackground, }
) when (viewModel.apiStatus) {
Box( ApiStatus.DONE -> {
LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxWidth()
.padding(10.dp) .padding(10.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.secondary)
) { ) {
Row( items(orderUiState.sessionList) { session ->
modifier = Modifier val count = remember { mutableIntStateOf(session.count) }
.fillMaxWidth() val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
.padding(8.dp), val formattedDate = dateFormatter.format(session.dateTime)
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
if (session.cinema.image != null)
Image(
bitmap = BitmapFactory.decodeByteArray(
session.cinema.image,
0,
session.cinema.image.size
).asImageBitmap(),
contentDescription = null,
modifier = Modifier
.size(90.dp)
.padding(4.dp)
)
Column( Text(
text = formattedDate,
color = MaterialTheme.colorScheme.onBackground,
)
Box(
modifier = Modifier modifier = Modifier
.weight(1f) .fillMaxWidth()
.padding(start = 8.dp), .padding(10.dp)
verticalArrangement = Arrangement.spacedBy(4.dp) .clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.secondary)
) { ) {
Text( Row(
text = "${session.cinema.name}, ${session.cinema.year}\n" + modifier = Modifier
"Цена: ${session.frozenPrice}\n" + .fillMaxWidth()
"Количество: ${count.value}", .padding(8.dp),
color = MaterialTheme.colorScheme.onSecondary verticalAlignment = Alignment.CenterVertically,
) horizontalArrangement = Arrangement.SpaceBetween
) {
if (session.cinema.image != null)
Image(
bitmap = BitmapFactory.decodeByteArray(
session.cinema.image,
0,
session.cinema.image.size
).asImageBitmap(),
contentDescription = null,
modifier = Modifier
.size(90.dp)
.padding(4.dp)
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "${session.cinema.name}, ${session.cinema.year}\n" +
"Цена: ${session.frozenPrice}\n" +
"Количество: $count",
color = MaterialTheme.colorScheme.onSecondary
)
}
}
} }
} }
} }
} }
}
}
ApiStatus.LOADING -> LoadingPlaceholder()
@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO) else -> ErrorPlaceholder(
@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) message = viewModel.apiError,
@Composable onBack = { navController.popBackStack() }
fun OrderViewPreview() { )
PmudemoTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
OrderView(id = 1)
}
} }
} }

View File

@ -1,31 +1,29 @@
package com.example.myapplication.database.entities.composeui package com.example.myapplication.database.entities.composeui
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import com.example.myapplication.composeui.MyViewModel
import androidx.lifecycle.viewModelScope
import com.example.myapplication.database.AppContainer
import com.example.myapplication.database.entities.model.SessionFromOrder import com.example.myapplication.database.entities.model.SessionFromOrder
import com.example.myapplication.database.entities.repository.OrderRepository import com.example.myapplication.database.entities.repository.OrderRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class OrderViewModel( class OrderViewModel(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val orderRepository: OrderRepository private val orderRepository: OrderRepository
) : ViewModel() { ) : MyViewModel() {
private val orderUid: Int = checkNotNull(savedStateHandle["id"]) private val orderUid: Int = checkNotNull(savedStateHandle["id"])
val orderUiState: StateFlow<OrderUiState> = var orderUiState by mutableStateOf(OrderUiState())
flow { emit(orderRepository.getOrder(orderUid)) }.map { private set
OrderUiState(it)
}.stateIn( suspend fun refreshState() {
scope = viewModelScope, runInScope(actionSuccess = {
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = AppContainer.TIMEOUT), orderUiState = OrderUiState(orderRepository.getOrder(orderUid))
initialValue = OrderUiState() }, actionError = {
) orderUiState = OrderUiState()
})
}
} }
data class OrderUiState(val sessionList: List<SessionFromOrder> = listOf()) data class OrderUiState(val sessionList: List<SessionFromOrder> = listOf())

View File

@ -1,7 +1,7 @@
package com.example.myapplication.database.entities.composeui package com.example.myapplication.database.entities.composeui
import androidx.lifecycle.ViewModel
import com.example.myapplication.LiveStore import com.example.myapplication.LiveStore
import com.example.myapplication.composeui.MyViewModel
import com.example.myapplication.database.entities.model.Session import com.example.myapplication.database.entities.model.Session
import com.example.myapplication.database.entities.model.SessionFromCinema import com.example.myapplication.database.entities.model.SessionFromCinema
import com.example.myapplication.database.entities.model.UserSessionCrossRef import com.example.myapplication.database.entities.model.UserSessionCrossRef
@ -11,25 +11,35 @@ import com.example.myapplication.database.entities.repository.UserSessionReposit
class SessionListViewModel( class SessionListViewModel(
private val sessionRepository: SessionRepository, private val sessionRepository: SessionRepository,
private val userSessionRepository: UserSessionRepository private val userSessionRepository: UserSessionRepository
) : ViewModel() { ) : MyViewModel() {
suspend fun deleteSession(session: SessionFromCinema) { suspend fun deleteSession(session: SessionFromCinema) {
sessionRepository.deleteSession( runInScope(actionSuccess = {
Session( sessionRepository.deleteSession(
uid = session.uid, Session(
dateTime = session.dateTime, uid = session.uid,
price = session.price, dateTime = session.dateTime,
maxCount = 0, price = session.price,
cinemaId = 0 maxCount = 0,
cinemaId = 0
)
) )
) })
} }
suspend fun addSessionInCart(sessionId: Int, count: Int = 1) { suspend fun addSessionInCart(sessionId: Int, count: Int = 1) {
try { val userId: Int = LiveStore.user.value?.uid ?: return
val userId: Int = LiveStore.user.value?.uid ?: return runInScope(actionSuccess = {
userSessionRepository.insertUserSession(UserSessionCrossRef(userId, sessionId, count)) try {
} catch (_: Exception) { userSessionRepository.insertUserSession(
UserSessionCrossRef(
userId,
sessionId,
count
)
)
} catch (_: Exception) {
} }
})
} }
} }

View File

@ -34,6 +34,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.example.myapplication.LiveStore import com.example.myapplication.LiveStore
import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.datastore.DataStoreManager import com.example.myapplication.datastore.DataStoreManager
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -47,11 +48,15 @@ fun UserProfile(
var isRegistration by remember { mutableStateOf(false) } var isRegistration by remember { mutableStateOf(false) }
val coroutine = rememberCoroutineScope() val coroutine = rememberCoroutineScope()
val errorStringId: Int? = viewModel.userUiState.errorId val errorStringId: Int? = viewModel.userUiState.errorId
val errorMessage = if (errorStringId == null) "" else stringResource(errorStringId) val errorMessage =
if (errorStringId == null || errorStringId == 0) "" else stringResource(errorStringId)
val user = LiveStore.user.observeAsState() val user = LiveStore.user.observeAsState()
LazyColumn { LazyColumn {
item { item {
if (errorStringId == 0) {
navController.navigate(Screen.CinemaList.route)
}
if (user.value != null) { if (user.value != null) {
Column( Column(
modifier = Modifier modifier = Modifier
@ -151,7 +156,7 @@ fun UserProfile(
if (isRegistration) { if (isRegistration) {
Button( Button(
onClick = { coroutine.launch { isRegistration = !viewModel.signUp() } }, onClick = { coroutine.launch { viewModel.signUp() } },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp) .padding(8.dp)
@ -169,13 +174,7 @@ fun UserProfile(
) )
} else { } else {
Button( Button(
onClick = { onClick = { coroutine.launch { viewModel.signIn(dataStoreManager) } },
coroutine.launch {
if (viewModel.signIn(dataStoreManager)) {
navController.popBackStack()
}
}
},
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp) .padding(8.dp)

View File

@ -1,17 +1,19 @@
package com.example.myapplication.database.entities.composeui package com.example.myapplication.database.entities.composeui
import android.widget.Toast
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import com.example.myapplication.MainComposeActivity
import com.example.myapplication.R import com.example.myapplication.R
import com.example.myapplication.composeui.MyViewModel
import com.example.myapplication.database.entities.model.User import com.example.myapplication.database.entities.model.User
import com.example.myapplication.database.entities.repository.UserRepository import com.example.myapplication.database.entities.repository.UserRepository
import com.example.myapplication.datastore.DataStoreManager import com.example.myapplication.datastore.DataStoreManager
class UserProfileViewModel( class UserProfileViewModel(
private val userRepository: UserRepository private val userRepository: UserRepository
) : ViewModel() { ) : MyViewModel() {
var userUiState by mutableStateOf(UserUiState()) var userUiState by mutableStateOf(UserUiState())
private set private set
@ -22,43 +24,62 @@ class UserProfileViewModel(
) )
} }
suspend fun signIn(dataStoreManager: DataStoreManager): Boolean { suspend fun signIn(dataStoreManager: DataStoreManager) {
userUiState.details.passwordConfirm = userUiState.details.password userUiState.details.passwordConfirm = userUiState.details.password
var errorId: Int? = validateInput(userUiState.details) var errorId: Int? = validateInput(userUiState.details)
if (errorId == null) { runInScope(
val overlap = userRepository.getUser(userUiState.details.login) actionSuccess = {
if (overlap == null || userUiState.details.password != overlap.password) { if (errorId == null) {
errorId = R.string.err_04 val overlap: User? = userRepository.getUser(userUiState.details.login)
} if (overlap == null || userUiState.details.password != overlap.password) {
} errorId = R.string.err_04
userUiState = UserUiState( } else {
details = userUiState.details, dataStoreManager.setLogin(userUiState.details.login)
errorId = errorId errorId = 0
) }
if (errorId == null) { }
dataStoreManager.setLogin(userUiState.details.login) userUiState = UserUiState(
return true details = userUiState.details,
} errorId = errorId
return false )
}, actionError = {
errorId = R.string.err_06
userUiState = UserUiState(
details = userUiState.details,
errorId = errorId
)
})
} }
suspend fun signUp(): Boolean { suspend fun signUp() {
var errorId: Int? = validateInput(userUiState.details) var errorId: Int? = validateInput(userUiState.details)
if (errorId == null) { runInScope(actionSuccess = {
val overlap = userRepository.getUser(userUiState.details.login) if (errorId == null) {
if (overlap != null) { val overlap = userRepository.getUser(userUiState.details.login)
errorId = R.string.err_03 if (overlap != null) {
errorId = R.string.err_03
}
} }
} userUiState = UserUiState(
userUiState = UserUiState( details = userUiState.details,
details = userUiState.details, errorId = errorId
errorId = errorId )
) if (errorId == null) {
if (errorId == null) { userRepository.insertUser(userUiState.details.toUser())
userRepository.insertUser(userUiState.details.toUser()) val toast = Toast.makeText(
return true MainComposeActivity.appContext,
} "Вы зарегистрированы",
return false Toast.LENGTH_SHORT
)
toast.show()
}
}, actionError = {
errorId = R.string.err_06
userUiState = UserUiState(
details = userUiState.details,
errorId = errorId
)
})
} }
private fun validateInput(details: UserDetails = userUiState.details): Int? { private fun validateInput(details: UserDetails = userUiState.details): Int? {

View File

@ -22,6 +22,9 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.example.myapplication.R import com.example.myapplication.R
import com.example.myapplication.api.ApiStatus
import com.example.myapplication.composeui.ErrorPlaceholder
import com.example.myapplication.composeui.LoadingPlaceholder
import com.example.myapplication.database.entities.composeui.AppViewModelProvider import com.example.myapplication.database.entities.composeui.AppViewModelProvider
import com.example.myapplication.ui.theme.PmudemoTheme import com.example.myapplication.ui.theme.PmudemoTheme
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -32,17 +35,25 @@ fun CinemaEdit(
viewModel: CinemaEditViewModel = viewModel(factory = AppViewModelProvider.Factory), viewModel: CinemaEditViewModel = viewModel(factory = AppViewModelProvider.Factory),
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
when (viewModel.apiStatus) {
CinemaEdit( ApiStatus.DONE -> {
cinemaUiState = viewModel.cinemaUiState, CinemaEdit(
onClick = { cinemaUiState = viewModel.cinemaUiState,
coroutineScope.launch { onClick = {
viewModel.saveCinema() coroutineScope.launch {
navController.popBackStack() viewModel.saveCinema()
} navController.popBackStack()
}, }
onUpdate = viewModel::updateUiState, },
) onUpdate = viewModel::updateUiState,
)
}
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.popBackStack() }
)
}
} }
@Composable @Composable

View File

@ -24,6 +24,9 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import com.example.myapplication.R import com.example.myapplication.R
import com.example.myapplication.api.ApiStatus
import com.example.myapplication.composeui.ErrorPlaceholder
import com.example.myapplication.composeui.LoadingPlaceholder
import com.example.myapplication.database.entities.composeui.AppViewModelProvider import com.example.myapplication.database.entities.composeui.AppViewModelProvider
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.threeten.bp.Instant import org.threeten.bp.Instant
@ -39,16 +42,26 @@ fun SessionEdit(
viewModel: SessionEditViewModel = viewModel(factory = AppViewModelProvider.Factory), viewModel: SessionEditViewModel = viewModel(factory = AppViewModelProvider.Factory),
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
SessionEdit( when (viewModel.apiStatus) {
sessionUiState = viewModel.sessionUiState, ApiStatus.DONE -> {
onClick = { SessionEdit(
coroutineScope.launch { sessionUiState = viewModel.sessionUiState,
viewModel.saveSession() onClick = {
navController.popBackStack() coroutineScope.launch {
} viewModel.saveSession()
}, navController.popBackStack()
onUpdate = viewModel::updateUiState }
) },
onUpdate = viewModel::updateUiState
)
}
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.popBackStack() }
)
}
} }
fun Long.toLocalDate(): org.threeten.bp.LocalDate { fun Long.toLocalDate(): org.threeten.bp.LocalDate {

View File

@ -25,6 +25,7 @@
<string name="err_03">Логин занят</string> <string name="err_03">Логин занят</string>
<string name="err_04">Неверный логин или пароль</string> <string name="err_04">Неверный логин или пароль</string>
<string name="err_05">Не совпадают пароли</string> <string name="err_05">Не совпадают пароли</string>
<string name="err_06">Ошибка сети</string>
<string name="back">Назад</string> <string name="back">Назад</string>
<string name="loading">Загрузка…</string> <string name="loading">Загрузка…</string>
</resources> </resources>

File diff suppressed because it is too large Load Diff