Compare commits

..

35 Commits

Author SHA1 Message Date
3619e27dec лишнее 2023-12-27 17:34:15 +04:00
5e33722852 исправление входа 2023-12-26 18:50:11 +04:00
ff58cccfc5 никчемный рефакторинг 2023-12-26 17:43:32 +04:00
93f4af21a9 хотфикс сеансов 2023-12-26 17:25:22 +04:00
d35ee4907d не знаю 2023-12-25 21:15:57 +04:00
c8f8b59672 немного изменения медиатора 2023-12-24 17:54:20 +04:00
4bf7ebb6bf заказы для админа 2023-12-24 17:28:56 +04:00
f11ebc82f5 итого в отчете 2023-12-22 15:18:00 +04:00
c4c941c0c9 Merge remote-tracking branch 'origin/coursework' into coursework 2023-12-21 14:40:42 +04:00
01bc335c4f исправления отчета. надо проверить работоспособность 2023-12-21 14:40:07 +04:00
7fbe49527a исправления отчета. надо проверить работоспособность 2023-12-21 14:39:16 +04:00
4b84248c66 защита от приколистов 2023-12-20 19:28:17 +04:00
39f2cf609c некорректное отображение данных в корзине 2023-12-20 16:26:51 +04:00
a66218c7ab некорректное отображение данных в корзине 2023-12-20 16:26:43 +04:00
a14249d6ef некорректное отображение данных в корзине 2023-12-19 21:53:53 +04:00
37f7fbaf9e некорректное отображение данных в корзине 2023-12-19 21:51:11 +04:00
1e1dd76b7b некорректное отображение данных в корзине 2023-12-19 21:51:03 +04:00
1a72d6d368 неиспользуемый функционал 2023-12-19 20:49:30 +04:00
5e992dbf8a неиспользуемый функционал 2023-12-19 19:44:47 +04:00
baf333dea1 исправление багов в медиаторе заказов 2023-12-18 22:35:26 +04:00
30f50e94c7 корректное отображение при регистрации 2023-12-18 22:00:26 +04:00
c0b94141a3 exception handling 2023-12-18 01:43:48 +04:00
42c21b0ce7 поиск и эдиты 2023-12-17 18:16:41 +04:00
55a0ac8581 поиск 2023-12-17 17:13:27 +04:00
3dc39aa82f рефакторинг 2023-12-16 21:00:28 +04:00
aab18402b5 отчет 2023-12-15 19:49:41 +04:00
d45b6d7ed8 регистрация и вход 2023-12-13 20:25:02 +04:00
d709367774 регистрация и вход 2023-12-13 20:02:51 +04:00
28fd92559c регистрация и вход 2023-12-13 19:12:51 +04:00
167d5ccc7d reports 2023-12-09 20:44:31 +04:00
7f0c3a638d lw5 2023-12-05 15:59:14 +04:00
f499c56d14 lw5 2023-12-05 15:31:10 +04:00
e4bc55da4e lw4 2023-12-05 15:29:15 +04:00
5a18ee67fc lw3 2023-12-05 14:48:48 +04:00
f879990c39 lw3 2023-12-05 14:48:15 +04:00
123 changed files with 514346 additions and 102 deletions

View File

@ -0,0 +1,41 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.10" /> <option name="version" value="1.8.20" />
</component> </component>
</project> </project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,6 +1,8 @@
plugins { plugins {
id("com.android.application") id("com.android.application")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("com.google.devtools.ksp")
id("org.jetbrains.kotlin.plugin.serialization")
} }
android { android {
@ -9,7 +11,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "com.example.myapplication" applicationId = "com.example.myapplication"
minSdk = 24 minSdk = 26
targetSdk = 33 targetSdk = 33
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
@ -23,39 +25,74 @@ android {
buildTypes { buildTypes {
release { release {
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 isCoreLibraryDesugaringEnabled = true
targetCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "17"
} }
buildFeatures { buildFeatures {
compose = true compose = true
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = "1.4.3" kotlinCompilerExtensionVersion = "1.4.5"
} }
packaging { packaging {
resources { resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
} }
} }
buildToolsVersion = "34.0.0"
} }
dependencies { dependencies {
implementation("org.threeten:threetenbp:1.5.0")
implementation("androidx.datastore:datastore-preferences:1.0.0")
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.core:core-ktx:1.9.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
implementation("androidx.activity:activity-compose:1.7.0")
// UI
implementation("androidx.activity:activity-compose:1.7.2")
implementation(platform("androidx.compose:compose-bom:2023.03.00")) implementation(platform("androidx.compose:compose-bom:2023.03.00"))
implementation("androidx.navigation:navigation-compose:2.6.0")
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
val room_version = "2.5.2"
implementation("androidx.room:room-runtime:$room_version")
annotationProcessor("androidx.room:room-compiler:$room_version")
ksp("androidx.room:room-compiler:$room_version")
implementation("androidx.room:room-ktx:$room_version")
implementation("androidx.room:room-paging:$room_version")
// retrofit
val retrofitVersion = "2.9.0"
implementation("com.squareup.retrofit2:retrofit:$retrofitVersion")
implementation("com.squareup.okhttp3:logging-interceptor:4.11.0")
implementation("androidx.paging:paging-compose:3.2.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
// Tests
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

View File

@ -1,13 +1,11 @@
package com.example.myapplication package com.example.myapplication
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.*
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.Assert.*
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
* *

View File

@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:name=".CinemaApplication"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
@ -10,13 +13,14 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.MyApplication" android:theme="@style/Theme.Pmudemo"
tools:targetApi="31"> tools:targetApi="31"
android:networkSecurityConfig="@xml/network_security_config">
<activity <activity
android:name=".MainActivity" android:name=".MainComposeActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.MyApplication"> android:theme="@style/Theme.Pmudemo">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@ -0,0 +1,14 @@
package com.example.myapplication
import android.app.Application
import com.example.myapplication.database.AppContainer
import com.example.myapplication.database.AppDataContainer
class CinemaApplication : Application() {
lateinit var container: AppContainer
override fun onCreate() {
super.onCreate()
container = AppDataContainer(this)
}
}

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

@ -1,43 +0,0 @@
package com.example.myapplication
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.myapplication.ui.theme.MyApplicationTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
MyApplicationTheme {
Greeting("Android")
}
}

View File

@ -0,0 +1,48 @@
package com.example.myapplication
import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
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
class MainComposeActivity : ComponentActivity() {
private val dataStoreManager = DataStoreManager(this)
private val isDarkTheme = mutableStateOf(true)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
application.deleteDatabase("pmy-db")
appContext = applicationContext
setContent {
PmudemoTheme(darkTheme = isDarkTheme.value) {
LaunchedEffect(key1 = true) {
dataStoreManager.getDarkTheme().collect { setting ->
isDarkTheme.value = setting == "Dark"
}
}
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Authenticator(dataStoreManager)
MainNavbar(
isDarkTheme = isDarkTheme,
dataStoreManager = dataStoreManager
)
}
}
}
}
companion object {
lateinit var appContext: Context
}
}

View File

@ -0,0 +1,3 @@
package com.example.myapplication.api
enum class ApiStatus { LOADING, ERROR, DONE }

View File

@ -0,0 +1,26 @@
package com.example.myapplication.api
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import org.threeten.bp.LocalDateTime
import org.threeten.bp.format.DateTimeFormatter
@kotlinx.serialization.ExperimentalSerializationApi
object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalDateTime) {
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
encoder.encodeString(dateFormatter.format(value).toString())
}
override fun deserialize(decoder: Decoder): LocalDateTime {
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
return LocalDateTime.parse(decoder.decodeString(), dateFormatter)
}
}

View File

@ -0,0 +1,194 @@
package com.example.myapplication.api
import com.example.myapplication.api.cinema.CinemaRemote
import com.example.myapplication.api.cinema.CinemaWithSessionsRemote
import com.example.myapplication.api.order.OrderRemote
import com.example.myapplication.api.order.OrderWithUserRemote
import com.example.myapplication.api.session.ReportRemote
import com.example.myapplication.api.session.SessionFromCinemaRemote
import com.example.myapplication.api.session.SessionRemote
import com.example.myapplication.api.session.SessionWithCinemaRemote
import com.example.myapplication.api.user.UserRemote
import com.example.myapplication.api.user.UserSessionRemote
import com.example.myapplication.api.user.UserSessionWithSessionRemote
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
import java.util.Date
interface MyServerService {
@GET("orders")
suspend fun getOrders(): List<OrderRemote>
@GET("cinemas")
suspend fun getCinemas(
@Query("_page") page: Int,
@Query("_limit") limit: Int,
): List<CinemaRemote>
@GET("cinemas/{id}")
suspend fun getCinema(
@Path("id") id: Int,
): CinemaRemote
@GET("cinemas/{id}?_embed=sessions")
suspend fun getCinemaWithSessions(
@Path("id") id: Int,
): CinemaWithSessionsRemote
@POST("cinemas")
suspend fun createCinema(
@Body cinema: CinemaRemote,
): CinemaRemote
@PUT("cinemas/{id}")
suspend fun updateCinema(
@Path("id") id: Int,
@Body cinema: CinemaRemote,
): CinemaRemote
@DELETE("cinemas/{id}")
suspend fun deleteCinema(
@Path("id") id: Int,
)
@GET("sessions/{id}?_expand=cinema")
suspend fun getSession(
@Path("id") id: Int,
): SessionWithCinemaRemote
@POST("sessions")
suspend fun createSession(
@Body session: SessionRemote,
): SessionRemote
@PUT("sessions/{id}")
suspend fun updateSession(
@Path("id") id: Int,
@Body session: SessionRemote,
): SessionRemote
@DELETE("sessions/{id}")
suspend fun deleteSession(
@Path("id") id: Int,
): SessionFromCinemaRemote
@GET("userssessions?_expand=session")
suspend fun getUserCart(
@Query("userId") userId: Int,
): List<UserSessionWithSessionRemote>
@GET("userssessions")
suspend fun getUserSessions(
@Query("userId") userId: Int,
): List<UserSessionRemote>
@GET("userssessions?_expand=session")
suspend fun getUsersSessions(): List<UserSessionWithSessionRemote>
@DELETE("userssessions/{id}")
suspend fun deleteUserSession(
@Path("id") id: Int,
): UserSessionRemote
@GET("users?_limit=1")
suspend fun getUser(
@Query("login") login: String,
): List<UserRemote>
@GET("userssessions?_limit=1")
suspend fun getUserSession(
@Query("userId") userId: Int,
@Query("sessionId") sessionId: Int,
): List<UserSessionRemote>
@POST("userssessions")
suspend fun createUserSession(
@Body userSessionRemote: UserSessionRemote,
): UserSessionRemote
@PUT("userssessions/{id}")
suspend fun updateUserCart(
@Path("id") id: Int,
@Body userSessionRemote: UserSessionRemote,
): UserSessionRemote
@POST("users")
suspend fun createUser(
@Body user: UserRemote,
): UserRemote
@GET("orders")
suspend fun getOrders(
@Query("userId") userId: Int,
@Query("_page") page: Int,
@Query("_limit") limit: Int,
): List<OrderRemote>
@GET("orders?_expand=user")
suspend fun getOrders(
@Query("_page") page: Int,
@Query("_limit") limit: Int,
): List<OrderWithUserRemote>
@GET("orders/{id}")
suspend fun getOrder(
@Path("id") id: Int,
): OrderRemote
@POST("orders")
suspend fun createOrder(
@Body order: OrderRemote,
): OrderRemote
@PUT("orders/{id}")
suspend fun updateOrder(
@Path("id") id: Int,
@Body orderRemote: OrderRemote,
): OrderRemote
@GET("report")
suspend fun getReport(
@Query("startDate") startDate: Date,
@Query("endDate") endDate: Date
): ReportRemote
companion object {
//private const val BASE_URL = "http://192.168.154.166:8080/"
private const val BASE_URL = "http://192.168.0.101:8079/"
@Volatile
private var INSTANCE: MyServerService? = null
@OptIn(ExperimentalSerializationApi::class)
fun getInstance(): MyServerService {
return INSTANCE ?: synchronized(this) {
val logger = HttpLoggingInterceptor()
logger.level = HttpLoggingInterceptor.Level.BASIC
val client = OkHttpClient.Builder().addInterceptor(logger).build()
val json = Json {
ignoreUnknownKeys = true
serializersModule = SerializersModule {
contextual(LocalDateTimeSerializer)
}
} // Создаем экземпляр Json с ignoreUnknownKeys = true
return Retrofit.Builder().baseUrl(BASE_URL).client(client)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType())) // Применяем конфигурацию Json
.build().create(MyServerService::class.java).also { INSTANCE = it }
}
}
}
}

View File

@ -0,0 +1,29 @@
package com.example.myapplication.api.cinema
import com.example.myapplication.database.entities.model.Cinema
import kotlinx.serialization.Serializable
@Serializable
data class CinemaRemote(
val id: Int = 0,
val name: String = "",
val description: String = "",
val image: ByteArray? = null,
val year: Long = 0
)
fun CinemaRemote.toCinema(): Cinema = Cinema(
id,
name,
description,
image,
year
)
fun Cinema.toCinemaRemote(): CinemaRemote = CinemaRemote(
uid,
name,
description,
image,
year
)

View File

@ -0,0 +1,112 @@
package com.example.myapplication.api.cinema
import android.database.sqlite.SQLiteConstraintException
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.example.myapplication.api.MyServerService
import com.example.myapplication.database.AppDatabase
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.repository.OfflineCinemaRepository
import com.example.myapplication.database.entities.repository.OfflineSessionRepository
import com.example.myapplication.database.remotekeys.model.RemoteKeyType
import com.example.myapplication.database.remotekeys.model.RemoteKeys
import com.example.myapplication.database.remotekeys.repository.OfflineRemoteKeyRepository
import retrofit2.HttpException
import java.io.IOException
@OptIn(ExperimentalPagingApi::class)
class CinemaRemoteMediator(
private val service: MyServerService,
private val dbCinemaRepository: OfflineCinemaRepository,
private val dbSessionRepository: OfflineSessionRepository,
private val dbRemoteKeyRepository: OfflineRemoteKeyRepository,
private val database: AppDatabase
) : RemoteMediator<Int, Cinema>() {
override suspend fun initialize(): InitializeAction {
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Cinema>
): MediatorResult {
val page = when (loadType) {
LoadType.REFRESH -> {
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(1) ?: 1
}
LoadType.PREPEND -> {
val remoteKeys = getRemoteKeyForFirstItem(state)
remoteKeys?.prevKey
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
}
LoadType.APPEND -> {
val remoteKeys = getRemoteKeyForLastItem(state)
remoteKeys?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
}
}
try {
val cinemas = service.getCinemas(page, state.config.pageSize).map { it.toCinema() }
val endOfPaginationReached = cinemas.isEmpty()
database.withTransaction {
if (loadType == LoadType.REFRESH) {
dbRemoteKeyRepository.deleteRemoteKey(RemoteKeyType.CINEMA)
dbSessionRepository.clearSessions()
dbCinemaRepository.clearCinemas()
}
val prevKey = if (page == 1) null else page - 1
val nextKey = if (endOfPaginationReached) null else page + 1
val keys = cinemas.map {
RemoteKeys(
entityId = it.uid,
type = RemoteKeyType.CINEMA,
prevKey = prevKey,
nextKey = nextKey
)
}
dbRemoteKeyRepository.createRemoteKeys(keys)
dbCinemaRepository.insertCinemas(cinemas)
}
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (exception: IOException) {
return MediatorResult.Error(exception)
} catch (exception: HttpException) {
return MediatorResult.Error(exception)
} catch (exception: SQLiteConstraintException) {
return MediatorResult.Error(exception)
}
}
private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, Cinema>): RemoteKeys? {
return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { cinema ->
dbRemoteKeyRepository.getAllRemoteKeys(cinema.uid, RemoteKeyType.CINEMA)
}
}
private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, Cinema>): RemoteKeys? {
return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
?.let { cinema ->
dbRemoteKeyRepository.getAllRemoteKeys(cinema.uid, RemoteKeyType.CINEMA)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(
state: PagingState<Int, Cinema>
): RemoteKeys? {
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.uid?.let { cinemaUid ->
dbRemoteKeyRepository.getAllRemoteKeys(cinemaUid, RemoteKeyType.CINEMA)
}
}
}
}

View File

@ -0,0 +1,26 @@
package com.example.myapplication.api.cinema
import com.example.myapplication.api.session.SessionFromCinemaRemote
import com.example.myapplication.database.entities.model.Cinema
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CinemaWithSessionsRemote(
val id: Int = 0,
val name: String = "",
val description: String = "",
val image: ByteArray? = null,
val year: Long = 0,
@SerialName("sessions")
val sessions: List<SessionFromCinemaRemote>,
)
fun CinemaWithSessionsRemote.toCinema(): Cinema = Cinema(
id,
name,
description,
image,
year
)

View File

@ -0,0 +1,94 @@
package com.example.myapplication.api.cinema
import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.example.myapplication.api.MyServerService
import com.example.myapplication.api.session.toSessionFromCinema
import com.example.myapplication.database.AppContainer
import com.example.myapplication.database.AppDatabase
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.CinemaWithSessions
import com.example.myapplication.database.entities.model.SessionFromCinema
import com.example.myapplication.database.entities.model.toSession
import com.example.myapplication.database.entities.repository.CinemaRepository
import com.example.myapplication.database.entities.repository.OfflineCinemaRepository
import com.example.myapplication.database.entities.repository.OfflineSessionRepository
import com.example.myapplication.database.remotekeys.repository.OfflineRemoteKeyRepository
import kotlinx.coroutines.flow.Flow
class RestCinemaRepository(
private val service: MyServerService,
private val dbCinemaRepository: OfflineCinemaRepository,
private val dbSessionRepository: OfflineSessionRepository,
private val dbRemoteKeyRepository: OfflineRemoteKeyRepository,
private val database: AppDatabase
) : CinemaRepository {
override fun getAllCinemas(name: String): Flow<PagingData<Cinema>> {
@OptIn(ExperimentalPagingApi::class)
return Pager(
config = PagingConfig(
pageSize = AppContainer.LIMIT,
enablePlaceholders = false
),
remoteMediator = CinemaRemoteMediator(
service,
dbCinemaRepository,
dbSessionRepository,
dbRemoteKeyRepository,
database,
),
) {
if (name.isEmpty()) {
dbCinemaRepository.getAllCinemasPagingSource()
} else {
dbCinemaRepository.getAllCinemasPagingSource(name)
}
}.flow
}
override suspend fun getCinema(uid: Int): CinemaWithSessions {
val cinemaWithSessions = service.getCinemaWithSessions(uid)
val orders = service.getOrders()
val sessions = cinemaWithSessions.sessions.map { sessionFromCinemaRemote ->
SessionFromCinema(
sessionFromCinemaRemote.id,
sessionFromCinemaRemote.dateTime,
sessionFromCinemaRemote.price,
sessionFromCinemaRemote.maxCount - orders.flatMap
{ order ->
order.sessions.filter { session ->
session.id == sessionFromCinemaRemote.id &&
session.cinemaId == sessionFromCinemaRemote.cinemaId &&
session.dateTime == sessionFromCinemaRemote.dateTime
}
}.sumOf { session -> session.count },
uid
)
}
dbSessionRepository.insertSessions(cinemaWithSessions.sessions.map {
it.toSessionFromCinema().toSession()
})
return CinemaWithSessions(cinemaWithSessions.toCinema(), sessions)
}
override suspend fun insertCinema(cinema: Cinema) {
service.createCinema(cinema.toCinemaRemote()).toCinema()
}
override suspend fun updateCinema(cinema: Cinema) {
service.updateCinema(cinema.uid, cinema.toCinemaRemote()).toCinema()
}
override suspend fun deleteCinema(cinema: Cinema) {
val cart = service.getUsersSessions()
cart.forEach { userSessionRemote ->
if (userSessionRemote.session.cinemaId == cinema.uid) {
service.deleteUserSession(userSessionRemote.id)
}
}
service.deleteCinema(cinema.uid)
dbCinemaRepository.deleteCinema(cinema)
}
}

View File

@ -0,0 +1,24 @@
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,
@Contextual
val dateTime: LocalDateTime = LocalDateTime.now(),
var sessions: List<SessionFromOrderRemote> = emptyList()
)
fun OrderRemote.toOrder(): Order = Order(
id, userId, dateTime
)
fun Order.toOrderRemote(): OrderRemote = OrderRemote(
uid, userId, dateTime, sessions = emptyList()
)

View File

@ -0,0 +1,126 @@
package com.example.myapplication.api.order
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.example.myapplication.LiveStore
import com.example.myapplication.api.MyServerService
import com.example.myapplication.api.user.toUser
import com.example.myapplication.database.AppDatabase
import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.model.OrderWithUser
import com.example.myapplication.database.entities.model.User
import com.example.myapplication.database.entities.model.UserRole
import com.example.myapplication.database.entities.repository.OfflineOrderRepository
import com.example.myapplication.database.entities.repository.OfflineUserRepository
import com.example.myapplication.database.remotekeys.model.RemoteKeyType
import com.example.myapplication.database.remotekeys.model.RemoteKeys
import com.example.myapplication.database.remotekeys.repository.OfflineRemoteKeyRepository
import retrofit2.HttpException
import java.io.IOException
@OptIn(ExperimentalPagingApi::class)
class OrderRemoteMediator(
private val service: MyServerService,
private val dbOrderRepository: OfflineOrderRepository,
private val dbUserRepository: OfflineUserRepository,
private val dbRemoteKeyRepository: OfflineRemoteKeyRepository,
private val database: AppDatabase
) : RemoteMediator<Int, OrderWithUser>() {
override suspend fun initialize(): InitializeAction {
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, OrderWithUser>
): MediatorResult {
val page = when (loadType) {
LoadType.REFRESH -> {
val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
remoteKeys?.nextKey?.minus(1) ?: 1
}
LoadType.PREPEND -> {
val remoteKeys = getRemoteKeyForFirstItem(state)
remoteKeys?.prevKey
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
}
LoadType.APPEND -> {
val remoteKeys = getRemoteKeyForLastItem(state)
remoteKeys?.nextKey
?: return MediatorResult.Success(endOfPaginationReached = remoteKeys != null)
}
}
try {
val user = LiveStore.user
var users: List<User> = emptyList()
val orders: List<Order> = if (user.value?.role == UserRole.ADMIN) {
val temp = service.getOrders(page = page, limit = state.config.pageSize)
users = temp.map { it.user.toUser() }
temp.map { it.toOrder() }
} else {
service.getOrders(
userId = user.value?.uid ?: 0, page = page, limit = state.config.pageSize
).map { it.toOrder() }
}
val endOfPaginationReached = orders.isEmpty()
database.withTransaction {
if (loadType == LoadType.REFRESH) {
dbRemoteKeyRepository.deleteRemoteKey(RemoteKeyType.ORDER)
dbUserRepository.clearUsers()
dbOrderRepository.clearOrders()
}
val prevKey = if (page == 1) null else page - 1
val nextKey = if (endOfPaginationReached) null else page + 1
val keys = orders.map {
RemoteKeys(
entityId = it.uid,
type = RemoteKeyType.ORDER,
prevKey = prevKey,
nextKey = nextKey
)
}
dbRemoteKeyRepository.createRemoteKeys(keys)
dbOrderRepository.insertOrders(orders)
dbUserRepository.insertUsers(users)
}
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (exception: IOException) {
return MediatorResult.Error(exception)
} catch (exception: HttpException) {
return MediatorResult.Error(exception)
}
}
private suspend fun getRemoteKeyForLastItem(state: PagingState<Int, OrderWithUser>): RemoteKeys? {
return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull()
?.let { order ->
dbRemoteKeyRepository.getAllRemoteKeys(order.uid, RemoteKeyType.ORDER)
}
}
private suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, OrderWithUser>): RemoteKeys? {
return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull()
?.let { order ->
dbRemoteKeyRepository.getAllRemoteKeys(order.uid, RemoteKeyType.ORDER)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(
state: PagingState<Int, OrderWithUser>
): RemoteKeys? {
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.uid?.let { orderUid ->
dbRemoteKeyRepository.getAllRemoteKeys(orderUid, RemoteKeyType.ORDER)
}
}
}
}

View File

@ -0,0 +1,22 @@
package com.example.myapplication.api.order
import com.example.myapplication.api.session.SessionFromOrderRemote
import com.example.myapplication.api.user.UserRemote
import com.example.myapplication.database.entities.model.Order
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import org.threeten.bp.LocalDateTime
@Serializable
data class OrderWithUserRemote(
val id: Int = 0,
val userId: Int = 0,
val user: UserRemote,
@Contextual
val dateTime: LocalDateTime = LocalDateTime.now(),
var sessions: List<SessionFromOrderRemote> = emptyList()
)
fun OrderWithUserRemote.toOrder(): Order = Order(
id, userId, dateTime
)

View File

@ -0,0 +1,69 @@
package com.example.myapplication.api.order
import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.example.myapplication.api.MyServerService
import com.example.myapplication.api.session.toSessionFromOrder
import com.example.myapplication.database.AppContainer
import com.example.myapplication.database.AppDatabase
import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.model.OrderSessionCrossRef
import com.example.myapplication.database.entities.model.OrderWithUser
import com.example.myapplication.database.entities.model.SessionFromOrder
import com.example.myapplication.database.entities.repository.OfflineOrderRepository
import com.example.myapplication.database.entities.repository.OfflineOrderSessionRepository
import com.example.myapplication.database.entities.repository.OfflineUserRepository
import com.example.myapplication.database.entities.repository.OrderRepository
import com.example.myapplication.database.remotekeys.repository.OfflineRemoteKeyRepository
import kotlinx.coroutines.flow.Flow
class RestOrderRepository(
private val service: MyServerService,
private val dbOrderRepository: OfflineOrderRepository,
private val dbUserRepository: OfflineUserRepository,
private val dbOrderSessionRepository: OfflineOrderSessionRepository,
private val dbRemoteKeyRepository: OfflineRemoteKeyRepository,
private val database: AppDatabase
) : OrderRepository {
override fun getAllOrders(): Flow<PagingData<OrderWithUser>> {
val pagingSourceFactory = { dbOrderRepository.getAllOrdersPagingSource() }
@OptIn(ExperimentalPagingApi::class)
return Pager(
config = PagingConfig(
pageSize = AppContainer.LIMIT,
enablePlaceholders = false
),
remoteMediator = OrderRemoteMediator(
service,
dbOrderRepository,
dbUserRepository,
dbRemoteKeyRepository,
database,
),
pagingSourceFactory = pagingSourceFactory
).flow
}
override suspend fun getOrder(uid: Int): List<SessionFromOrder> {
val order = service.getOrder(uid)
dbOrderSessionRepository.deleteOrderSessions(uid)
order.sessions.map {
dbOrderSessionRepository.insertOrderSession(
OrderSessionCrossRef(
uid,
it.id,
it.frozenPrice,
it.count
)
)
}
return order.sessions.map { x -> x.toSessionFromOrder() }
}
override suspend fun insertOrder(order: Order): Long {
return dbOrderRepository.insertOrder(service.createOrder(order.toOrderRemote()).toOrder())
}
}

View File

@ -0,0 +1,38 @@
package com.example.myapplication.api.ordersession
import com.example.myapplication.api.MyServerService
import com.example.myapplication.api.session.SessionFromOrderRemote
import com.example.myapplication.database.entities.model.OrderSessionCrossRef
import com.example.myapplication.database.entities.repository.OfflineOrderSessionRepository
import com.example.myapplication.database.entities.repository.OrderSessionRepository
class RestOrderSessionRepository(
private val service: MyServerService,
private val dbOrderSessionRepository: OfflineOrderSessionRepository
) : OrderSessionRepository {
override suspend fun insertOrderSession(orderSessionCrossRef: OrderSessionCrossRef) {
val orderRemote = service.getOrder(orderSessionCrossRef.orderId)
val session = service.getSession(orderSessionCrossRef.sessionId)
val sessionFromOrder = SessionFromOrderRemote(
session.id,
session.dateTime,
session.price,
orderSessionCrossRef.count,
session.cinemaId,
session.cinema
)
orderRemote.sessions = orderRemote.sessions.toMutableList().apply {
add(sessionFromOrder)
}
service.updateOrder(orderSessionCrossRef.orderId, orderRemote)
dbOrderSessionRepository.insertOrderSession(orderSessionCrossRef)
}
override suspend fun updateOrderSession(orderSessionCrossRef: OrderSessionCrossRef) {
}
override suspend fun deleteOrderSession(orderSessionCrossRef: OrderSessionCrossRef) {
}
}

View File

@ -0,0 +1,14 @@
package com.example.myapplication.api.session
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ReportRemote(
@SerialName("report_data")
val reportData: List<SessionFromReportRemote>,
@SerialName("total_revenue")
val totalRevenue: Double = 0.0,
@SerialName("total_purchased_tickets")
val totalPurchasedTickets: Int = 0
)

View File

@ -0,0 +1,52 @@
package com.example.myapplication.api.session
import com.example.myapplication.api.MyServerService
import com.example.myapplication.database.entities.model.Session
import com.example.myapplication.database.entities.repository.OfflineOrderSessionRepository
import com.example.myapplication.database.entities.repository.OfflineSessionRepository
import com.example.myapplication.database.entities.repository.OfflineUserSessionRepository
import com.example.myapplication.database.entities.repository.SessionRepository
import java.util.Date
class RestSessionRepository(
private val service: MyServerService,
private val dbSessionRepository: OfflineSessionRepository,
private val dbUserSessionRepository: OfflineUserSessionRepository,
private val dbOrderSessionRepository: OfflineOrderSessionRepository,
) : SessionRepository {
override suspend fun getSession(uid: Int): Session {
return service.getSession(uid).toSession()
}
override suspend fun insertSession(session: Session) {
dbSessionRepository.insertSession(
service.createSession(session.toSessionRemote()).toSession()
)
}
override suspend fun updateSession(session: Session) {
dbSessionRepository.updateSession(
service.updateSession(
session.uid,
session.toSessionRemote()
).toSession()
)
}
override suspend fun deleteSession(session: Session) {
val cart = service.getUsersSessions()
cart.forEach { userSessionRemote ->
if (userSessionRemote.session.id == session.uid) {
service.deleteUserSession(userSessionRemote.id)
}
}
service.deleteSession(session.uid)
dbUserSessionRepository.deleteSessionsByUid(session.uid)
dbOrderSessionRepository.deleteSessionsByUid(session.uid)
dbSessionRepository.deleteSession(session)
}
suspend fun getReport(startDate: Date, endDate: Date): ReportRemote {
return service.getReport(startDate, endDate)
}
}

View File

@ -0,0 +1,25 @@
package com.example.myapplication.api.session
import com.example.myapplication.database.entities.model.SessionFromCinema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import org.threeten.bp.LocalDateTime
@Serializable
class SessionFromCinemaRemote(
val id: Int = 0,
@Contextual
val dateTime: LocalDateTime = LocalDateTime.MIN,
val price: Double = 0.0,
val maxCount: Int = 0,
val availableCount: Int = 0,
val cinemaId: Int = 0,
)
fun SessionFromCinemaRemote.toSessionFromCinema(): SessionFromCinema = SessionFromCinema(
id,
dateTime,
price,
availableCount,
cinemaId
)

View File

@ -0,0 +1,24 @@
package com.example.myapplication.api.session
import com.example.myapplication.api.cinema.CinemaRemote
import com.example.myapplication.api.cinema.toCinema
import com.example.myapplication.database.entities.model.SessionFromOrder
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import org.threeten.bp.LocalDateTime
@Serializable
class SessionFromOrderRemote(
val id: Int = 0,
@Contextual
val dateTime: LocalDateTime = LocalDateTime.MIN,
val frozenPrice: Double = 0.0,
val count: Int = 0,
val cinemaId: Int = 0,
val cinema: CinemaRemote,
)
fun SessionFromOrderRemote.toSessionFromOrder(): SessionFromOrder =
SessionFromOrder(
id, dateTime, frozenPrice, count, cinemaId, cinema.toCinema()
)

View File

@ -0,0 +1,21 @@
package com.example.myapplication.api.session
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SessionFromReportRemote(
@SerialName("cinema_name")
val cinemaName: String = "",
@Contextual
@SerialName("current_ticket_date_time")
val ticketDateTime: org.threeten.bp.LocalDateTime,
@SerialName("current_ticket_price")
val ticketPrice: Double = 0.0,
@SerialName("max_ticket_quantity")
val ticketQuantity: Int = 0,
@SerialName("purchased_tickets")
val ticketsPurchased: Int = 0,
val revenue: Double = 0.0
)

View File

@ -0,0 +1,32 @@
package com.example.myapplication.api.session
import com.example.myapplication.database.entities.model.Session
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import org.threeten.bp.LocalDateTime
@Serializable
data class SessionRemote(
val id: Int = 0,
@Contextual
val dateTime: LocalDateTime,
val price: Double,
val maxCount: Int,
val cinemaId: Int = 0
)
fun SessionRemote.toSession(): Session = Session(
id,
dateTime,
price,
maxCount,
cinemaId
)
fun Session.toSessionRemote(): SessionRemote = SessionRemote(
uid,
dateTime,
price,
maxCount,
cinemaId
)

View File

@ -0,0 +1,26 @@
package com.example.myapplication.api.session
import com.example.myapplication.api.cinema.CinemaRemote
import com.example.myapplication.database.entities.model.Session
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import org.threeten.bp.LocalDateTime
@Serializable
data class SessionWithCinemaRemote(
val id: Int = 0,
@Contextual
val dateTime: LocalDateTime,
val price: Double,
val maxCount: Int,
val cinemaId: Int = 0,
val cinema: CinemaRemote,
)
fun SessionWithCinemaRemote.toSession(): Session = Session(
id,
dateTime,
price,
maxCount,
cinemaId
)

View File

@ -0,0 +1,72 @@
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.database.entities.model.SessionFromCart
import com.example.myapplication.database.entities.model.User
import com.example.myapplication.database.entities.model.UserSessionCrossRef
import com.example.myapplication.database.entities.repository.OfflineUserRepository
import com.example.myapplication.database.entities.repository.OfflineUserSessionRepository
import com.example.myapplication.database.entities.repository.UserRepository
import kotlinx.coroutines.flow.Flow
class RestUserRepository(
private val service: MyServerService,
private val dbUserRepository: OfflineUserRepository,
private val dbUserSessionRepository: OfflineUserSessionRepository,
) : UserRepository {
override fun getAllUsers(): Flow<List<User>> {
Log.d(RestUserRepository::class.simpleName, "Get users")
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)
cart.map { sessionFromCartRemote ->
dbUserSessionRepository.insertUserSession(
UserSessionCrossRef(
userId,
sessionFromCartRemote.sessionId,
sessionFromCartRemote.count
)
)
}
val orders = service.getOrders()
val sessions = cart.map { sessionFromCartRemote ->
SessionFromCart(
uid = sessionFromCartRemote.sessionId,
dateTime = sessionFromCartRemote.session.dateTime,
price = sessionFromCartRemote.session.price,
availableCount = sessionFromCartRemote.session.maxCount - orders
.flatMap
{ order ->
order.sessions.filter { session ->
session.id == sessionFromCartRemote.sessionId &&
session.cinemaId == sessionFromCartRemote.session.cinemaId &&
session.dateTime == sessionFromCartRemote.session.dateTime
}
}.sumOf { session -> session.count },
count = sessionFromCartRemote.count,
cinemaId = sessionFromCartRemote.session.cinemaId,
cinema = service.getCinema(sessionFromCartRemote.session.cinemaId).toCinema()
)
}
return sessions
}
override suspend fun insertUser(user: User) {
service.createUser(user.toUserRemote()).toUser()
}
override suspend fun updateUser(user: User) {
}
override suspend fun deleteUser(user: User) {
}
}

View File

@ -0,0 +1,27 @@
package com.example.myapplication.api.user
import com.example.myapplication.database.entities.model.User
import com.example.myapplication.database.entities.model.UserRole
import kotlinx.serialization.Serializable
@Serializable
data class UserRemote(
val id: Int = 0,
val login: String = "",
val password: String = "",
val role: Int = -1,
)
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,11 @@
package com.example.myapplication.api.user
import kotlinx.serialization.Serializable
@Serializable
data class UserSessionRemote(
val id: Int = 0,
val userId: Int = 0,
val sessionId: Int = 0,
var count: Int = 0,
)

View File

@ -0,0 +1,13 @@
package com.example.myapplication.api.user
import com.example.myapplication.api.session.SessionRemote
import kotlinx.serialization.Serializable
@Serializable
data class UserSessionWithSessionRemote(
val id: Int = 0,
val userId: Int = 0,
val sessionId: Int = 0,
val count: Int = 0,
val session: SessionRemote,
)

View File

@ -0,0 +1,65 @@
package com.example.myapplication.api.usersession
import com.example.myapplication.api.MyServerService
import com.example.myapplication.api.user.UserSessionRemote
import com.example.myapplication.database.entities.model.UserSessionCrossRef
import com.example.myapplication.database.entities.repository.OfflineUserSessionRepository
import com.example.myapplication.database.entities.repository.UserSessionRepository
class RestUserSessionRepository(
private val service: MyServerService,
private val dbUserSessionRepository: OfflineUserSessionRepository
) : UserSessionRepository {
override suspend fun insertUserSession(userSessionCrossRef: UserSessionCrossRef) {
val cartSessions = service.getUserCart(userSessionCrossRef.userId)
cartSessions.forEach { session ->
if (session.sessionId == userSessionCrossRef.sessionId)
return
}
service.createUserSession(
UserSessionRemote(
id = 0,
userId = userSessionCrossRef.userId,
sessionId = userSessionCrossRef.sessionId,
count = userSessionCrossRef.count
)
)
dbUserSessionRepository.insertUserSession(userSessionCrossRef)
}
override suspend fun updateUserSession(userSessionCrossRef: UserSessionCrossRef) {
val userSessionRemote = service.getUserSession(
userSessionCrossRef.userId,
userSessionCrossRef.sessionId
).first()
if (userSessionCrossRef.count <= 0) {
service.deleteUserSession(userSessionRemote.id)
dbUserSessionRepository.deleteUserSession(userSessionCrossRef)
return
}
userSessionRemote.count = userSessionCrossRef.count
service.updateUserCart(userSessionRemote.id, userSessionRemote)
dbUserSessionRepository.updateUserSession(userSessionCrossRef)
}
override suspend fun deleteUserSession(userSessionCrossRef: UserSessionCrossRef) {
val userSessionRemote = service.getUserSession(
userSessionCrossRef.userId,
userSessionCrossRef.sessionId
).firstOrNull() ?: return
service.deleteUserSession(userSessionRemote.id)
dbUserSessionRepository.deleteUserSession(userSessionCrossRef)
}
override suspend fun deleteUserSessions(userId: Int) {
val cart = service.getUserSessions(userId)
cart.forEach {
service.deleteUserSession(it.id)
}
dbUserSessionRepository.deleteUserSessions(userId)
}
override suspend fun deleteUserSessions(userSessionCrossRefs: List<UserSessionCrossRef>) {
userSessionCrossRefs.forEach { deleteUserSession(it) }
}
}

View File

@ -0,0 +1,33 @@
package com.example.myapplication.composeui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
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 scope = rememberCoroutineScope()
val login = dataStoreManager.getLogin().collectAsState(initial = "").value
LiveStore.user.value = viewModel.authUiState.user
fun synchronize() {
scope.launch {
if (login == "") {
LiveStore.user.value = null
return@launch
}
viewModel.findUserByLogin(login)
}
}
synchronize()
}

View File

@ -0,0 +1,27 @@
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.repository.UserRepository
class AuthenticatorViewModel(
private val userRepository: UserRepository
) : MyViewModel() {
var authUiState by mutableStateOf(AuthenticatorUiState())
private set
suspend fun findUserByLogin(login: String) {
runInScope(
actionSuccess = {
authUiState = AuthenticatorUiState(userRepository.getUser(login))
},
actionError = {
authUiState = AuthenticatorUiState()
}
)
}
}
data class AuthenticatorUiState(val user: User? = null)

View File

@ -0,0 +1,258 @@
package com.example.myapplication.composeui
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
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.Icon
import androidx.compose.material3.IconButton
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.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
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.api.ApiStatus
import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.database.entities.composeui.AppViewModelProvider
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 kotlinx.coroutines.launch
import org.threeten.bp.format.DateTimeFormatter
@Composable
fun Cart(
navController: NavController,
viewModel: CartViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val coroutineScope = rememberCoroutineScope()
val cartUiState = viewModel.cartUiState
LaunchedEffect(Unit) {
viewModel.refreshState()
}
when (viewModel.apiStatus) {
ApiStatus.DONE -> {
Cart(
cartUiState = cartUiState,
modifier = Modifier
.padding(all = 10.dp),
onChangeCount = { session: SessionFromCart, count: Int ->
coroutineScope.launch {
viewModel.updateFromCart(
session = Session(
uid = session.uid,
dateTime = session.dateTime,
price = session.price,
maxCount = 0,
cinemaId = session.cinemaId
), count = count, availableCount = session.availableCount
)
}
},
onAddToOrder = { sessions: List<SessionFromCart> ->
coroutineScope.launch {
viewModel.addToOrder(sessions = sessions)
}
},
onDelete = { session: SessionFromCart ->
coroutineScope.launch {
viewModel.removeFromCart(
session = Session(
uid = session.uid,
dateTime = session.dateTime,
price = session.price,
maxCount = 0,
cinemaId = session.cinemaId
)
)
}
}
)
}
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.navigate(Screen.Cart.route) }
)
}
}
@Composable
private fun Cart(
cartUiState: CartUiState,
modifier: Modifier,
onChangeCount: (SessionFromCart, Int) -> Unit,
onAddToOrder: (List<SessionFromCart>) -> Unit,
onDelete: (SessionFromCart) -> Unit
) {
LazyColumn(
modifier = modifier
) {
items(cartUiState.sessionList, key = { it.uid.toString() }) { session ->
SessionListItem(
session = session,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.secondary),
onChangeCount = onChangeCount,
onDelete = onDelete,
)
}
item {
Spacer(modifier = Modifier.height(48.dp))
}
}
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(6.dp)
.fillMaxWidth()
) { Text("Купить") }
}
}
}
@Composable
private fun SessionListItem(
session: SessionFromCart,
modifier: Modifier = Modifier,
onChangeCount: (SessionFromCart, Int) -> Unit,
onDelete: (SessionFromCart) -> Unit
) {
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val formattedDate = dateFormatter.format(session.dateTime)
Text(
text = formattedDate,
color = MaterialTheme.colorScheme.onBackground,
)
Column(modifier = modifier.fillMaxWidth()) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
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),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "${session.cinema.name}, ${session.cinema.year}\n" +
"Цена: ${session.price}\n" +
if (session.availableCount <= 0) "Недоступно" else "${session.count}/${session.availableCount}",
color = MaterialTheme.colorScheme.onSecondary
)
}
}
}
Row(
modifier = Modifier
.background(color = MaterialTheme.colorScheme.primary)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = { onDelete(session) }
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Удалить",
tint = MaterialTheme.colorScheme.onPrimary
)
}
Spacer(modifier = Modifier.weight(1F))
if (session.availableCount > 0) {
IconButton(
enabled = session.count != 1,
onClick = { onChangeCount(session, session.count - 1) }
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.minus),
contentDescription = "Уменьшить",
tint = MaterialTheme.colorScheme.onPrimary
)
}
Text(
text = "${session.count}",
color = MaterialTheme.colorScheme.onPrimary
)
IconButton(
enabled = session.count != session.availableCount,
onClick = {
onChangeCount(
session,
if (session.count != session.availableCount) session.count + 1 else session.count
)
}
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Увеличить",
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
}
}
}

View File

@ -0,0 +1,114 @@
package com.example.myapplication.composeui
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.room.Transaction
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
import com.example.myapplication.database.entities.model.SessionFromCart
import com.example.myapplication.database.entities.model.UserSessionCrossRef
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 org.threeten.bp.LocalDateTime
class CartViewModel(
private val userSessionRepository: UserSessionRepository,
private val orderRepository: OrderRepository,
private val orderSessionRepository: OrderSessionRepository,
private val userRepository: UserRepository,
) : MyViewModel() {
private var isLoading: Boolean = false
var cartUiState by mutableStateOf(CartUiState())
private set
suspend fun refreshState(needLoadingScreen: Boolean = true) {
val userId: Int = LiveStore.user.value?.uid ?: return
runInScope(
actionSuccess = {
cartUiState = CartUiState(userRepository.getCartByUser(userId))
}, actionError = {
cartUiState = CartUiState()
},
needLoadingScreen = needLoadingScreen
)
}
@Transaction
suspend fun addToOrder(sessions: List<SessionFromCart>) {
if (isLoading)
return
isLoading = true
val userId: Int = LiveStore.user.value?.uid ?: return
val cart = sessions.filter { it.availableCount != 0 }
if (cart.isEmpty())
return
runInScope(
actionSuccess = {
val orderId = orderRepository.insertOrder(Order(0, userId, LocalDateTime.now()))
cart.forEach { session ->
if (session.availableCount > 0) {
orderSessionRepository.insertOrderSession(
OrderSessionCrossRef(
orderId.toInt(),
session.uid,
session.price,
session.count
)
)
}
}
userSessionRepository.deleteUserSessions(cart.map {
UserSessionCrossRef(userId, it.uid, it.count)
})
refreshState()
}
)
}
suspend fun removeFromCart(session: Session, count: Int = 0) {
val userId: Int = LiveStore.user.value?.uid ?: return
runInScope(
actionSuccess = {
userSessionRepository.deleteUserSession(
UserSessionCrossRef(
userId,
session.uid,
count
)
)
refreshState()
}
)
}
suspend fun updateFromCart(session: Session, count: Int, availableCount: Int) {
val userId: Int = LiveStore.user.value?.uid ?: return
if (count == 0) {
removeFromCart(session, count)
return
}
if (count > availableCount)
return
runInScope(
actionSuccess = {
userSessionRepository.updateUserSession(
UserSessionCrossRef(
userId,
session.uid,
count
)
)
refreshState()
},
actionError = { },
needLoadingScreen = false
)
}
}
data class CartUiState(val sessionList: List<SessionFromCart> = listOf())

View File

@ -0,0 +1,50 @@
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 androidx.lifecycle.viewModelScope
import com.example.myapplication.api.ApiStatus
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException
open class MyViewModel : ViewModel() {
var apiStatus by mutableStateOf(ApiStatus.DONE)
private set
var apiError by mutableStateOf("")
private set
fun runInScope(
actionSuccess: suspend () -> Unit,
actionError: suspend () -> Unit,
needLoadingScreen: Boolean = true,
) {
viewModelScope.launch {
if (needLoadingScreen)
apiStatus = ApiStatus.LOADING
runCatching {
actionSuccess()
apiStatus = ApiStatus.DONE
apiError = ""
}.onFailure { e: Throwable ->
when (e) {
is IOException,
is HttpException -> {
actionError()
apiStatus = ApiStatus.ERROR
apiError = e.localizedMessage ?: e.toString()
}
else -> throw e
}
}
}
}
fun runInScope(actionSuccess: suspend () -> Unit) {
runInScope(actionSuccess, actionError = {})
}
}

View File

@ -0,0 +1,65 @@
package com.example.myapplication.composeui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import com.example.myapplication.R
@Composable
fun ErrorPlaceholder(message: String, onBack: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontSize = TextUnit(value = 20F, type = TextUnitType.Sp),
text = message,
color = Color(0xFFFF1744)
)
Spacer(modifier = Modifier.padding(bottom = 10.dp))
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { onBack() }
) {
Text(stringResource(id = R.string.back))
}
}
}
@Composable
fun LoadingPlaceholder() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontSize = TextUnit(value = 25F, type = TextUnitType.Sp),
text = stringResource(id = R.string.loading)
)
}
}

View File

@ -0,0 +1,153 @@
package com.example.myapplication.composeui
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DisplayMode
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.example.myapplication.api.ApiStatus
import com.example.myapplication.api.session.ReportRemote
import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.database.entities.composeui.AppViewModelProvider
import kotlinx.coroutines.launch
import org.threeten.bp.format.DateTimeFormatter
import java.util.Date
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Report(
navController: NavController,
viewModel: ReportViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val dateStateStart = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
val dateStateEnd = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
val coroutineScope = rememberCoroutineScope()
when (viewModel.apiStatus) {
ApiStatus.DONE -> {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.padding(all = 10.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
)
{
Text(
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 = "Конец периода",
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,
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Получить отчет")
}
Spacer(modifier = Modifier.height(16.dp))
CardScreen(report = viewModel.reportResultUiState.report)
}
}
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.navigate(Screen.Report.route) }
)
}
}
@Composable
fun CardScreen(report: ReportRemote?) {
if (report == null) return
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
report.reportData.forEach {
val (cinemaName, ticketDateTime, ticketPrice, ticketQuantity, ticketsPurchased, revenue) = it
Row(
modifier = Modifier
.fillMaxWidth()
.border(width = 1.dp, color = MaterialTheme.colorScheme.onBackground, shape = MaterialTheme.shapes.small)
) {
Column(
Modifier
.padding(16.dp)
.background(color = Color.Transparent)
) {
Text(text = "Фильм: $cinemaName")
Text(text = "Сеанс: ${dateFormatter.format(ticketDateTime)}")
Text(text = "Стоимость: $ticketPrice")
Text(text = "Максимальное кол-во билетов: $ticketQuantity")
Text(text = "Купили: $ticketsPurchased")
Text(text = "Выручка: $revenue")
}
}
Spacer(modifier = Modifier.height(16.dp))
}
Text(text = "Итого: ")
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.border(width = 1.dp, color = MaterialTheme.colorScheme.onBackground, shape = MaterialTheme.shapes.small)
) {
Column(
Modifier
.padding(16.dp)
.background(color = Color.Transparent)
) {
Text(text = "Купили: ${report.totalPurchasedTickets}")
Text(text = "Выручка: ${report.totalRevenue}")
}
}
Spacer(modifier = Modifier.height(16.dp))
}

View File

@ -0,0 +1,61 @@
package com.example.myapplication.composeui
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.example.myapplication.api.session.ReportRemote
import com.example.myapplication.api.session.RestSessionRepository
import java.util.Date
class ReportViewModel(private val serialRepository: RestSessionRepository) : MyViewModel() {
var reportUiState by mutableStateOf(ReportUiState())
private set
var reportResultUiState by mutableStateOf(ReportResultUiState())
private set
fun onUpdate(reportDetails: ReportDetails) {
reportUiState = ReportUiState(
reportDetails = reportDetails,
isEntryValid = validateInput(reportDetails)
)
}
private fun validateInput(uiState: ReportDetails = reportUiState.reportDetails): Boolean {
return with(uiState) {
startDate != Date(0)
&& endDate != Date(0)
&& startDate <= endDate
}
}
suspend fun getReport() {
if (validateInput()) {
runInScope(
actionSuccess = {
val temp = serialRepository.getReport(
reportUiState.reportDetails.startDate,
reportUiState.reportDetails.endDate
)
reportResultUiState = ReportResultUiState(temp)
}, actionError = {
reportResultUiState = ReportResultUiState()
}
)
}
}
}
data class ReportDetails(
val startDate: Date = Date(0),
val endDate: Date = Date(0)
)
data class ReportUiState(
val reportDetails: ReportDetails = ReportDetails(),
val isEntryValid: Boolean = false
)
data class ReportResultUiState(
val report: ReportRemote? = null
)

View File

@ -0,0 +1,63 @@
package com.example.myapplication.composeui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun Search(
initValue: String,
onDone: (String) -> Unit,
modifier: Modifier
) {
val maxLength = 20
val (value, setValue) = remember { mutableStateOf(initValue) }
fun clear() {
setValue("")
onDone("")
}
fun handleChange(newValue: String) {
if (newValue.length > maxLength) {
return
} else if (newValue.isEmpty()) {
clear()
}
setValue(newValue)
}
fun handleDone() {
onDone(value)
}
BasicTextField(
value = value,
onValueChange = { handleChange(it) },
modifier = modifier,
singleLine = true,
keyboardActions = KeyboardActions(
onDone = { handleDone() }
)
)
Spacer(modifier = Modifier.width(16.dp))
Icon(
imageVector = Icons.Default.Search,
contentDescription = null,
modifier = Modifier
.size(30.dp)
.clickable { handleDone() },
tint = MaterialTheme.colorScheme.secondary,
)
}

View File

@ -0,0 +1,218 @@
package com.example.myapplication.composeui.navigation
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.example.myapplication.LiveStore
import com.example.myapplication.composeui.Cart
import com.example.myapplication.composeui.Report
import com.example.myapplication.composeui.Search
import com.example.myapplication.database.entities.composeui.CinemaList
import com.example.myapplication.database.entities.composeui.CinemaView
import com.example.myapplication.database.entities.composeui.OrderList
import com.example.myapplication.database.entities.composeui.OrderView
import com.example.myapplication.database.entities.composeui.UserProfile
import com.example.myapplication.database.entities.composeui.edit.CinemaEdit
import com.example.myapplication.database.entities.composeui.edit.SessionEdit
import com.example.myapplication.database.entities.model.UserRole
import com.example.myapplication.datastore.DataStoreManager
@Composable
fun Topbar(
navController: NavHostController,
currentScreen: Screen?
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(color = MaterialTheme.colorScheme.primary)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (
navController.previousBackStackEntry != null
&& (currentScreen == null || !currentScreen.showInBottomBar)
) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = null,
modifier = Modifier
.size(30.dp)
.clickable { navController.navigateUp() },
tint = MaterialTheme.colorScheme.secondary
)
} else
Icon(
imageVector = Icons.Default.Person,
contentDescription = null,
modifier = Modifier
.size(30.dp)
.clickable { navController.navigate(Screen.UserProfile.route) },
tint = MaterialTheme.colorScheme.secondary
)
Spacer(modifier = Modifier.width(16.dp))
Search(
initValue = LiveStore.searchRequest.value ?: "",
onDone = {
navController.navigate(Screen.CinemaList.route)
LiveStore.searchRequest.value = it
},
modifier = Modifier
.weight(1f)
.height(36.dp)
.background(
color = MaterialTheme.colorScheme.onPrimary,
RoundedCornerShape(18.dp)
)
.padding(start = 13.dp, top = 8.dp)
)
}
}
}
@Composable
fun Navbar(
navController: NavHostController,
currentDestination: NavDestination?,
modifier: Modifier = Modifier
) {
val user = LiveStore.user.observeAsState()
NavigationBar(modifier = modifier, containerColor = MaterialTheme.colorScheme.primary) {
Screen.bottomBarItems.forEach { screen ->
if (screen.route != Screen.Report.route || user.value?.role == UserRole.ADMIN) {
NavigationBarItem(
icon = {
Icon(
screen.icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary
)
},
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 = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
}
@Composable
fun Navhost(
navController: NavHostController,
innerPadding: PaddingValues,
isDarkTheme: MutableState<Boolean>,
dataStore: DataStoreManager,
modifier: Modifier = Modifier,
) {
NavHost(
navController,
startDestination = Screen.CinemaList.route,
modifier.padding(innerPadding)
) {
composable(Screen.CinemaList.route) { CinemaList(navController) }
composable(Screen.OrderList.route) { OrderList(navController) }
composable(Screen.Cart.route) { Cart(navController) }
composable(Screen.UserProfile.route) { UserProfile(isDarkTheme, dataStore) }
composable(
Screen.CinemaEdit.route,
arguments = listOf(navArgument("id") { type = NavType.IntType })
) {
CinemaEdit(navController)
}
composable(
Screen.SessionEdit.route,
arguments = listOf(navArgument("id") { type = NavType.IntType },
navArgument("cinemaId") { type = NavType.IntType })
) {
SessionEdit(navController)
}
composable(
Screen.CinemaView.route,
arguments = listOf(navArgument("id") { type = NavType.IntType })
) { backStackEntry ->
backStackEntry.arguments?.let { CinemaView(navController) }
}
composable(
Screen.OrderView.route,
arguments = listOf(navArgument("id") { type = NavType.IntType })
) { backStackEntry ->
backStackEntry.arguments?.let { OrderView(navController) }
}
composable(Screen.Report.route) { Report(navController) }
}
}
@Composable
fun MainNavbar(
isDarkTheme: MutableState<Boolean>,
dataStoreManager: DataStoreManager
) {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val currentScreen = currentDestination?.route?.let { Screen.getItem(it) }
Scaffold(
topBar = {
Topbar(navController, currentScreen)
},
bottomBar = {
if (currentScreen == null || currentScreen.showInBottomBar) {
Navbar(navController, currentDestination)
}
}
) { innerPadding ->
Navhost(navController, innerPadding, isDarkTheme, dataStoreManager)
}
}

View File

@ -0,0 +1,59 @@
package com.example.myapplication.composeui.navigation
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.ShoppingCart
import androidx.compose.ui.graphics.vector.ImageVector
import com.example.myapplication.R
enum class Screen(
val route: String,
@StringRes val resourceId: Int,
val icon: ImageVector = Icons.Filled.Favorite,
val showInBottomBar: Boolean = true
) {
CinemaList(
"Cinema-list", R.string.Cinema_main_title, Icons.Filled.Home
),
CinemaEdit(
"Cinema-edit/{id}", R.string.Cinema_view_title, showInBottomBar = false
),
SessionEdit(
"Session-edit/{id}/{cinemaId}", R.string.Session_view_title, showInBottomBar = false
),
CinemaView(
"Cinema-view/{id}", R.string.Cinema_view_title, showInBottomBar = false
),
Cart(
"cart", R.string.Cart_title, Icons.Filled.ShoppingCart
),
OrderList(
"Order-list", R.string.Order_title, Icons.Filled.List
),
OrderView(
"Order-view/{id}", R.string.Order_view_title, showInBottomBar = false
),
UserProfile(
"User-profile", R.string.Profile_title, showInBottomBar = false
),
Report(
"Report", R.string.Report_title,
);
companion object {
val bottomBarItems = listOf(
CinemaList,
Cart,
OrderList,
Report
)
fun getItem(route: String): Screen? {
val findRoute = route.split("/").first()
return values().find { value -> value.route.startsWith(findRoute) }
}
}
}

View File

@ -0,0 +1,101 @@
package com.example.myapplication.database
import android.content.Context
import com.example.myapplication.api.MyServerService
import com.example.myapplication.api.cinema.RestCinemaRepository
import com.example.myapplication.api.order.RestOrderRepository
import com.example.myapplication.api.ordersession.RestOrderSessionRepository
import com.example.myapplication.api.session.RestSessionRepository
import com.example.myapplication.api.user.RestUserRepository
import com.example.myapplication.api.usersession.RestUserSessionRepository
import com.example.myapplication.database.entities.repository.OfflineCinemaRepository
import com.example.myapplication.database.entities.repository.OfflineOrderRepository
import com.example.myapplication.database.entities.repository.OfflineOrderSessionRepository
import com.example.myapplication.database.entities.repository.OfflineSessionRepository
import com.example.myapplication.database.entities.repository.OfflineUserRepository
import com.example.myapplication.database.entities.repository.OfflineUserSessionRepository
import com.example.myapplication.database.remotekeys.repository.OfflineRemoteKeyRepository
interface AppContainer {
val cinemaRestRepository: RestCinemaRepository
val sessionRestRepository: RestSessionRepository
val userRestRepository: RestUserRepository
val orderRestRepository: RestOrderRepository
val orderSessionRestRepository: RestOrderSessionRepository
val userSessionRestRepository: RestUserSessionRepository
companion object {
const val TIMEOUT = 5000L
const val LIMIT = 10
}
}
class AppDataContainer(private val context: Context) : AppContainer {
private val cinemaRepository: OfflineCinemaRepository by lazy {
OfflineCinemaRepository(AppDatabase.getInstance(context).cinemaDao())
}
private val orderRepository: OfflineOrderRepository by lazy {
OfflineOrderRepository(AppDatabase.getInstance(context).orderDao())
}
private val orderSessionRepository: OfflineOrderSessionRepository by lazy {
OfflineOrderSessionRepository(AppDatabase.getInstance(context).orderSessionCrossRefDao())
}
private val sessionRepository: OfflineSessionRepository by lazy {
OfflineSessionRepository(AppDatabase.getInstance(context).sessionDao())
}
private val userRepository: OfflineUserRepository by lazy {
OfflineUserRepository(AppDatabase.getInstance(context).userDao())
}
private val userSessionRepository: OfflineUserSessionRepository by lazy {
OfflineUserSessionRepository(AppDatabase.getInstance(context).userSessionCrossRefDao())
}
private val remoteKeyRepository: OfflineRemoteKeyRepository by lazy {
OfflineRemoteKeyRepository(AppDatabase.getInstance(context).remoteKeysDao())
}
override val cinemaRestRepository: RestCinemaRepository by lazy {
RestCinemaRepository(
MyServerService.getInstance(),
cinemaRepository,
sessionRepository,
remoteKeyRepository,
AppDatabase.getInstance(context)
)
}
override val sessionRestRepository: RestSessionRepository by lazy {
RestSessionRepository(
MyServerService.getInstance(),
sessionRepository,
userSessionRepository,
orderSessionRepository,
)
}
override val userRestRepository: RestUserRepository by lazy {
RestUserRepository(
MyServerService.getInstance(),
userRepository,
userSessionRepository,
)
}
override val orderRestRepository: RestOrderRepository by lazy {
RestOrderRepository(
MyServerService.getInstance(),
orderRepository,
userRepository,
orderSessionRepository,
remoteKeyRepository,
AppDatabase.getInstance(context)
)
}
override val userSessionRestRepository: RestUserSessionRepository by lazy {
RestUserSessionRepository(
MyServerService.getInstance(),
userSessionRepository,
)
}
override val orderSessionRestRepository: RestOrderSessionRepository by lazy {
RestOrderSessionRepository(
MyServerService.getInstance(),
orderSessionRepository,
)
}
}

View File

@ -0,0 +1,195 @@
package com.example.myapplication.database
import android.content.Context
import android.graphics.Bitmap
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.sqlite.db.SupportSQLiteDatabase
import com.example.myapplication.database.entities.dao.CinemaDao
import com.example.myapplication.database.entities.dao.OrderDao
import com.example.myapplication.database.entities.dao.OrderSessionCrossRefDao
import com.example.myapplication.database.entities.dao.SessionDao
import com.example.myapplication.database.entities.dao.UserDao
import com.example.myapplication.database.entities.dao.UserSessionCrossRefDao
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.LocalDateTimeConverter
import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.model.OrderSessionCrossRef
import com.example.myapplication.database.entities.model.Session
import com.example.myapplication.database.entities.model.User
import com.example.myapplication.database.entities.model.UserSessionCrossRef
import com.example.myapplication.database.remotekeys.dao.RemoteKeysDao
import com.example.myapplication.database.remotekeys.model.RemoteKeys
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
@Database(
entities = [
Cinema::class,
Session::class,
Order::class,
OrderSessionCrossRef::class,
User::class,
UserSessionCrossRef::class,
RemoteKeys::class
],
version = 1,
exportSchema = false
)
@TypeConverters(LocalDateTimeConverter::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun cinemaDao(): CinemaDao
abstract fun sessionDao(): SessionDao
abstract fun orderDao(): OrderDao
abstract fun orderSessionCrossRefDao(): OrderSessionCrossRefDao
abstract fun userDao(): UserDao
abstract fun userSessionCrossRefDao(): UserSessionCrossRefDao
abstract fun remoteKeysDao(): RemoteKeysDao
companion object {
private const val DB_NAME: String = "pmy-db"
@Volatile
private var INSTANCE: AppDatabase? = null
private suspend fun populateDatabase() {
INSTANCE?.let { database ->
// Users
val userDao = database.userDao()
val user1 = User(1, "login", "password")
userDao.insert(user1)
/*// Cinemas
val cinemaDao = database.cinemaDao()
val cinema1 =
Cinema(1, "a", "Desc1", createColoredImage(android.graphics.Color.BLUE), 2023)
val cinema2 =
Cinema(2, "b", "Desc2", createColoredImage(android.graphics.Color.GREEN), 2023)
val cinema3 =
Cinema(3, "c", "Desc3", createColoredImage(android.graphics.Color.RED), 2023)
val cinema4 =
Cinema(4, "d", "Desc4", createColoredImage(android.graphics.Color.CYAN), 2023)
cinemaDao.insert(cinema1)
cinemaDao.insert(cinema2)
cinemaDao.insert(cinema3)
cinemaDao.insert(cinema4)
for (i in 5..20) {
val cinema = Cinema(
uid = i,
name = generateCinemaName(i),
description = "Description $i",
image = createColoredImage(getRandomColorInt()),
year = 2023
)
cinemaDao.insert(cinema)
}
// Orders
val orderDao = database.orderDao()
val order1 = Order(1, 1)
val order2 = Order(2, 1)
val order3 = Order(3, 1)
val order4 = Order(4, 1)
orderDao.insert(order1)
orderDao.insert(order2)
orderDao.insert(order3)
orderDao.insert(order4)
// Sessions
val sessionDao = database.sessionDao()
val session1 = Session(1, LocalDateTime.now(), 150.0, 120, cinema1.uid)
val session2 = Session(2, LocalDateTime.now(), 200.0, 110, cinema2.uid)
val session3 = Session(3, LocalDateTime.now(), 300.0, 100, cinema3.uid)
val session4 = Session(4, LocalDateTime.now(), 450.0, 200, cinema1.uid)
sessionDao.insert(session1)
sessionDao.insert(session2)
sessionDao.insert(session3)
sessionDao.insert(session4)
// OrderSessionCrossRef для связи заказов с сеансами
val orderSessionCrossRefDao = database.orderSessionCrossRefDao()
if (session1.uid != null && session2.uid != null && session3.uid != null) {
val orderSessionCrossRef1 =
OrderSessionCrossRef(order1.uid, session3.uid, 150.0, 5)
val orderSessionCrossRef2 =
OrderSessionCrossRef(order1.uid, session2.uid, 300.0, 10)
val orderSessionCrossRef3 =
OrderSessionCrossRef(order2.uid, session2.uid, 350.0, 6)
val orderSessionCrossRef4 =
OrderSessionCrossRef(order3.uid, session1.uid, 250.0, 10)
val orderSessionCrossRef5 =
OrderSessionCrossRef(order3.uid, session3.uid, 150.0, 16)
val orderSessionCrossRef6 =
OrderSessionCrossRef(order4.uid, session3.uid, 150.0, 2)
orderSessionCrossRefDao.insert(orderSessionCrossRef1)
orderSessionCrossRefDao.insert(orderSessionCrossRef2)
orderSessionCrossRefDao.insert(orderSessionCrossRef3)
orderSessionCrossRefDao.insert(orderSessionCrossRef4)
orderSessionCrossRefDao.insert(orderSessionCrossRef5)
orderSessionCrossRefDao.insert(orderSessionCrossRef6)
}
// UserSessions
val userSessionCrossRefDao = database.userSessionCrossRefDao()
val userSessionCrossRef1 = UserSessionCrossRef(1, 1, 5)
val userSessionCrossRef2 = UserSessionCrossRef(1, 3, 15)
userSessionCrossRefDao.insert(userSessionCrossRef1)
userSessionCrossRefDao.insert(userSessionCrossRef2)*/
}
}
fun getInstance(appContext: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
Room.databaseBuilder(
appContext,
AppDatabase::class.java,
DB_NAME
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
CoroutineScope(Dispatchers.IO).launch {
populateDatabase()
}
}
})
.build()
.also { INSTANCE = it }
}
}
private fun createColoredImage(color: Int): ByteArray {
val width = 100
val height = 100
val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bmp.eraseColor(color)
val stream = ByteArrayOutputStream()
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream)
return stream.toByteArray()
}
private fun getRandomColorInt(): Int {
val red = (0..255).random()
val green = (0..255).random()
val blue = (0..255).random()
return (0xFF shl 24) or (red shl 16) or (green shl 8) or blue
}
private fun generateCinemaName(index: Int): String {
val base = 'a'.code
val alphabetSize = 26
val sb = StringBuilder()
var remainder = index
do {
val letter = (remainder % alphabetSize + base).toChar()
sb.insert(0, letter)
remainder /= alphabetSize
} while (remainder > 0)
return sb.toString()
}
}
}

View File

@ -0,0 +1,74 @@
package com.example.myapplication.database.entities.composeui
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.createSavedStateHandle
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.AuthenticatorViewModel
import com.example.myapplication.composeui.CartViewModel
import com.example.myapplication.composeui.ReportViewModel
import com.example.myapplication.database.entities.composeui.edit.CinemaEditViewModel
import com.example.myapplication.database.entities.composeui.edit.SessionEditViewModel
object AppViewModelProvider {
val Factory = viewModelFactory {
initializer {
CinemaListViewModel(cinemaApplication().container.cinemaRestRepository)
}
initializer {
CinemaEditViewModel(
this.createSavedStateHandle(),
cinemaApplication().container.cinemaRestRepository
)
}
initializer {
CinemaViewModel(
this.createSavedStateHandle(),
cinemaApplication().container.cinemaRestRepository,
cinemaApplication().container.sessionRestRepository,
cinemaApplication().container.userSessionRestRepository,
)
}
initializer {
SessionEditViewModel(
this.createSavedStateHandle(),
cinemaApplication().container.sessionRestRepository,
)
}
initializer {
CartViewModel(
cinemaApplication().container.userSessionRestRepository,
cinemaApplication().container.orderRestRepository,
cinemaApplication().container.orderSessionRestRepository,
cinemaApplication().container.userRestRepository,
)
}
initializer {
OrderListViewModel(
cinemaApplication().container.orderRestRepository,
)
}
initializer {
OrderViewModel(
this.createSavedStateHandle(),
cinemaApplication().container.orderRestRepository,
)
}
initializer {
UserProfileViewModel(
cinemaApplication().container.userRestRepository,
)
}
initializer {
AuthenticatorViewModel(cinemaApplication().container.userRestRepository)
}
initializer {
ReportViewModel(cinemaApplication().container.sessionRestRepository)
}
}
}
fun CreationExtras.cinemaApplication(): CinemaApplication =
(this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as CinemaApplication)

View File

@ -0,0 +1,201 @@
package com.example.myapplication.database.entities.composeui
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
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
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.unit.dp
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
fun CinemaList(
navController: NavController,
viewModel: CinemaListViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val coroutineScope = rememberCoroutineScope()
val cinemaPagingItems = viewModel.cinemaPagingFlow.flow.collectAsLazyPagingItems()
val user = LiveStore.user.observeAsState()
val searchPattern = LiveStore.searchRequest.observeAsState("")
LaunchedEffect(searchPattern.value) {
viewModel.refresh()
}
Scaffold(
topBar = {},
floatingActionButton = {
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 ->
CinemaList(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize(),
pagingCinema = cinemaPagingItems,
onClick = { uid: Int ->
val route = Screen.CinemaView.route.replace("{id}", uid.toString())
navController.navigate(route)
},
onDeleteClick = { cinema: Cinema ->
coroutineScope.launch {
viewModel.deleteCinema(cinema)
}
},
onEditClick = { uid: Int ->
val route = Screen.CinemaEdit.route.replace("{id}", uid.toString())
navController.navigate(route)
},
)
}
}
@Composable
private fun CinemaList(
modifier: Modifier = Modifier,
pagingCinema: LazyPagingItems<Cinema>,
onClick: (uid: Int) -> Unit,
onDeleteClick: (cinema: Cinema) -> Unit,
onEditClick: (cinema: Int) -> Unit
) {
Column(
modifier = modifier
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(all = 10.dp)
) {
items(pagingCinema.itemCount) { index ->
val cinema = pagingCinema[index]
if (cinema != null) {
CinemaListItem(
cinema = cinema,
modifier = Modifier
.padding(vertical = 7.dp)
.clickable { onClick(cinema.uid) }
.background(
color = MaterialTheme.colorScheme.secondary,
shape = RoundedCornerShape(16.dp)
),
onDeleteClick = onDeleteClick,
onEditClick = onEditClick,
)
}
}
}
}
}
@Composable
private fun CinemaListItem(
cinema: Cinema,
modifier: Modifier = Modifier,
onDeleteClick: (cinema: Cinema) -> Unit,
onEditClick: (cinema: Int) -> Unit
) {
val user = LiveStore.user.observeAsState()
Box(
modifier = modifier
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (cinema.image != null)
Image(
bitmap = BitmapFactory.decodeByteArray(
cinema.image,
0,
cinema.image.size
).asImageBitmap(),
contentDescription = null,
modifier = Modifier
.size(90.dp)
.padding(4.dp)
)
Text(
"${cinema.name}, ${cinema.year}",
color = 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

@ -0,0 +1,39 @@
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.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.example.myapplication.LiveStore
import com.example.myapplication.composeui.MyViewModel
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.repository.CinemaRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
class CinemaListViewModel(
private val cinemaRepository: CinemaRepository
) : MyViewModel() {
var cinemaPagingFlow by mutableStateOf(CinemaPagingFlowState())
private set
fun refresh() {
val name = "%${LiveStore.searchRequest.value}%"
runInScope(actionSuccess = {
val pagingSource = cinemaRepository.getAllCinemas(name)
cinemaPagingFlow = CinemaPagingFlowState(pagingSource.cachedIn(viewModelScope))
})
}
suspend fun deleteCinema(cinema: Cinema) {
runInScope(actionSuccess = {
cinemaRepository.deleteCinema(cinema)
})
}
}
data class CinemaPagingFlowState(
val flow: Flow<PagingData<Cinema>> = emptyFlow(),
)

View File

@ -0,0 +1,156 @@
package com.example.myapplication.database.entities.composeui
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
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.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.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.UserRole
@Composable
fun CinemaView(
navController: NavController,
viewModel: CinemaViewModel = viewModel(factory = AppViewModelProvider.Factory),
) {
val cinemaUiState = viewModel.cinemaUiState
val user = LiveStore.user.observeAsState()
LaunchedEffect(Unit) {
viewModel.refreshState()
}
when (viewModel.apiStatus) {
ApiStatus.DONE -> {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
) {
val cinema: Cinema? = cinemaUiState.cinemaWithSessions?.cinema
if (cinema != null) {
Box(
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.secondary,
shape = RoundedCornerShape(16.dp)
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.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)
)
}
if (cinema.image != null)
Image(
bitmap = BitmapFactory.decodeByteArray(
cinema.image,
0,
cinema.image.size
).asImageBitmap(),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(4.dp)
)
Text(
text = cinema.description,
color = MaterialTheme.colorScheme.onSecondary
)
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
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)
}
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "Добавить сеанс",
)
}
}
}
if (cinemaUiState.cinemaWithSessions != null) {
SessionList(viewModel, navController)
}
}
}
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.popBackStack() }
)
}
}

View File

@ -0,0 +1,66 @@
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 com.example.myapplication.LiveStore
import com.example.myapplication.composeui.MyViewModel
import com.example.myapplication.database.entities.model.CinemaWithSessions
import com.example.myapplication.database.entities.model.Session
import com.example.myapplication.database.entities.model.SessionFromCinema
import com.example.myapplication.database.entities.model.UserSessionCrossRef
import com.example.myapplication.database.entities.repository.CinemaRepository
import com.example.myapplication.database.entities.repository.SessionRepository
import com.example.myapplication.database.entities.repository.UserSessionRepository
class CinemaViewModel(
savedStateHandle: SavedStateHandle, private val cinemaRepository: CinemaRepository,
private val sessionRepository: SessionRepository,
private val userSessionRepository: UserSessionRepository
) : MyViewModel() {
private val cinemaUid: Int = checkNotNull(savedStateHandle["id"])
var cinemaUiState by mutableStateOf(CinemaUiState())
private set
suspend fun refreshState() {
if (cinemaUid > 0) {
runInScope(actionSuccess = {
cinemaUiState = CinemaUiState(cinemaRepository.getCinema(cinemaUid))
}, actionError = {
cinemaUiState = CinemaUiState()
})
}
}
suspend fun deleteSession(session: SessionFromCinema) {
runInScope(actionSuccess = {
sessionRepository.deleteSession(
Session(
uid = session.uid,
dateTime = session.dateTime,
price = session.price,
maxCount = 0,
cinemaId = 0
)
)
refreshState()
})
}
suspend fun addSessionInCart(sessionId: Int, count: Int = 1) {
val userId: Int = LiveStore.user.value?.uid ?: return
runInScope(actionSuccess = {
userSessionRepository.insertUserSession(
UserSessionCrossRef(
userId,
sessionId,
count
)
)
}, actionError = {}, needLoadingScreen = false)
}
}
data class CinemaUiState(val cinemaWithSessions: CinemaWithSessions? = null)

View File

@ -0,0 +1,99 @@
package com.example.myapplication.database.entities.composeui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemContentType
import androidx.paging.compose.itemKey
import com.example.myapplication.LiveStore
import com.example.myapplication.composeui.navigation.Screen
import com.example.myapplication.database.entities.model.UserRole
import com.example.myapplication.ui.theme.PmudemoTheme
import org.threeten.bp.format.DateTimeFormatter
@Composable
fun OrderList(
navController: NavController?,
viewModel: OrderListViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val ordersUiState = viewModel.orderListUiState.collectAsLazyPagingItems()
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(all = 10.dp)
) {
items(
count = ordersUiState.itemCount,
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()
.padding(all = 10.dp)
.clickable { navController?.navigate(orderId) }
.background(
color = MaterialTheme.colorScheme.secondary,
shape = RoundedCornerShape(16.dp)
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (LiveStore.user.value?.role == UserRole.ADMIN)
Text(
"Заказ №${order.uid}, ${formattedDate}\n" +
"Пользователь: ${order.user?.login ?: "Неизвестно"}",
color = MaterialTheme.colorScheme.onSecondary
)
else
Text(
"Заказ №${order.uid}, $formattedDate",
color = MaterialTheme.colorScheme.onSecondary
)
}
}
}
}
}
@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 OrderListPreview() {
PmudemoTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
OrderList(navController = null)
}
}
}

View File

@ -0,0 +1,14 @@
package com.example.myapplication.database.entities.composeui
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.OrderWithUser
import com.example.myapplication.database.entities.repository.OrderRepository
import kotlinx.coroutines.flow.Flow
class OrderListViewModel(
orderRepository: OrderRepository
) : MyViewModel() {
val orderListUiState: Flow<PagingData<OrderWithUser>> = orderRepository.getAllOrders()
}

View File

@ -0,0 +1,111 @@
package com.example.myapplication.database.entities.composeui
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
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
@Composable
fun OrderView(
navController: NavController,
viewModel: OrderViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val orderUiState = viewModel.orderUiState
LaunchedEffect(Unit) {
viewModel.refreshState()
}
when (viewModel.apiStatus) {
ApiStatus.DONE -> {
LazyColumn(
modifier = Modifier
.padding(10.dp)
) {
items(orderUiState.sessionList) { session ->
val count = remember { mutableIntStateOf(session.count) }
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val formattedDate = dateFormatter.format(session.dateTime)
Text(
text = formattedDate,
color = MaterialTheme.colorScheme.onBackground,
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.secondary)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
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.intValue}",
color = MaterialTheme.colorScheme.onSecondary
)
}
}
}
}
}
}
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.popBackStack() }
)
}
}

View File

@ -0,0 +1,29 @@
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 com.example.myapplication.composeui.MyViewModel
import com.example.myapplication.database.entities.model.SessionFromOrder
import com.example.myapplication.database.entities.repository.OrderRepository
class OrderViewModel(
savedStateHandle: SavedStateHandle,
private val orderRepository: OrderRepository
) : MyViewModel() {
private val orderUid: Int = checkNotNull(savedStateHandle["id"])
var orderUiState by mutableStateOf(OrderUiState())
private set
suspend fun refreshState() {
runInScope(actionSuccess = {
orderUiState = OrderUiState(orderRepository.getOrder(orderUid))
}, actionError = {
orderUiState = OrderUiState()
})
}
}
data class OrderUiState(val sessionList: List<SessionFromOrder> = listOf())

View File

@ -0,0 +1,148 @@
package com.example.myapplication.database.entities.composeui
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.ShoppingCart
import androidx.compose.material3.Icon
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
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
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
@Composable
fun SessionList(
cinemaWithSessionsViewModel: CinemaViewModel,
navController: NavController
) {
val coroutineScope = rememberCoroutineScope()
val cinemaWithSessions = cinemaWithSessionsViewModel.cinemaUiState.cinemaWithSessions!!
val user = LiveStore.user.observeAsState()
LazyColumn {
if (cinemaWithSessions.sessions.isEmpty()) {
item {
Text(
text = stringResource(R.string.Session_empty_description),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge
)
}
} else {
items(cinemaWithSessions.sessions, key = { it.uid }) { session ->
val route = Screen.SessionEdit.route.replace(
"{id}", session.uid.toString()
).replace(
"{cinemaId}", cinemaWithSessions.cinema.uid.toString()
)
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val formattedDate = dateFormatter.format(session.dateTime)
Column {
Text(
text = formattedDate,
color = MaterialTheme.colorScheme.onBackground,
)
Box(modifier = Modifier
.padding(vertical = 7.dp)
.clickable {
if (user.value?.role == UserRole.ADMIN)
navController.navigate(route)
}
.background(
color = MaterialTheme.colorScheme.secondary,
shape = RoundedCornerShape(16.dp)
)) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (cinemaWithSessions.cinema.image != null) Image(
bitmap = BitmapFactory.decodeByteArray(
cinemaWithSessions.cinema.image,
0,
cinemaWithSessions.cinema.image.size
).asImageBitmap(),
contentDescription = null,
modifier = Modifier
.size(90.dp)
.padding(4.dp)
)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "Цена: ${session.price}\n" + "Билетов: ${session.availableCount}",
color = MaterialTheme.colorScheme.onSecondary
)
}
if (user.value?.role == UserRole.USER) {
IconButton(
onClick = {
coroutineScope.launch {
cinemaWithSessionsViewModel.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.ADMIN) {
IconButton(
onClick = {
coroutineScope.launch {
cinemaWithSessionsViewModel.deleteSession(session = session)
}
},
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSecondary
)
}
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,236 @@
package com.example.myapplication.database.entities.composeui
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.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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 com.example.myapplication.LiveStore
import com.example.myapplication.datastore.DataStoreManager
import kotlinx.coroutines.launch
@Composable
fun UserProfile(
isDarkTheme: MutableState<Boolean>,
dataStoreManager: DataStoreManager,
viewModel: UserProfileViewModel = viewModel(factory = AppViewModelProvider.Factory),
) {
var isRegistration by remember { mutableStateOf(false) }
val coroutine = rememberCoroutineScope()
val errorStringId: Int? = viewModel.userUiState.errorId
val errorMessage =
if (errorStringId == null || errorStringId == 0) "" else stringResource(errorStringId)
val user = LiveStore.user.observeAsState()
LaunchedEffect(errorStringId) {
if (errorStringId == 0) {
isRegistration = false
}
}
LazyColumn {
item {
if (user.value != null) {
Column(
modifier = Modifier
.padding(16.dp),
) {
Text(
text = "Текущий пользователь: " + (user.value?.login ?: ""),
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Button(
enabled = user.value != null,
onClick = {
coroutine.launch {
dataStoreManager.setLogin("")
}
},
modifier = Modifier
.fillMaxWidth()
.padding(start = 2.dp, top = 10.dp)
) { Text("Выход") }
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = errorMessage,
color = Color.Red
)
Text(
text = "Логин",
modifier = Modifier.align(Alignment.CenterHorizontally)
)
BasicTextField(
value = viewModel.userUiState.details.login,
onValueChange = {
viewModel.updateUiState(viewModel.userUiState.details.copy(login = it))
},
modifier = Modifier
.fillMaxWidth()
.size(36.dp)
.background(
MaterialTheme.colorScheme.secondary,
RoundedCornerShape(18.dp)
)
.padding(start = 13.dp, top = 8.dp)
)
Text(
text = "Пароль",
modifier = Modifier.align(Alignment.CenterHorizontally)
)
BasicTextField(
value = viewModel.userUiState.details.password,
onValueChange = {
viewModel.updateUiState(viewModel.userUiState.details.copy(password = it))
},
modifier = Modifier
.fillMaxWidth()
.size(36.dp)
.background(
MaterialTheme.colorScheme.secondary,
RoundedCornerShape(18.dp)
)
.padding(start = 13.dp, top = 8.dp),
visualTransformation = PasswordVisualTransformation()
)
if (isRegistration) {
Text(
text = "Подтверждение пароля",
modifier = Modifier.align(Alignment.CenterHorizontally)
)
BasicTextField(
value = viewModel.userUiState.details.passwordConfirm,
onValueChange = {
viewModel.updateUiState(
viewModel.userUiState.details.copy(
passwordConfirm = it
)
)
},
modifier = Modifier
.fillMaxWidth()
.size(36.dp)
.background(
MaterialTheme.colorScheme.secondary,
RoundedCornerShape(18.dp)
)
.padding(start = 13.dp, top = 8.dp),
visualTransformation = PasswordVisualTransformation()
)
}
if (isRegistration) {
Button(
onClick = { coroutine.launch { viewModel.signUp() } },
modifier = Modifier
.fillMaxWidth()
.padding(start = 2.dp, top = 8.dp)
) {
Text("Регистрация")
}
Text(
text = "Уже есть аккаунт? Войти",
modifier = Modifier
.clickable {
isRegistration = false
}
.align(Alignment.CenterHorizontally),
color = MaterialTheme.colorScheme.onBackground
)
} else {
Button(
onClick = {
coroutine.launch {
viewModel.signIn(dataStoreManager)
}
},
modifier = Modifier
.fillMaxWidth()
.padding(start = 2.dp, top = 8.dp)
) {
Text("Вход")
}
Text(
text = "Нет аккаунта? Зарегистрироваться",
modifier = Modifier
.clickable {
isRegistration = true
}
.align(Alignment.CenterHorizontally),
color = MaterialTheme.colorScheme.onBackground
)
}
}
}
val switchColors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.primary, // Change the color when the switch is checked
checkedTrackColor = MaterialTheme.colorScheme.secondary, // Change the color of the track when the switch is checked
uncheckedThumbColor = MaterialTheme.colorScheme.primary, // Change the color when the switch is unchecked
uncheckedTrackColor = MaterialTheme.colorScheme.onPrimary // Change the color of the track when the switch is unchecked
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.End
) {
Text(
"Темная тема", modifier = Modifier
.align(Alignment.CenterVertically)
.padding(5.dp)
)
Switch(
checked = isDarkTheme.value,
onCheckedChange = {
isDarkTheme.value = !isDarkTheme.value
coroutine.launch {
if (isDarkTheme.value) {
dataStoreManager.setDarkTheme("Dark")
} else {
dataStoreManager.setDarkTheme("Light")
}
}
},
colors = switchColors
)
}
}
}
}

View File

@ -0,0 +1,118 @@
package com.example.myapplication.database.entities.composeui
import android.util.Log
import android.widget.Toast
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.example.myapplication.MainComposeActivity
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.repository.UserRepository
import com.example.myapplication.datastore.DataStoreManager
class UserProfileViewModel(
private val userRepository: UserRepository
) : MyViewModel() {
var userUiState by mutableStateOf(UserUiState())
private set
fun updateUiState(userDetails: UserDetails) {
userUiState = UserUiState(
details = userDetails,
errorId = null
)
}
suspend fun signIn(dataStoreManager: DataStoreManager) {
userUiState.details.passwordConfirm = userUiState.details.password
var errorId: Int? = validateInput(userUiState.details)
runInScope(
actionSuccess = {
if (errorId == null) {
val overlap: User? = userRepository.getUser(userUiState.details.login)
if (overlap == null || userUiState.details.password != overlap.password) {
errorId = R.string.err_04
} else {
dataStoreManager.setLogin("")
dataStoreManager.setLogin(userUiState.details.login)
Log.d("UserProfileViewModel", "sign in success")
}
}
userUiState = UserUiState(
details = userUiState.details,
errorId = errorId
)
}, actionError = {
errorId = R.string.err_06
userUiState = UserUiState(
details = userUiState.details,
errorId = errorId
)
})
}
suspend fun signUp() {
var errorId: Int? = validateInput(userUiState.details)
runInScope(actionSuccess = {
if (errorId == null) {
val overlap = userRepository.getUser(userUiState.details.login)
if (overlap != null) {
errorId = R.string.err_03
}
}
if (errorId == null) {
userRepository.insertUser(userUiState.details.toUser())
val toast = Toast.makeText(
MainComposeActivity.appContext,
"Вы зарегистрированы",
Toast.LENGTH_SHORT
)
toast.show()
errorId = 0
}
userUiState = UserUiState(
details = userUiState.details,
errorId = errorId
)
}, actionError = {
errorId = R.string.err_06
userUiState = UserUiState(
details = userUiState.details,
errorId = errorId
)
})
}
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 if (details.passwordConfirm.isBlank()
|| details.password != details.passwordConfirm
) {
R.string.err_05
} else {
null
}
}
}
data class UserDetails(
val login: String = "",
val password: String = "",
var passwordConfirm: 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

@ -0,0 +1,137 @@
package com.example.myapplication.database.entities.composeui.edit
import android.content.res.Configuration
import android.graphics.BitmapFactory
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.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.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
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.ui.theme.PmudemoTheme
import kotlinx.coroutines.launch
@Composable
fun CinemaEdit(
navController: NavController,
viewModel: CinemaEditViewModel = viewModel(factory = AppViewModelProvider.Factory),
) {
val coroutineScope = rememberCoroutineScope()
when (viewModel.apiStatus) {
ApiStatus.DONE -> {
CinemaEdit(
cinemaUiState = viewModel.cinemaUiState,
onClick = {
coroutineScope.launch {
viewModel.saveCinema()
navController.popBackStack()
}
},
onUpdate = viewModel::updateUiState,
)
}
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.popBackStack() }
)
}
}
@Composable
private fun CinemaEdit(
cinemaUiState: CinemaUiState,
onClick: () -> Unit,
onUpdate: (CinemaDetails) -> Unit,
) {
Column(
Modifier
.fillMaxWidth()
.padding(all = 10.dp)
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = cinemaUiState.cinemaDetails.name,
onValueChange = { onUpdate(cinemaUiState.cinemaDetails.copy(name = it)) },
label = { Text(stringResource(id = R.string.Cinema_name)) },
singleLine = true
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = cinemaUiState.cinemaDetails.description,
onValueChange = { onUpdate(cinemaUiState.cinemaDetails.copy(description = it)) },
label = { Text(stringResource(id = R.string.Cinema_description)) },
singleLine = true
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = cinemaUiState.cinemaDetails.year.toString(),
onValueChange = { newValue ->
val parsedYear = newValue.toLongOrNull() ?: 0L
onUpdate(cinemaUiState.cinemaDetails.copy(year = parsedYear))
},
label = { Text(stringResource(id = R.string.Cinema_year)) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
visualTransformation = VisualTransformation.None // Отключает маскировку
)
if (cinemaUiState.cinemaDetails.image != null)
ImageUploader(
bitmap = BitmapFactory.decodeByteArray(
cinemaUiState.cinemaDetails.image,
0,
cinemaUiState.cinemaDetails.image.size
),
onResult = { byteArray: ByteArray? ->
onUpdate(
cinemaUiState.cinemaDetails.copy(
image = byteArray
)
)
}
)
Button(
onClick = onClick,
enabled = cinemaUiState.isEntryValid,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.Save_button))
}
}
}
@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 CinemaEditPreview() {
PmudemoTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
CinemaEdit(
cinemaUiState = CinemaUiState(),
onClick = {},
onUpdate = {},
)
}
}
}

View File

@ -0,0 +1,104 @@
package com.example.myapplication.database.entities.composeui.edit
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import com.example.myapplication.composeui.MyViewModel
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.CinemaWithSessions
import com.example.myapplication.database.entities.model.SessionFromCinema
import com.example.myapplication.database.entities.repository.CinemaRepository
class CinemaEditViewModel(
savedStateHandle: SavedStateHandle,
private val cinemaRepository: CinemaRepository
) : MyViewModel() {
var cinemaUiState by mutableStateOf(CinemaUiState())
private set
private val cinemaUid: Int = checkNotNull(savedStateHandle["id"])
init {
if (cinemaUid > 0) {
runInScope(
actionSuccess = {
cinemaUiState = cinemaRepository.getCinema(cinemaUid)
.toUiState(true)
},
actionError = {
cinemaUiState = CinemaUiState()
}
)
}
}
fun updateUiState(cinemaDetails: CinemaDetails) {
cinemaUiState = CinemaUiState(
cinemaDetails = cinemaDetails,
isEntryValid = validateInput(cinemaDetails)
)
}
suspend fun saveCinema() {
if (validateInput()) {
runInScope(
actionSuccess = {
if (cinemaUid > 0) {
cinemaRepository.updateCinema(cinemaUiState.cinemaDetails.toCinema(cinemaUid))
} else {
cinemaRepository.insertCinema(cinemaUiState.cinemaDetails.toCinema())
}
}
)
}
}
private fun validateInput(uiState: CinemaDetails = cinemaUiState.cinemaDetails): Boolean {
return with(uiState) {
name.isNotBlank()
&& description.isNotBlank()
&& year >= 1900
&& year <= 2100
}
}
}
data class CinemaUiState(
val cinemaDetails: CinemaDetails = CinemaDetails(),
val isEntryValid: Boolean = false
)
data class CinemaDetails(
val name: String = "",
val description: String = "",
val image: ByteArray? = byteArrayOf(),
val year: Long = 1900,
val sessions: List<SessionFromCinema> = emptyList()
)
fun CinemaDetails.toCinema(uid: Int = 0): Cinema = Cinema(
uid = uid,
name = name,
description = description,
image = image,
year = year
)
fun CinemaWithSessions.toDetails(): CinemaDetails {
val cinema = this.cinema
val sessions = this.sessions
return CinemaDetails(
name = cinema.name,
description = cinema.description,
image = cinema.image,
year = cinema.year,
sessions = sessions
)
}
fun CinemaWithSessions.toUiState(isEntryValid: Boolean = false): CinemaUiState = CinemaUiState(
cinemaDetails = this.toDetails(),
isEntryValid = isEntryValid
)

View File

@ -0,0 +1,121 @@
package com.example.myapplication.database.entities.composeui.edit
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.example.myapplication.R
import java.io.ByteArrayOutputStream
@Composable
fun ImageUploader(
bitmap: Bitmap?,
onResult: (ByteArray) -> Unit
) {
val context = LocalContext.current
val title: String = if (bitmap == null) {
stringResource(R.string.not_uploaded)
} else {
stringResource(R.string.size, bitmap.width, bitmap.height)
}
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent(),
onResult = { uri: Uri? ->
uri?.let {
val inputStream = context.contentResolver.openInputStream(uri)
val newBitmap: Bitmap = BitmapFactory.decodeStream(inputStream)
val scaledBitmap = resizeBitmapWithAspectRatio(newBitmap, 200)
val stream = ByteArrayOutputStream()
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
onResult(stream.toByteArray())
}
}
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(10.dp)
)
.aspectRatio(1f)
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.primary,
shape = RoundedCornerShape(10.dp)
)
.clickable { launcher.launch("image/*") }
.padding(16.dp)
) {
if (bitmap != null) {
Image(
bitmap = bitmap.asImageBitmap(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.clip(shape = RoundedCornerShape(10.dp))
)
} else {
Image(
painter = painterResource(id = R.drawable.photo),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.clip(shape = RoundedCornerShape(10.dp))
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = title,
color = MaterialTheme.colorScheme.onBackground
)
}
}
fun resizeBitmapWithAspectRatio(bitmap: Bitmap, maxHeight: Int): Bitmap {
if (bitmap.height <= maxHeight) {
return bitmap
}
val aspectRatio = bitmap.width.toFloat() / bitmap.height
val newWidth = (maxHeight * aspectRatio).toInt()
return Bitmap.createScaledBitmap(bitmap, newWidth, maxHeight, true)
}

View File

@ -0,0 +1,161 @@
package com.example.myapplication.database.entities.composeui.edit
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DisplayMode
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TimePicker
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberTimePickerState
import androidx.compose.runtime.*
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.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
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 kotlinx.coroutines.launch
import org.threeten.bp.Instant
import org.threeten.bp.LocalDateTime
import org.threeten.bp.LocalTime
import org.threeten.bp.ZoneId
import org.threeten.bp.ZoneOffset
@Composable
fun SessionEdit(
navController: NavController,
viewModel: SessionEditViewModel = viewModel(factory = AppViewModelProvider.Factory),
) {
val coroutineScope = rememberCoroutineScope()
when (viewModel.apiStatus) {
ApiStatus.DONE -> {
SessionEdit(
sessionUiState = viewModel.sessionUiState,
onClick = {
coroutineScope.launch {
viewModel.saveSession()
navController.popBackStack()
}
},
onUpdate = viewModel::updateUiState
)
}
ApiStatus.LOADING -> LoadingPlaceholder()
else -> ErrorPlaceholder(
message = viewModel.apiError,
onBack = { navController.popBackStack() }
)
}
}
fun Long.toLocalDate(): org.threeten.bp.LocalDate {
val instant = Instant.ofEpochMilli(this)
return instant.atZone(ZoneId.systemDefault()).toLocalDate()
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SessionEdit(
sessionUiState: SessionUiState,
onClick: () -> Unit,
onUpdate: (SessionDetails) -> Unit,
) {
LazyColumn(
Modifier
.fillMaxWidth()
.padding(all = 10.dp)
) {
item {
if (sessionUiState.sessionDetails.dateTime != LocalDateTime.MIN) {
val selectedDateMillis =
sessionUiState.sessionDetails.dateTime.toInstant(ZoneOffset.UTC).toEpochMilli()
val dateState = rememberDatePickerState(
initialDisplayMode = DisplayMode.Input,
initialSelectedDateMillis = selectedDateMillis
)
val timeState = rememberTimePickerState(
sessionUiState.sessionDetails.dateTime.hour,
sessionUiState.sessionDetails.dateTime.minute
)
DatePicker(
state = dateState,
modifier = Modifier.padding(16.dp)
)
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
TimePicker(state = timeState)
}
val selectedDate = dateState.selectedDateMillis?.toLocalDate()
val selectedTime = LocalTime.of(timeState.hour, timeState.minute)
if (selectedDate != null) {
val resultDateTime = LocalDateTime.of(selectedDate, selectedTime)
onUpdate(sessionUiState.sessionDetails.copy(dateTime = resultDateTime))
}
} else {
val dateState = rememberDatePickerState(initialDisplayMode = DisplayMode.Input)
val timeState = rememberTimePickerState()
DatePicker(
state = dateState,
modifier = Modifier.padding(16.dp)
)
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
TimePicker(state = timeState)
}
val selectedDate = dateState.selectedDateMillis?.toLocalDate()
val selectedTime = LocalTime.of(timeState.hour, timeState.minute)
if (selectedDate != null) {
val resultDateTime = LocalDateTime.of(selectedDate, selectedTime)
onUpdate(sessionUiState.sessionDetails.copy(dateTime = resultDateTime))
}
}
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = sessionUiState.sessionDetails.price,
label = { Text(text = "Цена") },
onValueChange = {
onUpdate(sessionUiState.sessionDetails.copy(price = it))
},
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = sessionUiState.sessionDetails.maxCount.toString(),
onValueChange = { newValue ->
val parsedMaxCount = newValue.toIntOrNull() ?: 0 // Преобразование в Int
onUpdate(sessionUiState.sessionDetails.copy(maxCount = parsedMaxCount))
},
label = { Text(text = "Количество") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
Button(
onClick = onClick,
enabled = sessionUiState.isEntryValid,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.Save_button))
}
}
}
}

View File

@ -0,0 +1,113 @@
package com.example.myapplication.database.entities.composeui.edit
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import com.example.myapplication.composeui.MyViewModel
import com.example.myapplication.database.entities.model.Session
import com.example.myapplication.database.entities.repository.SessionRepository
import org.threeten.bp.LocalDateTime
class SessionEditViewModel(
savedStateHandle: SavedStateHandle,
private val sessionRepository: SessionRepository
) : MyViewModel() {
var sessionUiState by mutableStateOf(SessionUiState())
private set
private val sessionUid: Int = checkNotNull(savedStateHandle["id"])
private val cinemaUid: Int = checkNotNull(savedStateHandle["cinemaId"])
init {
if (sessionUid > 0) {
runInScope(
actionSuccess = {
sessionUiState = sessionRepository.getSession(sessionUid)
.toUiState(true)
},
actionError = {
sessionUiState = SessionUiState()
}
)
}
}
fun updateUiState(sessionDetails: SessionDetails) {
sessionUiState = SessionUiState(
sessionDetails = sessionDetails,
isEntryValid = validateInput(sessionDetails)
)
}
suspend fun saveSession() {
if (validateInput()) {
runInScope(
actionSuccess = {
if (cinemaUid > 0)
if (sessionUid > 0) {
sessionRepository.updateSession(
sessionUiState.sessionDetails
.toSession(uid = sessionUid, cinemaUid = cinemaUid)
)
} else {
sessionRepository.insertSession(
sessionUiState.sessionDetails.toSession(
cinemaUid = cinemaUid
)
)
}
}
)
}
}
private fun validateInput(uiState: SessionDetails = sessionUiState.sessionDetails): Boolean {
return with(uiState) {
dateTime != LocalDateTime.MIN
&& isValidDouble(price)
&& maxCount > 0
&& cinemaUid > 0
}
}
}
val regex = """^-?\d+(.\d+)?+(,\d+)?$""".toRegex()
fun isValidDouble(input: String): Boolean {
return regex.matches(input)
}
data class SessionUiState(
val sessionDetails: SessionDetails = SessionDetails(),
val isEntryValid: Boolean = false
)
data class SessionDetails(
val uid: Int = 0,
val dateTime: LocalDateTime = LocalDateTime.MIN,
val price: String = "0",
val maxCount: Int = 0,
val cinemaId: Int = 0
)
fun SessionDetails.toSession(uid: Int = 0, cinemaUid: Int = 0): Session = Session(
uid = uid,
dateTime = dateTime,
price = price.toDoubleOrNull() ?: 0.0,
maxCount = maxCount,
cinemaId = cinemaUid
)
fun Session.toDetails(): SessionDetails = SessionDetails(
uid = uid,
dateTime = dateTime,
price = price.toString(),
maxCount = maxCount,
cinemaId = cinemaId
)
fun Session.toUiState(isEntryValid: Boolean = false): SessionUiState = SessionUiState(
sessionDetails = this.toDetails(),
isEntryValid = isEntryValid
)

View File

@ -0,0 +1,43 @@
package com.example.myapplication.database.entities.dao
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.SessionFromCinema
import kotlinx.coroutines.flow.Flow
@Dao
interface CinemaDao {
@Query("select * from cinemas order by name")
fun getAll(): PagingSource<Int, Cinema>
@Query("select * from cinemas where cinemas.name like :name order by name collate nocase asc")
fun getAll(name: String): PagingSource<Int, Cinema>
@Query(
"SELECT c.*, s.uid as session_uid, s.date_time, s.price, s.max_count-IFNULL(SUM(os.count), 0) as available_count, c.uid as cinema_id " +
"FROM cinemas AS c " +
"LEFT JOIN sessions AS s ON s.cinema_id = c.uid " +
"LEFT JOIN orders_sessions AS os ON os.session_id = s.uid " +
"WHERE c.uid = :cinemaId " +
"GROUP BY session_uid"
)
fun getByUid(cinemaId: Int?): Flow<Map<Cinema, List<SessionFromCinema>>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg cinema: Cinema)
@Update
suspend fun update(cinema: Cinema)
@Delete
suspend fun delete(cinema: Cinema)
@Query("DELETE FROM cinemas")
suspend fun deleteAll()
}

View File

@ -0,0 +1,43 @@
package com.example.myapplication.database.entities.dao
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.model.OrderWithUser
import com.example.myapplication.database.entities.model.SessionFromOrder
@Dao
interface OrderDao {
@Query("select * from orders where user_id = :userId")
fun getAll(userId: Int?): PagingSource<Int, OrderWithUser>
@Query("select * from orders " +
"left join users on orders.user_id = users.uid")
fun getAll(): PagingSource<Int, OrderWithUser>
@Query(
"SELECT o.*, s.*, os.count, os.frozen_price " +
"FROM orders AS o " +
"JOIN orders_sessions AS os ON os.order_id = o.uid " +
"JOIN sessions AS s ON s.uid = os.session_id " +
"WHERE o.uid = :orderId"
)
fun getByUid(orderId: Int?): List<SessionFromOrder>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg order: Order): List<Long>
@Update
suspend fun update(order: Order)
@Delete
suspend fun delete(order: Order)
@Query("DELETE FROM orders")
suspend fun deleteAll()
}

View File

@ -0,0 +1,27 @@
package com.example.myapplication.database.entities.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.myapplication.database.entities.model.OrderSessionCrossRef
@Dao
interface OrderSessionCrossRefDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(orderSessionCrossRef: OrderSessionCrossRef)
@Update
suspend fun update(orderSessionCrossRef: OrderSessionCrossRef)
@Delete
suspend fun delete(orderSessionCrossRef: OrderSessionCrossRef)
@Query("DELETE FROM orders_sessions where orders_sessions.order_id = :orderId")
suspend fun deleteByOrderUid(orderId: Int)
@Query("DELETE FROM orders_sessions where orders_sessions.session_id = :sessionId")
suspend fun deleteBySessionUid(sessionId: Int)
}

View File

@ -0,0 +1,36 @@
package com.example.myapplication.database.entities.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.myapplication.database.entities.model.Session
@Dao
interface SessionDao {
@Query("select * from sessions where sessions.uid = :uid")
suspend fun getByUid(uid: Int): Session
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg session: Session)
@Update
suspend fun update(session: Session)
@Delete
suspend fun delete(session: Session)
@Query("DELETE FROM sessions")
suspend fun deleteAll()
@Query(
"SELECT s.max_count-IFNULL(SUM(os.count), 0) as available_count " +
"FROM sessions AS s " +
"LEFT JOIN orders_sessions AS os ON os.session_id = s.uid " +
"WHERE s.uid = :sessionId " +
"GROUP BY s.uid"
)
suspend fun getAvailableCountOfSession(sessionId: Int): Int
}

View File

@ -0,0 +1,42 @@
package com.example.myapplication.database.entities.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.myapplication.database.entities.model.SessionFromCart
import com.example.myapplication.database.entities.model.User
import kotlinx.coroutines.flow.Flow
@Dao
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 " +
"join users_sessions on sessions.uid = users_sessions.session_id " +
"left join orders_sessions on sessions.uid = orders_sessions.session_id " +
"where users_sessions.user_id = :userId " +
"GROUP BY users_sessions.session_id "
)
suspend fun getCartByUid(userId: Int): List<SessionFromCart>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg user: User)
@Update
suspend fun update(user: User)
@Delete
suspend fun delete(user: User)
@Query("DELETE FROM users")
suspend fun deleteAll()
}

View File

@ -0,0 +1,27 @@
package com.example.myapplication.database.entities.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import com.example.myapplication.database.entities.model.UserSessionCrossRef
@Dao
interface UserSessionCrossRefDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(userSessionCrossRef: UserSessionCrossRef)
@Update
suspend fun update(userSessionCrossRef: UserSessionCrossRef)
@Delete
suspend fun delete(vararg userSessionCrossRef: UserSessionCrossRef)
@Query("DELETE FROM users_sessions where users_sessions.user_id = :userId")
suspend fun deleteByUserUid(userId: Int)
@Query("DELETE FROM users_sessions where users_sessions.session_id = :sessionId")
suspend fun deleteBySessionUid(sessionId: Int)
}

View File

@ -0,0 +1,57 @@
package com.example.myapplication.database.entities.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
@Entity(tableName = "cinemas")
data class Cinema(
@PrimaryKey(autoGenerate = true)
val uid: Int = 0,
val name: String,
val description: String,
@ColumnInfo(typeAffinity = ColumnInfo.BLOB)
val image: ByteArray?,
val year: Long
) {
@Ignore
constructor(
name: String,
description: String,
image: ByteArray?,
year: Long
) : this(0, name, description, image, year)
companion object {
fun getCinema(index: Int = 0): Cinema {
return Cinema(
index,
"name",
"desc",
byteArrayOf(),
0,
)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Cinema
if (uid != other.uid) return false
if (name != other.name) return false
if (description != other.description) return false
if (year != other.year) return false
return image.contentEquals(other.image)
}
override fun hashCode(): Int {
var result = uid
result = 31 * result + name.hashCode()
result = 31 * result + description.hashCode()
result = 31 * result + year.hashCode()
return result
}
}

View File

@ -0,0 +1,24 @@
package com.example.myapplication.database.entities.model
data class CinemaWithSessions(
val cinema: Cinema,
val sessions: List<SessionFromCinema>
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CinemaWithSessions
if (cinema != other.cinema) return false
if (sessions != other.sessions) return false
return true
}
override fun hashCode(): Int {
var result = cinema.hashCode()
result = 31 * result + sessions.hashCode()
return result
}
}

View File

@ -0,0 +1,16 @@
package com.example.myapplication.database.entities.model
import androidx.room.TypeConverter
import org.threeten.bp.LocalDateTime
class LocalDateTimeConverter {
@TypeConverter
fun fromLocalDateTime(value: LocalDateTime?): String? {
return value?.toString()
}
@TypeConverter
fun toLocalDateTime(value: String?): LocalDateTime? {
return value?.let { LocalDateTime.parse(it) }
}
}

View File

@ -0,0 +1,31 @@
package com.example.myapplication.database.entities.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.threeten.bp.LocalDateTime
@Entity(
tableName = "orders"
)
data class Order(
@PrimaryKey(autoGenerate = true)
val uid: Int,
@ColumnInfo(name = "user_id", index = true)
val userId: Int = 0,
@ColumnInfo(name = "date_time")
val dateTime: LocalDateTime,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Order
if (uid != other.uid) return false
return true
}
override fun hashCode(): Int {
return uid
}
}

View File

@ -0,0 +1,37 @@
package com.example.myapplication.database.entities.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import java.util.Objects
@Entity(
tableName = "orders_sessions",
primaryKeys = ["order_id", "session_id"]
)
data class OrderSessionCrossRef(
@ColumnInfo(name = "order_id", index = true)
val orderId: Int,
@ColumnInfo(name = "session_id", index = true)
val sessionId: Int,
@ColumnInfo(name = "frozen_price")
val frozenPrice: Double,
val count: Int
) {
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (javaClass != other?.javaClass) {
return false
}
other as OrderSessionCrossRef
if (orderId == other.orderId && sessionId == other.sessionId) {
return true
}
return false
}
override fun hashCode(): Int {
return Objects.hash(orderId, sessionId)
}
}

View File

@ -0,0 +1,18 @@
package com.example.myapplication.database.entities.model
import androidx.room.ColumnInfo
import androidx.room.Relation
import org.threeten.bp.LocalDateTime
data class OrderWithUser(
val uid: Int,
@ColumnInfo(name = "user_id", index = true)
val userId: Int = 0,
@ColumnInfo(name = "date_time")
val dateTime: LocalDateTime,
@Relation(
parentColumn = "user_id",
entity = User::class,
entityColumn = "uid"
) val user: User?
)

View File

@ -0,0 +1,73 @@
package com.example.myapplication.database.entities.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Ignore
import androidx.room.PrimaryKey
import org.threeten.bp.LocalDateTime
@Entity(
tableName = "sessions", foreignKeys = [
ForeignKey(
entity = Cinema::class,
parentColumns = ["uid"],
childColumns = ["cinema_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.RESTRICT
)
]
)
data class Session(
@PrimaryKey(autoGenerate = true)
val uid: Int = 0,
@ColumnInfo(name = "date_time")
val dateTime: LocalDateTime,
val price: Double,
@ColumnInfo(name = "max_count")
val maxCount: Int,
@ColumnInfo(name = "cinema_id", index = true)
val cinemaId: Int = 0,
) {
@Ignore
constructor(
dateTime: LocalDateTime,
price: Double,
maxCount: Int,
cinema: Cinema,
) : this(0, dateTime, price, maxCount, cinema.uid)
companion object {
fun getSession(index: Int = 0): Session {
return Session(
index,
LocalDateTime.MIN,
0.0,
0,
0
)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Session
if (uid != other.uid) return false
if (dateTime != other.dateTime) return false
if (price != other.price) return false
if (maxCount != other.maxCount) return false
if (cinemaId != other.cinemaId) return false
return true
}
override fun hashCode(): Int {
var result = uid
result = 31 * result + dateTime.hashCode()
result = 31 * result + price.hashCode()
result = 31 * result + maxCount.hashCode()
result = 31 * result + cinemaId.hashCode()
return result
}
}

View File

@ -0,0 +1,23 @@
package com.example.myapplication.database.entities.model
import androidx.room.ColumnInfo
import androidx.room.Relation
import org.threeten.bp.LocalDateTime
data class SessionFromCart(
@ColumnInfo(name = "uid")
val uid: Int = 0,
@ColumnInfo(name = "date_time")
val dateTime: LocalDateTime,
val price: Double,
@ColumnInfo(name = "available_count")
val availableCount: Int,
val count: Int,
@ColumnInfo(name = "cinema_id")
val cinemaId: Int = 0,
@Relation(
parentColumn = "cinema_id",
entity = Cinema::class,
entityColumn = "uid"
) val cinema: Cinema
)

View File

@ -0,0 +1,47 @@
package com.example.myapplication.database.entities.model
import androidx.room.ColumnInfo
import org.threeten.bp.LocalDateTime
import org.threeten.bp.format.DateTimeFormatter
data class SessionFromCinema(
@ColumnInfo(name = "session_uid")
val uid: Int,
@ColumnInfo(name = "date_time")
val dateTime: LocalDateTime,
val price: Double,
@ColumnInfo(name = "available_count")
val availableCount: Int,
@ColumnInfo(name = "cinema_id")
val cinemaId: Int,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SessionFromCinema
if (uid != other.uid) return false
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
if (dateFormatter.format(dateTime) != dateFormatter.format(other.dateTime)) return false
if (price != other.price) return false
if (availableCount != other.availableCount) return false
if (cinemaId != other.cinemaId) return false
return true
}
override fun hashCode(): Int {
var result = uid
result = 31 * result + dateTime.hashCode()
result = 31 * result + price.hashCode()
result = 31 * result + availableCount.hashCode()
result = 31 * result + cinemaId.hashCode()
return result
}
}
fun SessionFromCinema.toSession(): Session = Session(
uid,
dateTime,
price,
availableCount,
cinemaId
)

View File

@ -0,0 +1,23 @@
package com.example.myapplication.database.entities.model
import androidx.room.ColumnInfo
import androidx.room.Relation
import org.threeten.bp.LocalDateTime
data class SessionFromOrder(
@ColumnInfo(name = "uid")
val uid: Int = 0,
@ColumnInfo(name = "date_time")
val dateTime: LocalDateTime,
@ColumnInfo(name = "frozen_price")
val frozenPrice: Double,
val count: Int,
@ColumnInfo(name = "cinema_id")
val cinemaId: Int = 0,
@Relation(
parentColumn = "cinema_id",
entity = Cinema::class,
entityColumn = "uid"
)
val cinema: Cinema
)

View File

@ -0,0 +1,25 @@
package com.example.myapplication.database.entities.model
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val uid: Int = 0,
val login: String,
val password: String,
val role: UserRole = UserRole.USER
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as User
if (uid != other.uid) return false
return true
}
override fun hashCode(): Int {
return uid ?: -1
}
}

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

@ -0,0 +1,36 @@
package com.example.myapplication.database.entities.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import java.util.Objects.hash
@Entity(
tableName = "users_sessions",
primaryKeys = ["user_id", "session_id"]
)
data class UserSessionCrossRef(
@ColumnInfo(name = "user_id", index = true)
val userId: Int,
@ColumnInfo(name = "session_id", index = true)
val sessionId: Int,
@ColumnInfo(name = "count")
val count: Int,
) {
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (javaClass != other?.javaClass) {
return false
}
other as UserSessionCrossRef
if (userId == other.userId && sessionId == other.sessionId) {
return true
}
return false
}
override fun hashCode(): Int {
return hash(userId, sessionId)
}
}

View File

@ -0,0 +1,14 @@
package com.example.myapplication.database.entities.repository
import androidx.paging.PagingData
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.CinemaWithSessions
import kotlinx.coroutines.flow.Flow
interface CinemaRepository {
fun getAllCinemas(name: String): Flow<PagingData<Cinema>>
suspend fun getCinema(uid: Int): CinemaWithSessions
suspend fun insertCinema(cinema: Cinema)
suspend fun updateCinema(cinema: Cinema)
suspend fun deleteCinema(cinema: Cinema)
}

View File

@ -0,0 +1,53 @@
package com.example.myapplication.database.entities.repository
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import com.example.myapplication.database.AppContainer
import com.example.myapplication.database.entities.dao.CinemaDao
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.CinemaWithSessions
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
class OfflineCinemaRepository(private val cinemaDao: CinemaDao) : CinemaRepository {
override fun getAllCinemas(name: String): Flow<PagingData<Cinema>> = Pager(
config = PagingConfig(
pageSize = AppContainer.LIMIT,
enablePlaceholders = false
),
pagingSourceFactory = { cinemaDao.getAll(name) }
).flow
override suspend fun getCinema(uid: Int): CinemaWithSessions {
val item = cinemaDao.getByUid(uid)
.map { map ->
map.firstNotNullOf {
CinemaWithSessions(
cinema = it.key,
sessions = it.value
)
}
}
.first()
return item
}
override suspend fun insertCinema(cinema: Cinema) = cinemaDao.insert(cinema)
override suspend fun updateCinema(cinema: Cinema) = cinemaDao.update(cinema)
override suspend fun deleteCinema(cinema: Cinema) = cinemaDao.delete(cinema)
fun getAllCinemasPagingSource(): PagingSource<Int, Cinema> = cinemaDao.getAll()
fun getAllCinemasPagingSource(name: String): PagingSource<Int, Cinema> = cinemaDao.getAll(name)
suspend fun insertCinemas(cinemas: List<Cinema>) =
cinemaDao.insert(*cinemas.toTypedArray())
suspend fun clearCinemas() = cinemaDao.deleteAll()
}

View File

@ -0,0 +1,39 @@
package com.example.myapplication.database.entities.repository
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import com.example.myapplication.LiveStore
import com.example.myapplication.database.AppContainer
import com.example.myapplication.database.entities.dao.OrderDao
import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.model.OrderWithUser
import com.example.myapplication.database.entities.model.SessionFromOrder
import com.example.myapplication.database.entities.model.UserRole
import kotlinx.coroutines.flow.Flow
class OfflineOrderRepository(private val orderDao: OrderDao) : OrderRepository {
override fun getAllOrders(): Flow<PagingData<OrderWithUser>> = Pager(
config = PagingConfig(
pageSize = AppContainer.LIMIT,
enablePlaceholders = false
),
pagingSourceFactory = { getAllOrdersPagingSource() }
).flow
override suspend fun getOrder(uid: Int): List<SessionFromOrder> = orderDao.getByUid(uid)
override suspend fun insertOrder(order: Order): Long = orderDao.insert(order).first()
fun getAllOrdersPagingSource(): PagingSource<Int, OrderWithUser> {
val user = LiveStore.user.value
if (user?.role == UserRole.ADMIN)
return orderDao.getAll()
return orderDao.getAll(user?.uid ?: 0)
}
suspend fun clearOrders() = orderDao.deleteAll()
suspend fun insertOrders(orders: List<Order>) = orderDao.insert(*orders.toTypedArray())
}

View File

@ -0,0 +1,20 @@
package com.example.myapplication.database.entities.repository
import com.example.myapplication.database.entities.dao.OrderSessionCrossRefDao
import com.example.myapplication.database.entities.model.OrderSessionCrossRef
class OfflineOrderSessionRepository(private val orderSessionDao: OrderSessionCrossRefDao) :
OrderSessionRepository {
override suspend fun insertOrderSession(orderSessionCrossRef: OrderSessionCrossRef) =
orderSessionDao.insert(orderSessionCrossRef)
override suspend fun updateOrderSession(orderSessionCrossRef: OrderSessionCrossRef) =
orderSessionDao.update(orderSessionCrossRef)
override suspend fun deleteOrderSession(orderSessionCrossRef: OrderSessionCrossRef) =
orderSessionDao.delete(orderSessionCrossRef)
suspend fun deleteOrderSessions(userId: Int) = orderSessionDao.deleteByOrderUid(userId)
suspend fun deleteSessionsByUid(sessionId: Int) = orderSessionDao.deleteBySessionUid(sessionId)
}

View File

@ -0,0 +1,19 @@
package com.example.myapplication.database.entities.repository
import com.example.myapplication.database.entities.dao.SessionDao
import com.example.myapplication.database.entities.model.Session
class OfflineSessionRepository(private val sessionDao: SessionDao) : SessionRepository {
override suspend fun getSession(uid: Int): Session = sessionDao.getByUid(uid)
override suspend fun insertSession(session: Session) = sessionDao.insert(session)
override suspend fun updateSession(session: Session) = sessionDao.update(session)
override suspend fun deleteSession(session: Session) = sessionDao.delete(session)
suspend fun insertSessions(sessions: List<Session>) =
sessionDao.insert(*sessions.toTypedArray())
suspend fun clearSessions() = sessionDao.deleteAll()
}

View File

@ -0,0 +1,26 @@
package com.example.myapplication.database.entities.repository
import com.example.myapplication.database.entities.dao.UserDao
import com.example.myapplication.database.entities.model.SessionFromCart
import com.example.myapplication.database.entities.model.User
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)
override suspend fun insertUser(user: User) = userDao.insert(user)
override suspend fun updateUser(user: User) = userDao.update(user)
override suspend fun deleteUser(user: User) = userDao.delete(user)
suspend fun insertUsers(users: List<User>) =
userDao.insert(*users.toTypedArray())
suspend fun clearUsers() = userDao.deleteAll()
}

View File

@ -0,0 +1,23 @@
package com.example.myapplication.database.entities.repository
import com.example.myapplication.database.entities.dao.UserSessionCrossRefDao
import com.example.myapplication.database.entities.model.UserSessionCrossRef
class OfflineUserSessionRepository(private val userSessionDao: UserSessionCrossRefDao) :
UserSessionRepository {
override suspend fun insertUserSession(userSessionCrossRef: UserSessionCrossRef) =
userSessionDao.insert(userSessionCrossRef)
override suspend fun updateUserSession(userSessionCrossRef: UserSessionCrossRef) =
userSessionDao.update(userSessionCrossRef)
override suspend fun deleteUserSession(userSessionCrossRef: UserSessionCrossRef) =
userSessionDao.delete(userSessionCrossRef)
override suspend fun deleteUserSessions(userId: Int) = userSessionDao.deleteByUserUid(userId)
override suspend fun deleteUserSessions(userSessionCrossRefs: List<UserSessionCrossRef>) =
userSessionDao.delete(*userSessionCrossRefs.toTypedArray())
suspend fun deleteSessionsByUid(sessionId: Int) = userSessionDao.deleteBySessionUid(sessionId)
}

View File

@ -0,0 +1,13 @@
package com.example.myapplication.database.entities.repository
import androidx.paging.PagingData
import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.model.OrderWithUser
import com.example.myapplication.database.entities.model.SessionFromOrder
import kotlinx.coroutines.flow.Flow
interface OrderRepository {
fun getAllOrders(): Flow<PagingData<OrderWithUser>>
suspend fun getOrder(uid: Int): List<SessionFromOrder>
suspend fun insertOrder(order: Order): Long
}

View File

@ -0,0 +1,9 @@
package com.example.myapplication.database.entities.repository
import com.example.myapplication.database.entities.model.OrderSessionCrossRef
interface OrderSessionRepository {
suspend fun insertOrderSession(orderSessionCrossRef: OrderSessionCrossRef)
suspend fun updateOrderSession(orderSessionCrossRef: OrderSessionCrossRef)
suspend fun deleteOrderSession(orderSessionCrossRef: OrderSessionCrossRef)
}

View File

@ -0,0 +1,10 @@
package com.example.myapplication.database.entities.repository
import com.example.myapplication.database.entities.model.Session
interface SessionRepository {
suspend fun getSession(uid: Int): Session
suspend fun insertSession(session: Session)
suspend fun updateSession(session: Session)
suspend fun deleteSession(session: Session)
}

View File

@ -0,0 +1,14 @@
package com.example.myapplication.database.entities.repository
import com.example.myapplication.database.entities.model.SessionFromCart
import com.example.myapplication.database.entities.model.User
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)
suspend fun deleteUser(user: User)
}

View File

@ -0,0 +1,11 @@
package com.example.myapplication.database.entities.repository
import com.example.myapplication.database.entities.model.UserSessionCrossRef
interface UserSessionRepository {
suspend fun insertUserSession(userSessionCrossRef: UserSessionCrossRef)
suspend fun updateUserSession(userSessionCrossRef: UserSessionCrossRef)
suspend fun deleteUserSession(userSessionCrossRef: UserSessionCrossRef)
suspend fun deleteUserSessions(userId: Int)
suspend fun deleteUserSessions(userSessionCrossRefs: List<UserSessionCrossRef>)
}

View File

@ -0,0 +1,20 @@
package com.example.myapplication.database.remotekeys.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.example.myapplication.database.remotekeys.model.RemoteKeyType
import com.example.myapplication.database.remotekeys.model.RemoteKeys
@Dao
interface RemoteKeysDao {
@Query("SELECT * FROM remote_keys WHERE entityId = :entityId AND type = :type")
suspend fun getRemoteKeys(entityId: Int, type: RemoteKeyType): RemoteKeys?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(remoteKey: List<RemoteKeys>)
@Query("DELETE FROM remote_keys WHERE type = :type")
suspend fun clearRemoteKeys(type: RemoteKeyType)
}

View File

@ -0,0 +1,28 @@
package com.example.myapplication.database.remotekeys.model
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.Order
enum class RemoteKeyType(private val type: String) {
CINEMA(Cinema::class.simpleName ?: "Cinema"),
ORDER(Order::class.simpleName ?: "Order");
@TypeConverter
fun toRemoteKeyType(value: String) = RemoteKeyType.values().first { it.type == value }
@TypeConverter
fun fromRemoteKeyType(value: RemoteKeyType) = value.type
}
@Entity(tableName = "remote_keys")
data class RemoteKeys(
@PrimaryKey val entityId: Int,
@TypeConverters(RemoteKeyType::class)
val type: RemoteKeyType,
val prevKey: Int?,
val nextKey: Int?
)

View File

@ -0,0 +1,16 @@
package com.example.myapplication.database.remotekeys.repository
import com.example.myapplication.database.remotekeys.dao.RemoteKeysDao
import com.example.myapplication.database.remotekeys.model.RemoteKeyType
import com.example.myapplication.database.remotekeys.model.RemoteKeys
class OfflineRemoteKeyRepository(private val remoteKeysDao: RemoteKeysDao) : RemoteKeyRepository {
override suspend fun getAllRemoteKeys(id: Int, type: RemoteKeyType) =
remoteKeysDao.getRemoteKeys(id, type)
override suspend fun createRemoteKeys(remoteKeys: List<RemoteKeys>) =
remoteKeysDao.insertAll(remoteKeys)
override suspend fun deleteRemoteKey(type: RemoteKeyType) =
remoteKeysDao.clearRemoteKeys(type)
}

View File

@ -0,0 +1,10 @@
package com.example.myapplication.database.remotekeys.repository
import com.example.myapplication.database.remotekeys.model.RemoteKeyType
import com.example.myapplication.database.remotekeys.model.RemoteKeys
interface RemoteKeyRepository {
suspend fun getAllRemoteKeys(id: Int, type: RemoteKeyType): RemoteKeys?
suspend fun createRemoteKeys(remoteKeys: List<RemoteKeys>)
suspend fun deleteRemoteKey(type: RemoteKeyType)
}

View File

@ -0,0 +1,47 @@
package com.example.myapplication.datastore
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
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
class DataStoreManager(private val context: Context) {
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
}
}
suspend fun setDarkTheme(darkTheme: String) {
saveStringValue(DARK_THEME, darkTheme)
}
suspend fun setLogin(login: String) {
saveStringValue(LOGIN, login)
}
}

Some files were not shown because too many files have changed in this diff Show More