Compare commits

...

3 Commits

Author SHA1 Message Date
abazov73
2eca8db087 Course work: implement full performance CRUD 2023-12-27 01:21:02 +04:00
abazov73
a687ff002f Course work: implement login 2023-12-26 23:33:11 +04:00
abazov73
d79f6982fe Course work: edit routes for new server 2023-12-26 18:55:30 +04:00
25 changed files with 900 additions and 45 deletions

View File

@ -67,7 +67,8 @@ dependencies {
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview") 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 // Room
val room_version = "2.5.2" val room_version = "2.5.2"
@ -94,4 +95,7 @@ dependencies {
androidTestImplementation("androidx.compose.ui:ui-test-junit4") androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest") debugImplementation("androidx.compose.ui:ui-test-manifest")
implementation("javax.inject:javax.inject:1")
implementation("androidx.compose.runtime:runtime-livedata")
} }

View File

@ -1,8 +1,11 @@
package com.example.mobile_labs.api package com.example.mobile_labs.api
import android.util.Log
import com.example.mobile_labs.api.models.Credentials
import com.example.mobile_labs.api.models.EventRemote import com.example.mobile_labs.api.models.EventRemote
import com.example.mobile_labs.api.models.PerformanceRemote import com.example.mobile_labs.api.models.PerformanceRemote
import com.example.mobile_labs.api.models.PersonRemote import com.example.mobile_labs.api.models.PersonRemote
import com.example.mobile_labs.api.models.Token
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
@ -16,53 +19,96 @@ import retrofit2.http.POST
import retrofit2.http.PUT import retrofit2.http.PUT
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
import java.util.concurrent.TimeUnit
interface MyServerService { interface MyServerService {
@GET("persons") @GET("api/persons")
suspend fun getPeople( suspend fun getPeople(
@Query("_page") page: Int, @Query("_page") page: Int,
@Query("_limit") limit: Int, @Query("_limit") limit: Int,
): List<PersonRemote> ): List<PersonRemote>
@GET("performances") @GET("api/performances")
suspend fun getPerformances( suspend fun getPerformances(
@Query("_page") page: Int, @Query("_page") page: Int,
@Query("_limit") limit: Int, @Query("_limit") limit: Int,
): List<PerformanceRemote> ): List<PerformanceRemote>
@GET("events") @GET("api/events")
suspend fun getEvents( suspend fun getEvents(
@Query("_page") page: Int, @Query("_page") page: Int,
@Query("_limit") limit: Int, @Query("_limit") limit: Int,
): List<EventRemote> ): List<EventRemote>
@GET("performances/{id}") @GET("api/performances/{id}")
suspend fun getPerformance( suspend fun getPerformance(
@Path("id") id: Int, @Path("id") id: Int,
): PerformanceRemote ): PerformanceRemote
@GET("persons/{id}") @GET("api/persons/{id}")
suspend fun getPerson( suspend fun getPerson(
@Path("id") id: Int, @Path("id") id: Int,
): PersonRemote ): PersonRemote
@GET("events/{id}") @GET("api/events/{id}")
suspend fun getEvent( suspend fun getEvent(
@Path("id") id: Int, @Path("id") id: Int,
): EventRemote ): EventRemote
@POST("api/login")
suspend fun login(
@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 { companion object {
private const val BASE_URL = "http://10.0.2.2:26000/" private const val BASE_URL = "http://10.0.2.2:8000/"
@Volatile @Volatile
private var INSTANCE: MyServerService? = null private var INSTANCE: MyServerService? = null
private var _token: String = ""
fun getInstance(): MyServerService { fun getInstance(): MyServerService {
return INSTANCE ?: synchronized(this) { return INSTANCE ?: synchronized(this) {
val logger = HttpLoggingInterceptor() val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BASIC logger.level = HttpLoggingInterceptor.Level.BASIC
val client = OkHttpClient.Builder() val client = OkHttpClient.Builder()
.connectTimeout(1, TimeUnit.DAYS)
.readTimeout(1, TimeUnit.DAYS)
.writeTimeout(1, TimeUnit.DAYS)
.retryOnConnectionFailure(false)
.callTimeout(1, TimeUnit.DAYS)
.addInterceptor(logger) .addInterceptor(logger)
.addInterceptor {
val originalRequest = it.request()
if (_token.isEmpty()) {
it.proceed(originalRequest)
} else {
it.proceed(
originalRequest
.newBuilder()
.header("Authorization", "Bearer $_token")
.method(originalRequest.method, originalRequest.body)
.build()
)
}
}
.build() .build()
return Retrofit.Builder() return Retrofit.Builder()
.baseUrl(BASE_URL) .baseUrl(BASE_URL)
@ -73,5 +119,13 @@ interface MyServerService {
.also { INSTANCE = it } .also { INSTANCE = it }
} }
} }
fun getToken(): String {
return _token
}
fun setToken(token: String) {
_token = token
}
} }
} }

View File

@ -73,6 +73,9 @@ class EventRemoteMediator(
nextKey = nextKey nextKey = nextKey
) )
} }
dbRemoteKeyRepository.deleteRemoteKey(RemoteKeyType.EVENT)
dbEventRepository.clearEvents()
performanceRestRepository.getAllPerformances() performanceRestRepository.getAllPerformances()
dbRemoteKeyRepository.createRemoteKeys(keys) dbRemoteKeyRepository.createRemoteKeys(keys)
dbEventRepository.insertEvents(events) dbEventRepository.insertEvents(events)

View File

@ -0,0 +1,9 @@
package com.example.mobile_labs.api.models
import kotlinx.serialization.Serializable
@Serializable
data class Credentials(
val email: String = "",
val password: String = "",
)

View File

@ -10,16 +10,16 @@ import java.time.LocalDate
data class EventRemote( data class EventRemote(
val id: Int = 0, val id: Int = 0,
val date: String = "", val date: String = "",
val performanceId: Int = 0, val performance_id: Int = 0,
) )
fun EventRemote.toEvent(): Event = Event( fun EventRemote.toEvent(): Event = Event(
id, id,
LocalDate.parse(date), LocalDate.parse(date),
performanceId performance_id
) )
suspend fun EventRemote.toEventWithPerformance(service: MyServerService): EventWithPerformance = EventWithPerformance( suspend fun EventRemote.toEventWithPerformance(service: MyServerService): EventWithPerformance = EventWithPerformance(
service.getEvent(id).toEvent(), service.getEvent(id).toEvent(),
service.getPerformance(performanceId).toPerformance() service.getPerformance(performance_id).toPerformance()
) )

View File

@ -11,10 +11,10 @@ data class PerformanceRemote(
val id: Int = 0, val id: Int = 0,
val title: String = "", val title: String = "",
val description: String = "", val description: String = "",
val authorId: Int = 0, val author_id: Int = 0,
val directorId: Int = 0, val director_id: Int = 0,
val imageURL: String = "", val image_url: String = "",
val previewImageURL: String = "", val preview_image_url: String = "",
val actors: List<Int> = listOf() val actors: List<Int> = listOf()
) )
@ -22,10 +22,10 @@ fun PerformanceRemote.toPerformance(): Performance = Performance(
id, id,
title, title,
description, description,
authorId, author_id,
directorId, director_id,
imageURL, image_url,
previewImageURL preview_image_url
) )
suspend fun PerformanceRemote.toPerformanceWithPeople(service: MyServerService): PerformanceWithPeople { suspend fun PerformanceRemote.toPerformanceWithPeople(service: MyServerService): PerformanceWithPeople {
@ -36,8 +36,18 @@ suspend fun PerformanceRemote.toPerformanceWithPeople(service: MyServerService):
return PerformanceWithPeople( return PerformanceWithPeople(
service.getPerformance(id).toPerformance(), service.getPerformance(id).toPerformance(),
service.getPerson(authorId).toPerson(), service.getPerson(author_id).toPerson(),
service.getPerson(directorId).toPerson(), service.getPerson(director_id).toPerson(),
actorsList.toList() actorsList.toList()
) )
} }
fun Performance.toPerformanceRemote(): PerformanceRemote = PerformanceRemote(
performance_uid!!,
title,
description,
authorId!!,
directorId!!,
imageURL,
previewImageURL
)

View File

@ -10,12 +10,12 @@ data class PersonRemote(
val id: Int = 0, val id: Int = 0,
val last_name: String = "", val last_name: String = "",
val first_name: String = "", val first_name: String = "",
val imageURL: String = "" val image_url: String = ""
) )
fun PersonRemote.toPerson(): Person = Person( fun PersonRemote.toPerson(): Person = Person(
id, id,
last_name, last_name,
first_name, first_name,
imageURL image_url
) )

View File

@ -0,0 +1,8 @@
package com.example.mobile_labs.api.models
import kotlinx.serialization.Serializable
@Serializable
data class Token(
val token: String
)

View File

@ -71,6 +71,9 @@ class PeopleRemoteMediator(
nextKey = nextKey nextKey = nextKey
) )
} }
dbRemoteKeyRepository.deleteRemoteKey(RemoteKeyType.PERSON)
dbPersonRepository.clearPeople()
dbRemoteKeyRepository.createRemoteKeys(keys) dbRemoteKeyRepository.createRemoteKeys(keys)
dbPersonRepository.insertPeople(people) dbPersonRepository.insertPeople(people)
} }

View File

@ -1,9 +1,11 @@
package com.example.mobile_labs.api.performance package com.example.mobile_labs.api.performance
import android.util.Log
import androidx.paging.ExperimentalPagingApi import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType import androidx.paging.LoadType
import androidx.paging.PagingState import androidx.paging.PagingState
import androidx.paging.RemoteMediator import androidx.paging.RemoteMediator
import androidx.paging.log
import androidx.room.withTransaction import androidx.room.withTransaction
import com.example.mobile_labs.api.MyServerService import com.example.mobile_labs.api.MyServerService
import com.example.mobile_labs.api.models.toPerformance import com.example.mobile_labs.api.models.toPerformance
@ -73,6 +75,9 @@ class PerformanceRemoteMediator(
nextKey = nextKey nextKey = nextKey
) )
} }
dbRemoteKeyRepository.deleteRemoteKey(RemoteKeyType.PERFORMANCE)
dbPerformanceRepository.clearPerformances()
personRestRepository.getAllPeople() personRestRepository.getAllPeople()
dbRemoteKeyRepository.createRemoteKeys(keys) dbRemoteKeyRepository.createRemoteKeys(keys)
dbPerformanceRepository.insertPerformances(performances) dbPerformanceRepository.insertPerformances(performances)

View File

@ -6,6 +6,7 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData import androidx.paging.PagingData
import com.example.mobile_labs.api.MyServerService import com.example.mobile_labs.api.MyServerService
import com.example.mobile_labs.api.models.toPerformance 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.models.toPerformanceWithPeople
import com.example.mobile_labs.api.people.RestPersonRepository import com.example.mobile_labs.api.people.RestPersonRepository
import com.example.mobile_labs.common.AppDataContainer import com.example.mobile_labs.common.AppDataContainer
@ -50,4 +51,16 @@ class RestPerformanceRepository(
override suspend fun getPerformance(uid: Int): PerformanceWithPeople = override suspend fun getPerformance(uid: Int): PerformanceWithPeople =
service.getPerformance(uid).toPerformanceWithPeople(service) 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 { companion object {
const val TIMEOUT = 5000L const val TIMEOUT = 5000L
const val LIMIT = 3 const val LIMIT = 20
} }
} }

View File

@ -7,7 +7,10 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import com.example.mobile_labs.TheatreApplication import com.example.mobile_labs.TheatreApplication
import com.example.mobile_labs.ui.event.list.EventListViewModel 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.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.performance.view.PerformanceViewModel
import com.example.mobile_labs.ui.person.list.PeopleListViewModel import com.example.mobile_labs.ui.person.list.PeopleListViewModel
@ -25,6 +28,15 @@ object AppViewModelProvider {
initializer { initializer {
PerformanceViewModel(this.createSavedStateHandle(), theatreApplication().container.performanceRestRepository) PerformanceViewModel(this.createSavedStateHandle(), theatreApplication().container.performanceRestRepository)
} }
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 { interface PerformanceRepository {
fun getAllPerformances(): Flow<PagingData<Performance>> fun getAllPerformances(): Flow<PagingData<Performance>>
suspend fun getPerformance(uid: Int): PerformanceWithPeople 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 ).flow
override suspend fun getPerformance(uid: Int): PerformanceWithPeople = performanceDao.getByUid(uid).first(); 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() suspend fun clearPerformances() = performanceDao.deleteAll()

View File

@ -0,0 +1,116 @@
package com.example.mobile_labs.ui.login
import androidx.compose.foundation.layout.*
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.*
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import com.example.mobile_labs.R
import com.example.mobile_labs.common.AppViewModelProvider
import com.example.mobile_labs.ui.navigation.Screen
@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun Login(
navController: NavHostController,
viewModel: LoginViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val success = viewModel.successState.observeAsState().value
val login = remember { mutableStateOf(TextFieldValue(""))}
val password = remember { mutableStateOf(TextFieldValue(""))}
if (success == false) {
AlertDialog(
title = {
Text(
text = "Ошибка авторизации",
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)
},
text = {
Text(
text = "Неверный логин или пароль",
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
},
onDismissRequest = {
viewModel.calmSuccessState()
},
confirmButton = {
TextButton(
onClick = {
viewModel.calmSuccessState()
}
) {
Text("OK")
}
}
)
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize()
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth(0.7f).fillMaxHeight().padding(vertical=100.dp)
) {
Text(
text = "Вход",
style = TextStyle(
fontSize = 40.sp,
fontWeight = FontWeight(400),
),
modifier = Modifier.fillMaxHeight(0.3f)
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth().padding(bottom=30.dp),
value = login.value,
onValueChange = { login.value = it },
label = { Text(stringResource(id = R.string.login)) },
singleLine = true
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth().padding(bottom=30.dp),
value = password.value,
onValueChange = { password.value = it },
label = { Text(stringResource(id = R.string.password)) },
singleLine = true
)
Button(
{
viewModel.login(login.value.text, password.value.text, navController)
},
modifier = Modifier.padding(bottom=20.dp).fillMaxWidth(0.5f)
) {
Text(
text = "Войти",
style = TextStyle(
fontSize = 24.sp,
fontWeight = FontWeight(400),
color = Color(0xFFFFFFFF),
)
)
}
}
}
}

View File

@ -0,0 +1,49 @@
package com.example.mobile_labs.ui.login
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavHostController
import com.example.mobile_labs.api.MyServerService
import com.example.mobile_labs.api.models.Credentials
import com.example.mobile_labs.ui.navigation.Screen
import kotlinx.coroutines.launch
import javax.inject.Inject
class LoginViewModel : ViewModel() {
private val _successState = MutableLiveData<Boolean?>()
val successState: LiveData<Boolean?>
get() = _successState
fun calmSuccessState() {
_successState.postValue(null)
}
fun login(login: String, password: String, navController: NavHostController) {
viewModelScope.launch {
val token = MyServerService.getInstance().login(Credentials(login, password))
if (token.token.isEmpty()) {
_successState.postValue(false)
return@launch
}
MyServerService.setToken(token.token)
_successState.setValue(true)
navigate(navController)
}
}
fun navigate(navController: NavHostController) {
Screen.About.route.let {
navController.navigate(it) {
popUpTo(it) {
inclusive = true
}
}
}
}
}

View File

@ -1,6 +1,7 @@
package com.example.mobile_labs.ui.navigation package com.example.mobile_labs.ui.navigation
import android.content.res.Configuration import android.content.res.Configuration
import android.util.Log
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -33,9 +34,13 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.example.mobile_labs.ui.theme.Mobile_LabsTheme import com.example.mobile_labs.ui.theme.Mobile_LabsTheme
import com.example.mobile_labs.R import com.example.mobile_labs.R
import com.example.mobile_labs.api.MyServerService
import com.example.mobile_labs.ui.about.About import com.example.mobile_labs.ui.about.About
import com.example.mobile_labs.ui.event.list.EventList 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.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.performance.view.PerformanceView
import com.example.mobile_labs.ui.person.list.PeopleList import com.example.mobile_labs.ui.person.list.PeopleList
@ -77,21 +82,41 @@ fun Navbar(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
NavigationBar(modifier) { NavigationBar(modifier) {
Screen.bottomBarItems.forEach { screen -> if (MyServerService.getToken().isBlank()) {
NavigationBarItem( Screen.bottomBarItems.forEach { screen ->
icon = { Icon(screen.icon, contentDescription = null) }, NavigationBarItem(
label = { Text(stringResource(screen.resourceId)) }, icon = { Icon(screen.icon, contentDescription = null) },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true, label = { Text(stringResource(screen.resourceId)) },
onClick = { selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
navController.navigate(screen.route) { onClick = {
popUpTo(navController.graph.findStartDestination().id) { navController.navigate(screen.route) {
saveState = true popUpTo(navController.graph.findStartDestination().id) {
saveState = false
}
launchSingleTop = true
restoreState = false
} }
launchSingleTop = true
restoreState = true
} }
} )
) }
}
else {
Screen.adminBottomBarItems.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
}
}
)
}
} }
} }
} }
@ -108,14 +133,29 @@ fun Navhost(
modifier.padding(innerPadding) modifier.padding(innerPadding)
) { ) {
composable(Screen.Schedule.route) { EventList(navController) } composable(Screen.Schedule.route) { EventList(navController) }
composable(Screen.Repertoire.route) { PerformanceList(navController) } composable(Screen.Repertoire.route) {
if (MyServerService.getToken().isNotBlank()) {
AdminPerformanceList(navController)
}
else {
PerformanceList(navController)
}
}
composable(Screen.PeopleList.route) { PeopleList() } composable(Screen.PeopleList.route) { PeopleList() }
composable(Screen.About.route) { About() } composable(Screen.About.route) { About() }
if (MyServerService.getToken().isBlank()) {
composable(Screen.Login.route) { Login(navController) }
}
composable( composable(
Screen.PerformanceView.route, Screen.PerformanceView.route,
arguments = listOf(navArgument("id") { type = NavType.IntType }) arguments = listOf(navArgument("id") { type = NavType.IntType })
) { backStackEntry -> ) { backStackEntry ->
backStackEntry.arguments?.let { PerformanceView() } if (MyServerService.getToken().isNotBlank()) {
AdminPerformanceView(navController)
}
else {
backStackEntry.arguments?.let { PerformanceView() }
}
} }
} }
} }

View File

@ -3,12 +3,14 @@ package com.example.mobile_labs.ui.navigation
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DateRange import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Face
import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.List import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import com.example.mobile_labs.R import com.example.mobile_labs.R
import com.example.mobile_labs.api.MyServerService
enum class Screen( enum class Screen(
val route: String, val route: String,
@ -30,6 +32,9 @@ enum class Screen(
), ),
About( About(
"about", R.string.about_main_title, Icons.Filled.Info "about", R.string.about_main_title, Icons.Filled.Info
),
Login(
"login", R.string.login_main_title, Icons.Filled.Face
); );
companion object { companion object {
@ -38,6 +43,12 @@ enum class Screen(
Repertoire, Repertoire,
PeopleList, PeopleList,
About, About,
Login
)
val adminBottomBarItems = listOf(
Repertoire,
About,
) )
fun getItem(route: String): Screen? { fun getItem(route: String): Screen? {

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

@ -1,6 +1,7 @@
package com.example.mobile_labs.ui.performance.list package com.example.mobile_labs.ui.performance.list
import android.content.res.Configuration import android.content.res.Configuration
import android.util.Log
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -28,6 +29,7 @@ import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey import androidx.paging.compose.itemKey
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.example.mobile_labs.R 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.database.performance.model.Performance
import com.example.mobile_labs.common.AppViewModelProvider import com.example.mobile_labs.common.AppViewModelProvider
import com.example.mobile_labs.ui.navigation.Screen import com.example.mobile_labs.ui.navigation.Screen

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
)

View File

@ -6,12 +6,15 @@
<string name="performance_author">Автор</string> <string name="performance_author">Автор</string>
<string name="performance_director">Режиссёр</string> <string name="performance_director">Режиссёр</string>
<string name="performance_actors">Актёры</string> <string name="performance_actors">Актёры</string>
<string name="people_main_title">Люди театра</string> <string name="people_main_title">Труппа</string>
<string name="about_main_title">О нас</string> <string name="about_main_title">О нас</string>
<string name="login_main_title">Вход</string>
<string name="performance_view_main_title">Представление</string> <string name="performance_view_main_title">Представление</string>
<string name="people_missing_description">Данные о людях отсутствуют</string> <string name="people_missing_description">Данные о людях отсутствуют</string>
<string name="events_missing_description">Данные о событиях отсутствуют</string> <string name="events_missing_description">Данные о событиях отсутствуют</string>
<string name="performance_missing_description">Данные о представлениях отсутствуют</string> <string name="performance_missing_description">Данные о представлениях отсутствуют</string>
<string name="login">Логин</string>
<string name="password">Пароль</string>
<string name="about_text"> <string name="about_text">
<p> <p>
Netus quis congue nascetur ullamcorper nibh, nostra iaculis turpis! Facilisi pretium vehicula purus porttitor vitae aliquet dignissim. Donec diam molestie litora magnis dolor aptent scelerisque mus. Sit mi, venenatis interdum. Commodo vel malesuada tincidunt eget. Aenean laoreet lacinia platea sem? Libero urna odio diam? Nisl, sodales nisi gravida. Interdum elementum libero turpis dapibus tristique per sed maecenas ante integer massa? Tortor molestie sapien himenaeos condimentum. Facilisis accumsan ullamcorper semper fermentum elementum quisque. Curae;, vivamus ante hac elit fringilla odio ornare curabitur quisque magna commodo. Placerat proin! Netus quis congue nascetur ullamcorper nibh, nostra iaculis turpis! Facilisi pretium vehicula purus porttitor vitae aliquet dignissim. Donec diam molestie litora magnis dolor aptent scelerisque mus. Sit mi, venenatis interdum. Commodo vel malesuada tincidunt eget. Aenean laoreet lacinia platea sem? Libero urna odio diam? Nisl, sodales nisi gravida. Interdum elementum libero turpis dapibus tristique per sed maecenas ante integer massa? Tortor molestie sapien himenaeos condimentum. Facilisis accumsan ullamcorper semper fermentum elementum quisque. Curae;, vivamus ante hac elit fringilla odio ornare curabitur quisque magna commodo. Placerat proin!