регистрация и вход

This commit is contained in:
dasha 2023-12-13 19:12:51 +04:00
parent 167d5ccc7d
commit 28fd92559c
33 changed files with 517 additions and 70393 deletions

View File

@ -50,6 +50,7 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
buildToolsVersion = "34.0.0"
}
dependencies {
@ -58,6 +59,9 @@ dependencies {
implementation("io.github.vanpra.compose-material-dialogs:datetime:0.8.1-rc")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
// LiveData
implementation("androidx.compose.runtime:runtime-livedata:1.5.4")
// Core
implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")

View File

@ -0,0 +1,11 @@
package com.example.myapplication
import androidx.lifecycle.MutableLiveData
import com.example.myapplication.database.entities.model.User
class LiveStore {
companion object {
val user = MutableLiveData<User?>(null)
val searchRequest = MutableLiveData("")
}
}

View File

@ -3,12 +3,17 @@ package com.example.myapplication
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.example.myapplication.composeui.Authenticator
import com.example.myapplication.composeui.navigation.MainNavbar
import com.example.myapplication.datastore.DataStoreManager
import com.example.myapplication.ui.theme.PmudemoTheme
@ -22,14 +27,15 @@ class MainComposeActivity : ComponentActivity() {
setContent {
PmudemoTheme(darkTheme = isDarkTheme.value) {
LaunchedEffect(key1 = true) {
dataStoreManager.getSettings().collect { setting ->
isDarkTheme.value = setting.isDarkTheme
dataStoreManager.getDarkTheme().collect { setting ->
isDarkTheme.value = setting == "Dark"
}
}
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Authenticator(dataStoreManager)
MainNavbar(
isDarkTheme = isDarkTheme,
dataStoreManager = dataStoreManager

View File

@ -87,12 +87,22 @@ interface MyServerService {
@Path("id") id: Int,
): UserRemote
@GET("users?_limit=1")
suspend fun getUser(
@Query("login") login: String,
): List<UserRemote>
@PUT("users/{id}")
suspend fun updateUserCart(
@Path("id") id: Int,
@Body userRemote: UserRemote,
): UserRemote
@POST("users")
suspend fun createUser(
@Body user: UserRemote,
): UserRemote
@GET("orders")
suspend fun getOrders(
@Query("_page") page: Int,
@ -116,7 +126,7 @@ interface MyServerService {
): OrderRemote
companion object {
//private const val BASE_URL = "http://192.168.154.166:8079/"
//private const val BASE_URL = "http://192.168.154.166:8080/"
private const val BASE_URL = "http://192.168.0.101:8079/"
@Volatile

View File

@ -2,17 +2,23 @@ package com.example.myapplication.api.order
import com.example.myapplication.api.session.SessionFromOrderRemote
import com.example.myapplication.database.entities.model.Order
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import org.threeten.bp.LocalDateTime
@Serializable
data class OrderRemote(
val id: Int = 0, val userId: Int = 0, var sessions: List<SessionFromOrderRemote> = emptyList()
val id: Int = 0,
val userId: Int = 0,
@Contextual
val dateTime: LocalDateTime = LocalDateTime.now(),
var sessions: List<SessionFromOrderRemote> = emptyList()
)
fun OrderRemote.toOrder(): Order = Order(
id, userId
id, userId, dateTime
)
fun Order.toOrderRemote(): OrderRemote = OrderRemote(
uid, userId!!, sessions = emptyList()
uid, userId!!, dateTime, sessions = emptyList()
)

View File

@ -2,6 +2,8 @@ package com.example.myapplication.api.user
import android.util.Log
import com.example.myapplication.api.MyServerService
import com.example.myapplication.api.cinema.toCinema
import com.example.myapplication.api.cinema.toCinemaRemote
import com.example.myapplication.api.session.toSessionFromCart
import com.example.myapplication.database.entities.model.SessionFromCart
import com.example.myapplication.database.entities.model.User
@ -21,6 +23,10 @@ class RestUserRepository(
return dbUserRepository.getAllUsers()
}
override suspend fun getUser(login: String): User? {
return service.getUser(login).firstOrNull()?.toUser()
}
override suspend fun getCartByUser(userId: Int): List<SessionFromCart> {
val cart = service.getUserCart(userId)
dbUserSessionRepository.deleteUserSessions(userId)
@ -47,6 +53,7 @@ class RestUserRepository(
}
override suspend fun insertUser(user: User) {
service.createUser(user.toUserRemote()).toUser()
}
override suspend fun updateUser(user: User) {

View File

@ -1,6 +1,10 @@
package com.example.myapplication.api.user
import com.example.myapplication.api.session.SessionFromCartRemote
import com.example.myapplication.api.session.SessionRemote
import com.example.myapplication.database.entities.model.Session
import com.example.myapplication.database.entities.model.User
import com.example.myapplication.database.entities.model.UserRole
import kotlinx.serialization.Serializable
@Serializable
@ -8,5 +12,20 @@ data class UserRemote(
val id: Int = 0,
val login: String = "",
val password: String = "",
val role: Int = -1,
var sessions: List<SessionFromCartRemote> = emptyList()
)
fun User.toUserRemote(): UserRemote = UserRemote(
uid,
login,
password,
role = role.ordinal,
)
fun UserRemote.toUser(): User = User(
id,
login,
password,
role = enumValues<UserRole>()[role],
)

View File

@ -0,0 +1,39 @@
package com.example.myapplication.composeui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.myapplication.LiveStore
import com.example.myapplication.database.entities.composeui.AppViewModelProvider
import com.example.myapplication.datastore.DataStoreManager
import kotlinx.coroutines.launch
@Composable
fun Authenticator(
dataStoreManager: DataStoreManager,
viewModel: AuthenticatorViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val login = dataStoreManager.getLogin().collectAsState(initial = "").value
fun synchronize() {
scope.launch {
if (login == "") {
LiveStore.user.value = null
return@launch
}
val overlap = viewModel.findUserByLogin(login)
if (overlap == null) {
dataStoreManager.setLogin("")
return@launch
}
LiveStore.user.value = overlap
}
}
synchronize()
}

View File

@ -0,0 +1,13 @@
package com.example.myapplication.composeui
import androidx.lifecycle.ViewModel
import com.example.myapplication.database.entities.model.User
import com.example.myapplication.database.entities.repository.UserRepository
class AuthenticatorViewModel(
private val userRepository: UserRepository
) : ViewModel() {
suspend fun findUserByLogin(login: String): User? {
return userRepository.getUser(login)
}
}

View File

@ -36,6 +36,8 @@ import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@ -51,14 +53,13 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.myapplication.LiveStore
import com.example.myapplication.R
import com.example.myapplication.database.entities.composeui.AppViewModelProvider
import com.example.myapplication.database.entities.composeui.CartUiState
import com.example.myapplication.database.entities.composeui.CartViewModel
import com.example.myapplication.database.entities.model.Session
import com.example.myapplication.database.entities.model.SessionFromCart
import com.example.myapplication.database.entities.model.UserRole
import com.example.myapplication.ui.theme.PmudemoTheme
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.threeten.bp.format.DateTimeFormatter
@ -77,7 +78,7 @@ fun Cart(
cartUiState = cartUiState,
modifier = Modifier
.padding(all = 10.dp),
onSwipe = { session: SessionFromCart, user: Int ->
onSwipe = { session: SessionFromCart ->
coroutineScope.launch {
viewModel.removeFromCart(
session = Session(
@ -86,11 +87,11 @@ fun Cart(
price = session.price,
maxCount = 0,
cinemaId = session.cinemaId
), user = user
)
)
}
},
onChangeCount = { session: SessionFromCart, user: Int, count: Int ->
onChangeCount = { session: SessionFromCart, count: Int ->
coroutineScope.launch {
viewModel.updateFromCart(
session = Session(
@ -99,13 +100,13 @@ fun Cart(
price = session.price,
maxCount = 0,
cinemaId = session.cinemaId
), userId = user, count = count, availableCount = session.availableCount
), count = count, availableCount = session.availableCount
)
}
},
onAddToOrder = { sessions: List<SessionFromCart>, user: Int ->
onAddToOrder = { sessions: List<SessionFromCart> ->
coroutineScope.launch {
viewModel.addToOrder(sessions = sessions, userId = user)
viewModel.addToOrder(sessions = sessions)
}
}
)
@ -116,9 +117,9 @@ fun Cart(
private fun Cart(
cartUiState: CartUiState,
modifier: Modifier,
onSwipe: (SessionFromCart, Int) -> Unit,
onChangeCount: (SessionFromCart, Int, Int) -> Unit,
onAddToOrder: (List<SessionFromCart>, Int) -> Unit
onSwipe: (SessionFromCart) -> Unit,
onChangeCount: (SessionFromCart, Int) -> Unit,
onAddToOrder: (List<SessionFromCart>) -> Unit
) {
LazyColumn(
modifier = modifier
@ -129,68 +130,8 @@ private fun Cart(
)
if (dismissState.isDismissed(direction = DismissDirection.EndToStart)) {
onSwipe(session, 1)
onSwipe(session)
}
SwipeToDelete(
dismissState = dismissState,
session = session,
onChangeCount = onChangeCount
)
}
}
Column {
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = { onAddToOrder(cartUiState.sessionList, 1) },
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) { Text("Купить") }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SwipeToDelete(
dismissState: DismissState,
session: SessionFromCart,
onChangeCount: (SessionFromCart, Int, Int) -> Unit,
) {
SwipeToDismiss(
state = dismissState,
directions = setOf(
DismissDirection.EndToStart
),
background = {
val backgroundColor by animateColorAsState(
when (dismissState.targetValue) {
DismissValue.DismissedToStart -> Color.Red.copy(alpha = 0.8f)
else -> MaterialTheme.colorScheme.background
},
label = ""
)
val iconScale by animateFloatAsState(
targetValue = if (dismissState.targetValue == DismissValue.DismissedToStart) 1.3f else 0.5f,
label = ""
)
Box(
Modifier
.fillMaxSize()
.background(color = backgroundColor)
.padding(end = 16.dp),
contentAlignment = Alignment.CenterEnd
) {
Icon(
modifier = Modifier.scale(iconScale),
imageVector = Icons.Outlined.Delete,
contentDescription = "Delete",
tint = Color.White
)
}
},
dismissContent = {
SessionListItem(
session = session,
modifier = Modifier
@ -201,16 +142,29 @@ private fun SwipeToDelete(
onChangeCount = onChangeCount
)
}
)
}
val user = LiveStore.user.observeAsState()
if (user.value?.role == UserRole.USER) {
Column {
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = { onAddToOrder(cartUiState.sessionList) },
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) { Text("Купить") }
}
}
}
@Composable
private fun SessionListItem(
session: SessionFromCart,
modifier: Modifier = Modifier,
onChangeCount: (SessionFromCart, Int, Int) -> Unit,
onChangeCount: (SessionFromCart, Int) -> Unit,
) {
var currentCount by remember { mutableStateOf(session.count) }
var currentCount by remember { mutableIntStateOf(session.count) }
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val formattedDate = dateFormatter.format(session.dateTime)
@ -265,7 +219,7 @@ private fun SessionListItem(
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = { onChangeCount(session, 1, --currentCount) }
onClick = { onChangeCount(session, --currentCount) }
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.minus),
@ -284,7 +238,6 @@ private fun SessionListItem(
onClick = {
onChangeCount(
session,
1,
if (currentCount != session.availableCount) ++currentCount else currentCount
)
}

View File

@ -1,9 +1,10 @@
package com.example.myapplication.database.entities.composeui
package com.example.myapplication.composeui
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.example.myapplication.LiveStore
import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.model.OrderSessionCrossRef
import com.example.myapplication.database.entities.model.Session
@ -13,7 +14,7 @@ import com.example.myapplication.database.entities.repository.OrderRepository
import com.example.myapplication.database.entities.repository.OrderSessionRepository
import com.example.myapplication.database.entities.repository.UserRepository
import com.example.myapplication.database.entities.repository.UserSessionRepository
import kotlinx.coroutines.delay
import org.threeten.bp.LocalDateTime
class CartViewModel(
private val userSessionRepository: UserSessionRepository,
@ -21,19 +22,20 @@ class CartViewModel(
private val orderSessionRepository: OrderSessionRepository,
private val userRepository: UserRepository,
) : ViewModel() {
private val userUid: Int = 1
var cartUiState by mutableStateOf(CartUiState())
private set
suspend fun refreshState() {
val cart = userRepository.getCartByUser(userUid)
val userId: Int = LiveStore.user.value?.uid ?: return
val cart = userRepository.getCartByUser(userId)
cartUiState = CartUiState(cart)
}
suspend fun addToOrder(userId: Int, sessions: List<SessionFromCart>) {
suspend fun addToOrder(sessions: List<SessionFromCart>) {
val userId: Int = LiveStore.user.value?.uid ?: return
if (sessions.isEmpty())
return
val orderId = orderRepository.insertOrder(Order(0, userId))
val orderId = orderRepository.insertOrder(Order(0, userId, LocalDateTime.now()))
sessions.forEach { session ->
orderSessionRepository.insertOrderSession(
OrderSessionCrossRef(
@ -48,15 +50,17 @@ class CartViewModel(
refreshState()
}
suspend fun removeFromCart(user: Int, session: Session, count: Int = 1) {
userSessionRepository.deleteUserSession(UserSessionCrossRef(user, session.uid, count))
suspend fun removeFromCart(session: Session, count: Int = 0) {
val userId: Int = LiveStore.user.value?.uid ?: return
userSessionRepository.deleteUserSession(UserSessionCrossRef(userId, session.uid, count))
refreshState()
}
suspend fun updateFromCart(userId: Int, session: Session, count: Int, availableCount: Int)
suspend fun updateFromCart(session: Session, count: Int, availableCount: Int)
: Boolean {
val userId: Int = LiveStore.user.value?.uid ?: return false
if (count == 0) {
removeFromCart(userId, session, count)
removeFromCart(session, count)
return false
}
if (count > availableCount)

View File

@ -176,9 +176,9 @@ fun Navhost(
modifier.padding(innerPadding)
) {
composable(Screen.CinemaList.route) { CinemaList(navController) }
composable(Screen.OrderList.route) { OrderList(navController, 1) }
composable(Screen.OrderList.route) { OrderList(navController) }
composable(Screen.Cart.route) { Cart() }
composable(Screen.UserProfile.route) { UserProfile(isDarkTheme, dataStore) }
composable(Screen.UserProfile.route) { UserProfile(isDarkTheme, dataStore, navController) }
composable(
Screen.CinemaEdit.route,
arguments = listOf(navArgument("id") { type = NavType.IntType })

View File

@ -6,6 +6,9 @@ import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.example.myapplication.CinemaApplication
import com.example.myapplication.composeui.Authenticator
import com.example.myapplication.composeui.AuthenticatorViewModel
import com.example.myapplication.composeui.CartViewModel
import com.example.myapplication.database.entities.composeui.edit.CinemaEditViewModel
import com.example.myapplication.database.entities.composeui.edit.SessionEditViewModel
@ -57,6 +60,14 @@ object AppViewModelProvider {
cinemaApplication().container.orderRestRepository,
)
}
initializer {
UserProfileViewModel(
cinemaApplication().container.userRestRepository,
)
}
initializer {
AuthenticatorViewModel(cinemaApplication().container.userRestRepository)
}
}
}

View File

@ -1,6 +1,7 @@
package com.example.myapplication.database.entities.composeui
import android.graphics.BitmapFactory
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -27,6 +28,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -36,8 +38,10 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import com.example.myapplication.LiveStore
import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.UserRole
import kotlinx.coroutines.launch
@Composable
@ -47,22 +51,25 @@ fun CinemaList(
) {
val coroutineScope = rememberCoroutineScope()
val cinemaPagingItems = viewModel.cinemaListUiState.collectAsLazyPagingItems()
val user = LiveStore.user.observeAsState()
Scaffold(
topBar = {},
floatingActionButton = {
FloatingActionButton(
onClick = {
val route = Screen.CinemaEdit.route.replace("{id}", 0.toString())
navController.navigate(route)
},
containerColor = MaterialTheme.colorScheme.primary,
) {
Icon(
Icons.Filled.Add,
"Добавить",
tint = MaterialTheme.colorScheme.onPrimary
)
if (user.value?.role == UserRole.ADMIN) {
FloatingActionButton(
onClick = {
val route = Screen.CinemaEdit.route.replace("{id}", 0.toString())
navController.navigate(route)
},
containerColor = MaterialTheme.colorScheme.primary,
) {
Icon(
Icons.Filled.Add,
"Добавить",
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
}
) { innerPadding ->
@ -132,6 +139,7 @@ private fun CinemaListItem(
onDeleteClick: (cinema: Cinema) -> Unit,
onEditClick: (cinema: Int) -> Unit
) {
val user = LiveStore.user.observeAsState()
Box(
modifier = modifier
) {
@ -160,27 +168,29 @@ private fun CinemaListItem(
color = MaterialTheme.colorScheme.onSecondary
)
// Добавляем пустое пространство для разделения текста и кнопки
Spacer(modifier = Modifier.weight(1f))
IconButton(
onClick = { onEditClick(cinema.uid) },
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = "Редактировать",
tint = MaterialTheme.colorScheme.onSecondary,
)
}
IconButton(
onClick = { onDeleteClick(cinema) },
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Удалить",
tint = MaterialTheme.colorScheme.onSecondary,
)
if (user.value?.role == UserRole.ADMIN) {
// Добавляем пустое пространство для разделения текста и кнопки
Spacer(modifier = Modifier.weight(1f))
IconButton(
onClick = { onEditClick(cinema.uid) },
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = "Редактировать",
tint = MaterialTheme.colorScheme.onSecondary,
)
}
IconButton(
onClick = { onDeleteClick(cinema) },
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Удалить",
tint = MaterialTheme.colorScheme.onSecondary,
)
}
}
}
}

View File

@ -20,6 +20,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
@ -29,8 +30,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.example.myapplication.LiveStore
import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.UserRole
@Composable
fun CinemaView(
@ -38,6 +41,7 @@ fun CinemaView(
viewModel: CinemaViewModel = viewModel(factory = AppViewModelProvider.Factory),
) {
val cinemaUiState = viewModel.cinemaUiState
val user = LiveStore.user.observeAsState()
LaunchedEffect(Unit) {
viewModel.refreshState()
@ -116,21 +120,22 @@ fun CinemaView(
.weight(1f) // Занимает доступное пространство
.padding(top = 8.dp, bottom = 8.dp)
)
IconButton(
onClick = {
val route = Screen.SessionEdit.route.replace("{id}", 0.toString())
.replace(
"{cinemaId}",
cinemaUiState.cinemaWithSessions?.cinema?.uid.toString()
)
navController.navigate(route)
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 = "Добавить сеанс",
)
}
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "Добавить сеанс",
)
}
}
if (cinemaUiState.cinemaWithSessions != null) {

View File

@ -21,25 +21,6 @@ class CinemaViewModel(
cinemaUiState = CinemaUiState(cinemaRepository.getCinema(cinemaUid))
}
}
// init {
// viewModelScope.launch {
// if (cinemaUid > 0) {
// cinemaUiState = CinemaUiState(cinemaRepository.getCinema(cinemaUid))
// }
// }
// }
// val cinemaUiState: mutableStateOf(CinemaUiState()) = cinemaRepository.getCinema(
// cinemaUid
// ).map
// {
// CinemaUiState(it)
// }.stateIn(
// scope = viewModelScope,
// started = SharingStarted.WhileSubscribed(stopTimeoutMillis = AppDataContainer.TIMEOUT),
// initialValue = CinemaUiState()
// )
}
data class CinemaUiState(val cinemaWithSessions: CinemaWithSessions? = null)

View File

@ -30,14 +30,13 @@ import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.ui.theme.PmudemoTheme
import org.threeten.bp.format.DateTimeFormatter
@Composable
fun OrderList(
navController: NavController?,
userId: Int?,
viewModel: OrderListViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val coroutineScope = rememberCoroutineScope()
val ordersUiState = viewModel.orderListUiState.collectAsLazyPagingItems()
LazyColumn(
modifier = Modifier
@ -48,7 +47,9 @@ fun OrderList(
key = ordersUiState.itemKey(),
contentType = ordersUiState.itemContentType()) { index ->
val order = ordersUiState[index]
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val orderId = Screen.OrderView.route.replace("{id}", order!!.uid.toString())
val formattedDate = dateFormatter.format(order.dateTime)
Box(
modifier = Modifier
.fillMaxWidth()
@ -66,7 +67,7 @@ fun OrderList(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("Заказ №${order.uid}", color = MaterialTheme.colorScheme.onSecondary)
Text("Заказ №${order.uid}, ${formattedDate}", color = MaterialTheme.colorScheme.onSecondary)
}
}
}
@ -81,7 +82,7 @@ fun OrderListPreview() {
Surface(
color = MaterialTheme.colorScheme.background
) {
OrderList(navController = null, 1)
OrderList(navController = null)
}
}
}

View File

@ -6,6 +6,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import com.example.myapplication.LiveStore
import com.example.myapplication.database.AppDataContainer
import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.repository.OrderRepository
@ -19,7 +20,7 @@ import kotlinx.coroutines.launch
class OrderListViewModel(
private val orderRepository: OrderRepository
) : ViewModel() {
val orderListUiState: Flow<PagingData<Order>> = orderRepository.getAllOrders(1)
val orderListUiState: Flow<PagingData<Order>> = orderRepository.getAllOrders(LiveStore.user.value?.uid ?: 0)
}
data class OrderListUiState(val orderList: List<Order> = listOf())

View File

@ -22,6 +22,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -31,8 +32,10 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.example.myapplication.LiveStore
import com.example.myapplication.R
import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.database.entities.model.UserRole
import kotlinx.coroutines.launch
import org.threeten.bp.format.DateTimeFormatter
@ -44,6 +47,7 @@ fun SessionList(
) {
val coroutineScope = rememberCoroutineScope()
val cinemaWithSessions = cinemaWithSessionsViewModel.cinemaUiState.cinemaWithSessions!!
val user = LiveStore.user.observeAsState()
LazyColumn {
if (cinemaWithSessions.sessions.isEmpty()) {
@ -104,36 +108,39 @@ fun SessionList(
color = MaterialTheme.colorScheme.onSecondary
)
}
IconButton(
onClick = {
coroutineScope.launch {
if (session.availableCount != 0)
viewModel.addSessionInCart(sessionId = session.uid)
}
},
) {
Icon(
imageVector = Icons.Filled.ShoppingCart,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSecondary
)
if (user.value?.role == UserRole.USER) {
IconButton(
onClick = {
coroutineScope.launch {
if (session.availableCount != 0)
viewModel.addSessionInCart(sessionId = session.uid)
}
},
) {
Icon(
imageVector = Icons.Filled.ShoppingCart,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSecondary
)
}
}
IconButton(
onClick = {
coroutineScope.launch {
viewModel.deleteSession(session = session)
cinemaWithSessionsViewModel.refreshState()
}
},
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSecondary
)
if (user.value?.role == UserRole.ADMIN) {
IconButton(
onClick = {
coroutineScope.launch {
viewModel.deleteSession(session = session)
cinemaWithSessionsViewModel.refreshState()
}
},
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSecondary
)
}
}
}
}

View File

@ -1,6 +1,7 @@
package com.example.myapplication.database.entities.composeui
import androidx.lifecycle.ViewModel
import com.example.myapplication.LiveStore
import com.example.myapplication.database.entities.model.Session
import com.example.myapplication.database.entities.model.SessionFromCinema
import com.example.myapplication.database.entities.model.UserSessionCrossRef
@ -25,7 +26,8 @@ class SessionListViewModel(
suspend fun addSessionInCart(sessionId: Int, count: Int = 1) {
try {
userSessionRepository.insertUserSession(UserSessionCrossRef(1, sessionId, count))
val userId: Int = LiveStore.user.value?.uid ?: return
userSessionRepository.insertUserSession(UserSessionCrossRef(userId, sessionId, count))
} catch (_: Exception) {
}

View File

@ -20,29 +20,60 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
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.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.example.myapplication.LiveStore
import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.datastore.DataStoreManager
import com.example.myapplication.datastore.SettingData
import kotlinx.coroutines.launch
@Composable
fun UserProfile(
isDarkTheme: MutableState<Boolean>,
dataStoreManager: DataStoreManager
dataStoreManager: DataStoreManager,
navController: NavController,
viewModel: UserProfileViewModel = viewModel(factory = AppViewModelProvider.Factory),
) {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var isRegistration by remember { mutableStateOf(false) }
val coroutineScope = rememberCoroutineScope()
val coroutine = rememberCoroutineScope()
val errorStringId: Int? = viewModel.userUiState.errorId
val errorMessage = if (errorStringId == null) "" else stringResource(errorStringId)
val user = LiveStore.user.observeAsState()
LazyColumn {
item {
Text(
text = "Текущий пользователь: " + (LiveStore.user.value?.login ?: ""),
)
Button(
enabled = user.value != null,
onClick = {
coroutineScope.launch {
dataStoreManager.setLogin("")
}
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Text("Выход")
}
Text(
text = errorMessage,
color = Color.Red
)
Column(
modifier = Modifier
.fillMaxSize()
@ -54,8 +85,10 @@ fun UserProfile(
modifier = Modifier.align(Alignment.CenterHorizontally)
)
BasicTextField(
value = username,
onValueChange = { newValue -> username = newValue },
value = viewModel.userUiState.details.login,
onValueChange = {
viewModel.updateUiState(viewModel.userUiState.details.copy(login = it))
},
modifier = Modifier
.fillMaxWidth()
.size(36.dp)
@ -68,8 +101,10 @@ fun UserProfile(
modifier = Modifier.align(Alignment.CenterHorizontally)
)
BasicTextField(
value = password,
onValueChange = { newValue -> password = newValue },
value = viewModel.userUiState.details.password,
onValueChange = {
viewModel.updateUiState(viewModel.userUiState.details.copy(password = it))
},
modifier = Modifier
.fillMaxWidth()
.size(36.dp)
@ -80,7 +115,12 @@ fun UserProfile(
if (isRegistration) {
Button(
onClick = { },
onClick = {
coroutineScope.launch {
val flag = viewModel.signUp()
isRegistration = !flag
}
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
@ -98,7 +138,13 @@ fun UserProfile(
)
} else {
Button(
onClick = { },
onClick = {
coroutineScope.launch {
if (viewModel.signIn(dataStoreManager)) {
navController.navigate(Screen.CinemaList.route)
}
}
},
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
@ -133,14 +179,16 @@ fun UserProfile(
.padding(5.dp)
)
val coroutine = rememberCoroutineScope()
Switch(
checked = isDarkTheme.value,
onCheckedChange = {
isDarkTheme.value = !isDarkTheme.value
coroutine.launch {
dataStoreManager.saveSettings(SettingData(isDarkTheme = isDarkTheme.value))
if (isDarkTheme.value) {
dataStoreManager.setDarkTheme("Dark")
} else {
dataStoreManager.setDarkTheme("Light")
}
}
},
colors = switchColors
@ -150,16 +198,3 @@ fun UserProfile(
}
}
}
/*@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 UserProfilePreview() {
PmudemoTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
UserProfile(navController = null, isDarkTheme = remember { mutableStateOf(true) })
}
}
}*/

View File

@ -0,0 +1,90 @@
package com.example.myapplication.database.entities.composeui
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.example.myapplication.R
import com.example.myapplication.database.entities.model.User
import com.example.myapplication.database.entities.repository.UserRepository
import com.example.myapplication.datastore.DataStoreManager
class UserProfileViewModel(
private val userRepository: UserRepository
) : ViewModel() {
var userUiState by mutableStateOf(UserUiState())
private set
fun updateUiState(userDetails: UserDetails) {
userUiState = UserUiState(
details = userDetails,
errorId = null
)
}
suspend fun signIn(dataStoreManager: DataStoreManager): Boolean {
var errorId: Int? = validateInput(userUiState.details)
if (errorId == null) {
val overlap = userRepository.getUser(userUiState.details.login)
if (overlap == null || userUiState.details.password != overlap.password) {
errorId = R.string.err_04
}
}
userUiState = UserUiState(
details = userUiState.details,
errorId = errorId
)
if (errorId == null) {
dataStoreManager.setLogin(userUiState.details.login)
return true
}
return false
}
suspend fun signUp(): Boolean {
var errorId: Int? = validateInput(userUiState.details)
if (errorId == null) {
val overlap = userRepository.getUser(userUiState.details.login)
if (overlap != null) {
errorId = R.string.err_03
}
}
userUiState = UserUiState(
details = userUiState.details,
errorId = errorId
)
if (errorId == null) {
userRepository.insertUser(userUiState.details.toUser())
return true
}
return false
}
private fun validateInput(details: UserDetails = userUiState.details): Int? {
return if (details.login.isBlank()) {
R.string.err_01
} else if (details.password.isBlank()) {
R.string.err_02
} else {
null
}
}
}
data class UserDetails(
val login: String = "",
val password: String = ""
)
data class UserUiState(
val details: UserDetails = UserDetails(),
val errorId: Int? = null
)
fun UserDetails.toUser(uid: Int = 0): User = User(
uid = uid,
login = login,
password = password,
)

View File

@ -14,6 +14,9 @@ interface UserDao {
@Query("select * from users order by login collate nocase asc")
fun getAll(): Flow<List<User>>
@Query("select * from users where users.login=:login LIMIT 1")
suspend fun getByLogin(login: String): User?
@Query(
"SELECT sessions.*, sessions.max_count-IFNULL(SUM(orders_sessions.count), 0) as available_count, " +
"users_sessions.count FROM sessions " +

View File

@ -4,23 +4,18 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import org.threeten.bp.LocalDateTime
@Entity(
tableName = "orders", foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["uid"],
childColumns = ["user_id"],
onDelete = ForeignKey.RESTRICT,
onUpdate = ForeignKey.RESTRICT
)
]
tableName = "orders"
)
data class Order(
@PrimaryKey(autoGenerate = true)
val uid: Int,
@ColumnInfo(name = "user_id", index = true)
val userId: Int?,
@ColumnInfo(name = "date_time")
val dateTime: LocalDateTime,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true

View File

@ -8,7 +8,8 @@ data class User(
@PrimaryKey(autoGenerate = true)
val uid: Int = 0,
val login: String,
val password: String
val password: String,
val role: UserRole = UserRole.USER
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true

View File

@ -0,0 +1,6 @@
package com.example.myapplication.database.entities.model
enum class UserRole(val value: Int) {
USER(0),
ADMIN(1)
}

View File

@ -8,6 +8,8 @@ import kotlinx.coroutines.flow.Flow
class OfflineUserRepository(private val userDao: UserDao) : UserRepository {
override fun getAllUsers(): Flow<List<User>> = userDao.getAll()
override suspend fun getUser(login: String): User? = userDao.getByLogin(login)
override suspend fun getCartByUser(userId: Int): List<SessionFromCart> =
userDao.getCartByUid(userId)

View File

@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.Flow
interface UserRepository {
fun getAllUsers(): Flow<List<User>>
suspend fun getUser(login: String): User?
suspend fun getCartByUser(userId: Int): List<SessionFromCart>
suspend fun insertUser(user: User)
suspend fun updateUser(user: User)

View File

@ -5,21 +5,46 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore("data_store")
class DataStoreManager(private val context: Context) {
suspend fun saveSettings(settingData: SettingData) {
context.dataStore.edit { pref ->
pref[booleanPreferencesKey("isDarkTheme")] = settingData.isDarkTheme
companion object {
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore("Store")
val DARK_THEME = stringPreferencesKey("dark_theme")
val LOGIN = stringPreferencesKey("login")
}
fun getDarkTheme(): Flow<String> {
return context.dataStore.data
.map { preferences ->
preferences[DARK_THEME] ?: "Dark"
}
}
fun getLogin(): Flow<String> {
return context.dataStore.data
.map { preferences ->
preferences[LOGIN] ?: ""
}
}
private suspend fun saveStringValue(key: Preferences.Key<String>, value: String) {
context.dataStore.edit { preferences ->
preferences[key] = value
}
}
fun getSettings() = context.dataStore.data.map { pref ->
return@map SettingData(
pref[booleanPreferencesKey("isDarkTheme")] ?: true
)
suspend fun setDarkTheme(darkTheme: String) {
saveStringValue(DARK_THEME, darkTheme)
}
suspend fun setLogin(login: String) {
saveStringValue(LOGIN, login)
}
}

View File

@ -1,5 +0,0 @@
package com.example.myapplication.datastore
data class SettingData(
val isDarkTheme: Boolean
)

View File

@ -19,4 +19,8 @@
<string name="session_cinema_not_select">Фильм не указан</string>
<string name="size">Размер загруженного изображения: %1$dx%2$d</string>
<string name="not_uploaded">Загрузите изображение</string>
<string name="err_01">Введите логин</string>
<string name="err_02">Введите пароль</string>
<string name="err_03">Логин занят</string>
<string name="err_04">Неверный логин или пароль</string>
</resources>

File diff suppressed because it is too large Load Diff

View File

@ -2,12 +2,19 @@
"users": [
{
"id": 1,
"login": "login",
"password": "password",
"login": "admin",
"password": "admin",
"role": 1
},
{
"id": 2,
"login": "qwe",
"password": "qwe",
"role": 0,
"sessions": [
{
"id": 5,
"count": 4,
"id": 6,
"count": 7,
"cinemaId": 2
}
]
@ -16,7 +23,8 @@
"orders": [
{
"id": 2,
"userId": 1,
"userId": 2,
"dateTime": "19.12.2022 02:15",
"sessions": [
{
"id": 1,
@ -29,15 +37,9 @@
},
{
"id": 3,
"userId": 1,
"userId": 2,
"dateTime": "13.12.2023 19:03",
"sessions": [
{
"id": 1,
"dateTime": "20.12.2022 02:15",
"frozenPrice": 150,
"count": 3,
"cinemaId": 3
},
{
"id": 5,
"dateTime": "04.04.2004 00:00",
@ -46,50 +48,6 @@
"cinemaId": 2
}
]
},
{
"id": 4,
"userId": 1,
"sessions": [
{
"id": 1,
"dateTime": "20.12.2022 02:15",
"frozenPrice": 150,
"count": 1,
"cinemaId": 3
},
{
"id": 5,
"dateTime": "04.04.2004 00:00",
"frozenPrice": 1234,
"count": 1,
"cinemaId": 2
}
]
},
{
"id": 5,
"userId": 1
},
{
"id": 6,
"userId": 1,
"sessions": [
{
"id": 5,
"dateTime": "04.04.2004 00:00",
"frozenPrice": 1234,
"count": 5,
"cinemaId": 2
},
{
"id": 2,
"dateTime": "22.03.2021 03:15",
"frozenPrice": 100,
"count": 5,
"cinemaId": 3
}
]
}
],
"sessions": [
@ -113,11 +71,19 @@
"maxCount": 15,
"cinemaId": 2,
"id": 5
},
{
"id": 6,
"dateTime": "22.02.2022 03:15",
"price": 100,
"maxCount": 500,
"cinemaId": 2
}
],
"cinemas": [
{
"name": "кино",
"id": 2,
"name": "кино123",
"description": "кино",
"image": [
-1,
@ -39085,8 +39051,7 @@
-1,
-39
],
"year": 2010,
"id": 2
"year": 2010
},
{
"id": 3,