Compare commits

...

6 Commits
master ... lw5

Author SHA1 Message Date
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
109 changed files with 146442 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,21 +25,25 @@ 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 {
@ -47,15 +53,42 @@ android {
} }
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")
// 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

@ -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,41 @@
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.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
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")
setContent {
PmudemoTheme(darkTheme = isDarkTheme.value) {
LaunchedEffect(key1 = true) {
dataStoreManager.getSettings().collect { setting ->
isDarkTheme.value = setting.isDarkTheme
}
}
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
MainNavbar(
isDarkTheme = isDarkTheme,
dataStoreManager = dataStoreManager
)
}
}
}
}
}

View File

@ -0,0 +1,26 @@
package com.example.myapplication.api
import androidx.room.TypeConverters
import com.example.myapplication.database.entities.model.LocalDateTimeConverter
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import org.threeten.bp.LocalDateTime
import org.threeten.bp.DateTimeUtils.toLocalDateTime
import org.threeten.bp.format.DateTimeFormatter
@Serializer(forClass = LocalDateTime::class)
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,142 @@
package com.example.myapplication.api
import com.example.myapplication.api.cinema.CinemaRemote
import com.example.myapplication.api.order.OrderRemote
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.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
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
interface MyServerService {
@GET("orders")
suspend fun getOrders(): List<OrderRemote>
@GET("users")
suspend fun getUsers(): List<UserRemote>
@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
@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("cinemas/{cinemaId}/sessions")
suspend fun getSessionsForCinema(
@Path("cinemaId") cinemaId: Int
): List<SessionFromCinemaRemote>
@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("users/{id}")
suspend fun getUserCart(
@Path("id") id: Int,
): UserRemote
@PUT("users/{id}")
suspend fun updateUserCart(
@Path("id") id: Int,
@Body userRemote: UserRemote,
): UserRemote
@GET("orders")
suspend fun getOrders(
@Query("_page") page: Int,
@Query("_limit") limit: Int,
): List<OrderRemote>
@GET("orders/{id}")
suspend fun getOrder(
@Path("id") id: Int,
): OrderRemote
@POST("orders")
suspend fun createOrder(
@Body cinema: OrderRemote,
): OrderRemote
@PUT("orders/{id}")
suspend fun updateOrder(
@Path("id") id: Int,
@Body orderRemote: OrderRemote,
): OrderRemote
companion object {
//private const val BASE_URL = "http://192.168.154.166:8079/"
private const val BASE_URL = "http://192.168.0.101:8079/"
@Volatile
private var INSTANCE: MyServerService? = null
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,121 @@
package com.example.myapplication.api.cinema
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.api.session.toSession
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 cinemasWithSessions = cinemas.map { cinema ->
service.getSessionsForCinema(cinema.uid).map {
service.getSession(it.id).toSession()
}
}
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)
cinemasWithSessions.forEach {
try {
dbSessionRepository.insertSessions(it)
} catch (_: Exception) {
}
}
}
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, 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,88 @@
package com.example.myapplication.api.cinema
import android.util.Log
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.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.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(): Flow<PagingData<Cinema>> {
Log.d(RestCinemaRepository::class.simpleName, "Get cinemas")
val pagingSourceFactory = { dbCinemaRepository.getAllCinemasPagingSource() }
@OptIn(ExperimentalPagingApi::class)
return Pager(
config = PagingConfig(
pageSize = AppContainer.LIMIT,
enablePlaceholders = false
),
remoteMediator = CinemaRemoteMediator(
service,
dbCinemaRepository,
dbSessionRepository,
dbRemoteKeyRepository,
database,
),
pagingSourceFactory = pagingSourceFactory
).flow
}
override suspend fun getCinema(uid: Int): CinemaWithSessions {
val cinema = service.getCinema(uid).toCinema()
val sessions = service.getSessionsForCinema(uid).map { x ->
SessionFromCinema(
x.id,
x.dateTime,
x.price,
x.maxCount - service.getOrders().flatMap { order ->
order.sessions.filter { session -> session.id == x.id }
}.sumOf { session -> session.count },
uid
)
}
return CinemaWithSessions(cinema, 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.getUsers()
cart.forEach { userRemote ->
userRemote.sessions = userRemote.sessions.filter { x -> x.cinemaId != cinema.uid }
service.updateUserCart(userRemote.id, userRemote)
}
val orders = service.getOrders()
orders.forEach { orderRemote ->
orderRemote.sessions = orderRemote.sessions.filter { x -> x.cinemaId != cinema.uid }
service.updateOrder(orderRemote.id, orderRemote)
}
service.deleteCinema(cinema.uid)
dbCinemaRepository.deleteCinema(cinema)
}
}

View File

@ -0,0 +1,18 @@
package com.example.myapplication.api.order
import com.example.myapplication.api.session.SessionFromOrderRemote
import com.example.myapplication.database.entities.model.Order
import kotlinx.serialization.Serializable
@Serializable
data class OrderRemote(
val id: Int = 0, val userId: Int = 0, var sessions: List<SessionFromOrderRemote> = emptyList()
)
fun OrderRemote.toOrder(): Order = Order(
id, userId
)
fun Order.toOrderRemote(): OrderRemote = OrderRemote(
uid, userId!!, sessions = emptyList()
)

View File

@ -0,0 +1,106 @@
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.api.MyServerService
import com.example.myapplication.database.AppDatabase
import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.repository.OfflineOrderRepository
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 dbRemoteKeyRepository: OfflineRemoteKeyRepository,
private val database: AppDatabase
) : RemoteMediator<Int, Order>() {
override suspend fun initialize(): InitializeAction {
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Order>
): 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 orders = service.getOrders(page, state.config.pageSize).map { it.toOrder() }
val endOfPaginationReached = orders.isEmpty()
database.withTransaction {
if (loadType == LoadType.REFRESH) {
dbRemoteKeyRepository.deleteRemoteKey(RemoteKeyType.ORDER)
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)
}
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, Order>): 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, Order>): 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, Order>
): RemoteKeys? {
return state.anchorPosition?.let { position ->
state.closestItemToPosition(position)?.uid?.let { orderUid ->
dbRemoteKeyRepository.getAllRemoteKeys(orderUid, RemoteKeyType.ORDER)
}
}
}
}

View File

@ -0,0 +1,72 @@
package com.example.myapplication.api.order
import android.util.Log
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.cinema.toCinemaRemote
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.SessionFromOrder
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.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 dbCinemaRepository: OfflineCinemaRepository,
private val dbOrderSessionRepository: OfflineOrderSessionRepository,
private val dbRemoteKeyRepository: OfflineRemoteKeyRepository,
private val database: AppDatabase
) : OrderRepository {
override fun getAllOrders(userId: Int?): Flow<PagingData<Order>> {
Log.d(RestOrderRepository::class.simpleName, "Get orders")
val pagingSourceFactory = { dbOrderRepository.getAllOrdersPagingSource(userId) }
@OptIn(ExperimentalPagingApi::class)
return Pager(
config = PagingConfig(
pageSize = AppContainer.LIMIT,
enablePlaceholders = false
),
remoteMediator = OrderRemoteMediator(
service,
dbOrderRepository,
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(dbCinemaRepository.getCinema(x.cinemaId).cinema.toCinemaRemote()) }
}
override suspend fun insertOrder(order: Order): Long {
return dbOrderRepository.insertOrder(service.createOrder(order.toOrderRemote()).toOrder())
}
}

View File

@ -0,0 +1,39 @@
package com.example.myapplication.api.ordersession
import com.example.myapplication.api.MyServerService
import com.example.myapplication.api.session.SessionFromOrderRemote
import com.example.myapplication.api.session.toSession
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) {
var orderRemote = service.getOrder(orderSessionCrossRef.orderId)
val session = service.getSession(orderSessionCrossRef.sessionId).toSession()
val sessionFromOrder = SessionFromOrderRemote(
session.uid,
session.dateTime,
session.price,
orderSessionCrossRef.count,
session.cinemaId
)
val updatedSessions = orderRemote.sessions.toMutableList()
updatedSessions.add(sessionFromOrder)
orderRemote = orderRemote.copy(sessions = updatedSessions)
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,71 @@
package com.example.myapplication.api.session
import com.example.myapplication.api.cinema.CinemaRemote
import com.example.myapplication.api.cinema.toCinema
import com.example.myapplication.api.session.SessionFromCinemaRemote
import com.example.myapplication.api.session.toSessionFromCinema
import com.example.myapplication.database.entities.model.Cinema
import com.example.myapplication.database.entities.model.CinemaWithSessions
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>,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CinemaWithSessionsRemote
if (id != other.id) return false
if (name != other.name) return false
if (description != other.description) return false
if (image != null) {
if (other.image == null) return false
if (!image.contentEquals(other.image)) return false
} else if (other.image != null) return false
if (year != other.year) return false
if (sessions != other.sessions) return false
return true
}
override fun hashCode(): Int {
var result = id
result = 31 * result + name.hashCode()
result = 31 * result + description.hashCode()
result = 31 * result + (image?.contentHashCode() ?: 0)
result = 31 * result + year.hashCode()
result = 31 * result + sessions.hashCode()
return result
}
}
fun CinemaWithSessionsRemote.toCinemaWithSessions(): CinemaWithSessions = CinemaWithSessions(
Cinema(
id,
name,
description,
image,
year
),
sessions.map { x -> x.toSessionFromCinema() }
)
fun Cinema.toCinemaWithSessionsRemote(): CinemaWithSessionsRemote = CinemaWithSessionsRemote(
uid,
name,
description,
image,
year,
sessions = emptyList()
)*/

View File

@ -0,0 +1,51 @@
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
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.getUsers()
cart.forEach { userRemote ->
userRemote.sessions = userRemote.sessions.filter { x -> x.id != session.uid }
service.updateUserCart(userRemote.id, userRemote)
}
val orders = service.getOrders()
orders.forEach { orderRemote ->
orderRemote.sessions = orderRemote.sessions.filter { x -> x.id != session.uid }
service.updateOrder(orderRemote.id, orderRemote)
}
service.deleteSession(session.uid)
dbUserSessionRepository.deleteSessionsByUid(session.uid)
dbOrderSessionRepository.deleteSessionsByUid(session.uid)
dbSessionRepository.deleteSession(session)
}
}

View File

@ -0,0 +1,20 @@
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.SessionFromCart
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import org.threeten.bp.LocalDateTime
@Serializable
class SessionFromCartRemote(
val id: Int = 0,
var count: Int = 0,
var cinemaId: Int = 0,
)
fun SessionFromCartRemote.toSessionFromCart(cinema: CinemaRemote, dateTime: LocalDateTime, price: Double, availableCount: Int): SessionFromCart =
SessionFromCart(
id, dateTime, price, availableCount, count, cinema.id, cinema.toCinema()
)

View File

@ -0,0 +1,34 @@
package com.example.myapplication.api.session
import com.example.myapplication.database.entities.model.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
)
fun SessionFromCinema.toSessionFromCinemaRemote(): SessionFromCinemaRemote = SessionFromCinemaRemote(
uid,
dateTime,
price,
availableCount,
cinemaId
)

View File

@ -0,0 +1,22 @@
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,
)
fun SessionFromOrderRemote.toSessionFromOrder(cinema: CinemaRemote): SessionFromOrder =
SessionFromOrder(
id, dateTime, frozenPrice, count, cinemaId, cinema.toCinema()
)

View File

@ -0,0 +1,34 @@
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.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,57 @@
package com.example.myapplication.api.user
import android.util.Log
import com.example.myapplication.api.MyServerService
import com.example.myapplication.api.session.toSessionFromCart
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 getCartByUser(userId: Int): List<SessionFromCart> {
val cart = service.getUserCart(userId)
dbUserSessionRepository.deleteUserSessions(userId)
cart.sessions.map { sessionFromCartRemote ->
dbUserSessionRepository.insertUserSession(
UserSessionCrossRef(
userId,
sessionFromCartRemote.id,
sessionFromCartRemote.count
)
)
}
return cart.sessions.map {
val session = service.getSession(it.id)
it.toSessionFromCart(
session.cinema,
session.dateTime,
session.price,
session.maxCount - service.getOrders().flatMap { order ->
order.sessions.filter { session -> session.id == it.id }
}.sumOf { session -> session.count })
}
}
override suspend fun insertUser(user: User) {
}
override suspend fun updateUser(user: User) {
}
override suspend fun deleteUser(user: User) {
}
}

View File

@ -0,0 +1,12 @@
package com.example.myapplication.api.user
import com.example.myapplication.api.session.SessionFromCartRemote
import kotlinx.serialization.Serializable
@Serializable
data class UserRemote(
val id: Int = 0,
val login: String = "",
val password: String = "",
var sessions: List<SessionFromCartRemote> = emptyList()
)

View File

@ -0,0 +1,62 @@
package com.example.myapplication.api.usersession
import com.example.myapplication.api.MyServerService
import com.example.myapplication.api.session.SessionFromCartRemote
import com.example.myapplication.api.session.toSession
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) {
var cartSessions = service.getUserCart(userSessionCrossRef.userId)
cartSessions.sessions.forEach { session ->
if (session.id == userSessionCrossRef.sessionId)
return
}
val session = service.getSession(userSessionCrossRef.sessionId).toSession()
val sessionFromCart = SessionFromCartRemote(
session.uid,
userSessionCrossRef.count,
session.cinemaId,
)
val updatedSessions = cartSessions.sessions.toMutableList()
updatedSessions.add(sessionFromCart)
cartSessions = cartSessions.copy(sessions = updatedSessions)
service.updateUserCart(userSessionCrossRef.userId, cartSessions)
dbUserSessionRepository.insertUserSession(userSessionCrossRef)
}
override suspend fun updateUserSession(userSessionCrossRef: UserSessionCrossRef) {
val userRemote = service.getUserCart(userSessionCrossRef.userId)
if (userSessionCrossRef.count <= 0) {
userRemote.sessions =
userRemote.sessions.filter { x -> x.id != userSessionCrossRef.sessionId }
} else
userRemote.sessions.forEach {
if (it.id == userSessionCrossRef.sessionId) {
it.count = userSessionCrossRef.count
}
}
service.updateUserCart(userSessionCrossRef.userId, userRemote)
dbUserSessionRepository.updateUserSession(userSessionCrossRef)
}
override suspend fun deleteUserSession(userSessionCrossRef: UserSessionCrossRef) {
updateUserSession(userSessionCrossRef)
dbUserSessionRepository.deleteUserSession(userSessionCrossRef)
}
override suspend fun deleteUserSessions(userId: Int) {
val userRemote = service.getUserCart(userId)
userRemote.sessions = emptyList()
service.updateUserCart(userId, userRemote)
dbUserSessionRepository.deleteUserSessions(userId)
}
}

View File

@ -0,0 +1,317 @@
package com.example.myapplication.composeui
import android.content.res.Configuration
import android.graphics.BitmapFactory
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
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.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.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.outlined.Delete
import androidx.compose.material3.Button
import androidx.compose.material3.DismissDirection
import androidx.compose.material3.DismissState
import androidx.compose.material3.DismissValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDismissState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.myapplication.R
import com.example.myapplication.database.entities.composeui.AppViewModelProvider
import com.example.myapplication.database.entities.composeui.CartUiState
import com.example.myapplication.database.entities.composeui.CartViewModel
import com.example.myapplication.database.entities.model.Session
import com.example.myapplication.database.entities.model.SessionFromCart
import com.example.myapplication.ui.theme.PmudemoTheme
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.threeten.bp.format.DateTimeFormatter
@Composable
fun Cart(
viewModel: CartViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val coroutineScope = rememberCoroutineScope()
val cartUiState = viewModel.cartUiState
LaunchedEffect(Unit) {
viewModel.refreshState()
}
Cart(
cartUiState = cartUiState,
modifier = Modifier
.padding(all = 10.dp),
onSwipe = { session: SessionFromCart, user: Int ->
coroutineScope.launch {
viewModel.removeFromCart(
session = Session(
uid = session.uid,
dateTime = session.dateTime,
price = session.price,
maxCount = 0,
cinemaId = session.cinemaId
), user = user
)
}
},
onChangeCount = { session: SessionFromCart, user: Int, count: Int ->
coroutineScope.launch {
viewModel.updateFromCart(
session = Session(
uid = session.uid,
dateTime = session.dateTime,
price = session.price,
maxCount = 0,
cinemaId = session.cinemaId
), userId = user, count = count, availableCount = session.availableCount
)
}
},
onAddToOrder = { sessions: List<SessionFromCart>, user: Int ->
coroutineScope.launch {
viewModel.addToOrder(sessions = sessions, userId = user)
}
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun Cart(
cartUiState: CartUiState,
modifier: Modifier,
onSwipe: (SessionFromCart, Int) -> Unit,
onChangeCount: (SessionFromCart, Int, Int) -> Unit,
onAddToOrder: (List<SessionFromCart>, Int) -> Unit
) {
LazyColumn(
modifier = modifier
) {
items(cartUiState.sessionList, key = { it.uid.toString() }) { session ->
val dismissState: DismissState = rememberDismissState(
positionalThreshold = { 200.dp.toPx() }
)
if (dismissState.isDismissed(direction = DismissDirection.EndToStart)) {
onSwipe(session, 1)
}
SwipeToDelete(
dismissState = dismissState,
session = session,
onChangeCount = onChangeCount
)
}
}
Column {
Spacer(modifier = Modifier.weight(1f))
Button(
onClick = { onAddToOrder(cartUiState.sessionList, 1) },
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) { Text("Купить") }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SwipeToDelete(
dismissState: DismissState,
session: SessionFromCart,
onChangeCount: (SessionFromCart, Int, Int) -> Unit,
) {
SwipeToDismiss(
state = dismissState,
directions = setOf(
DismissDirection.EndToStart
),
background = {
val backgroundColor by animateColorAsState(
when (dismissState.targetValue) {
DismissValue.DismissedToStart -> Color.Red.copy(alpha = 0.8f)
else -> MaterialTheme.colorScheme.background
},
label = ""
)
val iconScale by animateFloatAsState(
targetValue = if (dismissState.targetValue == DismissValue.DismissedToStart) 1.3f else 0.5f,
label = ""
)
Box(
Modifier
.fillMaxSize()
.background(color = backgroundColor)
.padding(end = 16.dp),
contentAlignment = Alignment.CenterEnd
) {
Icon(
modifier = Modifier.scale(iconScale),
imageVector = Icons.Outlined.Delete,
contentDescription = "Delete",
tint = Color.White
)
}
},
dismissContent = {
SessionListItem(
session = session,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.secondary),
onChangeCount = onChangeCount
)
}
)
}
@Composable
private fun SessionListItem(
session: SessionFromCart,
modifier: Modifier = Modifier,
onChangeCount: (SessionFromCart, Int, Int) -> Unit,
) {
var currentCount by remember { mutableStateOf(session.count) }
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
) {
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),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "${session.cinema.name}, ${session.cinema.year}\n" +
"Цена: ${session.price}\n" +
"${currentCount}/${session.availableCount}",
color = MaterialTheme.colorScheme.onSecondary
)
}
Box(
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.background,
shape = RoundedCornerShape(10.dp)
) // Задаем фон для кнопок
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = { onChangeCount(session, 1, --currentCount) }
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.minus),
contentDescription = "Уменьшить",
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.size(10.dp)
)
}
Text(
text = "$currentCount",
color = MaterialTheme.colorScheme.onBackground
)
IconButton(
onClick = {
onChangeCount(
session,
1,
if (currentCount != session.availableCount) ++currentCount else currentCount
)
}
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Увеличить",
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.size(10.dp)
)
}
}
}
}
}
}
}
@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 CartPreview() {
PmudemoTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
Cart()
}
}
}

View File

@ -0,0 +1,233 @@
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.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ExperimentalMaterial3Api
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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.composeui.Cart
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.datastore.DataStoreManager
@Composable
fun Topbar(
navController: NavHostController,
currentScreen: Screen?
) {
var searchQuery by remember { mutableStateOf("") }
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))
BasicTextField(
value = searchQuery,
onValueChange = { newValue -> searchQuery = newValue },
modifier = Modifier
.weight(1f)
.height(36.dp)
.background(
color = MaterialTheme.colorScheme.onPrimary,
RoundedCornerShape(18.dp)
)
.padding(start = 13.dp, top = 8.dp),
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = androidx.compose.ui.text.input.ImeAction.Search
),
keyboardActions = KeyboardActions(
onSearch = { }
)
)
Spacer(modifier = Modifier.width(16.dp))
Icon(
imageVector = Icons.Default.Search,
contentDescription = null,
modifier = Modifier
.size(30.dp)
.clickable { },
tint = MaterialTheme.colorScheme.secondary
)
}
}
}
@Composable
fun Navbar(
navController: NavHostController,
currentDestination: NavDestination?,
modifier: Modifier = Modifier
) {
NavigationBar(modifier = modifier, containerColor = MaterialTheme.colorScheme.primary) {
Screen.bottomBarItems.forEach { screen ->
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, 1) }
composable(Screen.Cart.route) { Cart() }
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(it.getInt("id")) }
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@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,58 @@
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
),
SessionList(
"Session-list", R.string.Sessions_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
);
companion object {
val bottomBarItems = listOf(
CinemaList,
Cart,
OrderList
)
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,
cinemaRepository,
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,196 @@
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 org.threeten.bp.LocalDateTime
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,64 @@
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.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,
)
}
initializer {
SessionListViewModel(
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,
)
}
}
}
fun CreationExtras.cinemaApplication(): CinemaApplication =
(this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as CinemaApplication)

View File

@ -0,0 +1,70 @@
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.ViewModel
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 kotlinx.coroutines.delay
class CartViewModel(
private val userSessionRepository: UserSessionRepository,
private val orderRepository: OrderRepository,
private val orderSessionRepository: OrderSessionRepository,
private val userRepository: UserRepository,
) : ViewModel() {
private val userUid: Int = 1
var cartUiState by mutableStateOf(CartUiState())
private set
suspend fun refreshState() {
val cart = userRepository.getCartByUser(userUid)
cartUiState = CartUiState(cart)
}
suspend fun addToOrder(userId: Int, sessions: List<SessionFromCart>) {
if (sessions.isEmpty())
return
val orderId = orderRepository.insertOrder(Order(0, userId))
sessions.forEach { session ->
orderSessionRepository.insertOrderSession(
OrderSessionCrossRef(
orderId.toInt(),
session.uid,
session.price,
session.count
)
)
}
userSessionRepository.deleteUserSessions(userId)
refreshState()
}
suspend fun removeFromCart(user: Int, session: Session, count: Int = 1) {
userSessionRepository.deleteUserSession(UserSessionCrossRef(user, session.uid, count))
refreshState()
}
suspend fun updateFromCart(userId: Int, session: Session, count: Int, availableCount: Int)
: Boolean {
if (count == 0) {
removeFromCart(userId, session, count)
return false
}
if (count > availableCount)
return false
userSessionRepository.updateUserSession(UserSessionCrossRef(userId, session.uid, count))
refreshState()
return true
}
}
data class CartUiState(val sessionList: List<SessionFromCart> = listOf())

View File

@ -0,0 +1,187 @@
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.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.composeui.navigation.Screen
import com.example.myapplication.database.entities.model.Cinema
import kotlinx.coroutines.launch
@Composable
fun CinemaList(
navController: NavController,
viewModel: CinemaListViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val coroutineScope = rememberCoroutineScope()
val cinemaPagingItems = viewModel.cinemaListUiState.collectAsLazyPagingItems()
Scaffold(
topBar = {},
floatingActionButton = {
FloatingActionButton(
onClick = {
val route = Screen.CinemaEdit.route.replace("{id}", 0.toString())
navController.navigate(route)
},
containerColor = MaterialTheme.colorScheme.primary,
) {
Icon(
Icons.Filled.Add,
"Добавить",
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
) { 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
) {
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
)
// Добавляем пустое пространство для разделения текста и кнопки
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,23 @@
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.ViewModel
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
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
) : ViewModel() {
val cinemaListUiState: Flow<PagingData<Cinema>> = cinemaRepository.getAllCinemas()
suspend fun deleteCinema(cinema: Cinema) {
cinemaRepository.deleteCinema(cinema)
}
}

View File

@ -0,0 +1,140 @@
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.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.composeui.navigation.Screen
import com.example.myapplication.database.entities.model.Cinema
@Composable
fun CinemaView(
navController: NavController,
viewModel: CinemaViewModel = viewModel(factory = AppViewModelProvider.Factory),
) {
val cinemaUiState = viewModel.cinemaUiState
LaunchedEffect(Unit) {
viewModel.refreshState()
}
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)
)
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)
}
}
}

View File

@ -0,0 +1,45 @@
package com.example.myapplication.database.entities.composeui
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.example.myapplication.database.entities.model.CinemaWithSessions
import com.example.myapplication.database.entities.repository.CinemaRepository
class CinemaViewModel(
savedStateHandle: SavedStateHandle, private val cinemaRepository: CinemaRepository
) : ViewModel() {
private val cinemaUid: Int = checkNotNull(savedStateHandle["id"])
var cinemaUiState by mutableStateOf(CinemaUiState())
private set
suspend fun refreshState() {
if (cinemaUid > 0) {
cinemaUiState = CinemaUiState(cinemaRepository.getCinema(cinemaUid))
}
}
// init {
// viewModelScope.launch {
// if (cinemaUid > 0) {
// cinemaUiState = CinemaUiState(cinemaRepository.getCinema(cinemaUid))
// }
// }
// }
// val cinemaUiState: mutableStateOf(CinemaUiState()) = cinemaRepository.getCinema(
// cinemaUid
// ).map
// {
// CinemaUiState(it)
// }.stateIn(
// scope = viewModelScope,
// started = SharingStarted.WhileSubscribed(stopTimeoutMillis = AppDataContainer.TIMEOUT),
// initialValue = CinemaUiState()
// )
}
data class CinemaUiState(val cinemaWithSessions: CinemaWithSessions? = null)

View File

@ -0,0 +1,87 @@
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.lazy.items
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.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
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.composeui.navigation.Screen
import com.example.myapplication.ui.theme.PmudemoTheme
@Composable
fun OrderList(
navController: NavController?,
userId: Int?,
viewModel: OrderListViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val coroutineScope = rememberCoroutineScope()
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 orderId = Screen.OrderView.route.replace("{id}", order!!.uid.toString())
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)
) {
Text("Заказ №${order.uid}", 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, 1)
}
}
}

View File

@ -0,0 +1,25 @@
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.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import com.example.myapplication.database.AppDataContainer
import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.repository.OrderRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class OrderListViewModel(
private val orderRepository: OrderRepository
) : ViewModel() {
val orderListUiState: Flow<PagingData<Order>> = orderRepository.getAllOrders(1)
}
data class OrderListUiState(val orderList: List<Order> = listOf())

View File

@ -0,0 +1,114 @@
package com.example.myapplication.database.entities.composeui
import android.content.res.Configuration
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.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.myapplication.ui.theme.PmudemoTheme
import org.threeten.bp.format.DateTimeFormatter
@Composable
fun OrderView(
id: Int,
viewModel: OrderViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val coroutineScope = rememberCoroutineScope()
val orderUiState by viewModel.orderUiState.collectAsState()
LazyColumn(
modifier = Modifier
.padding(10.dp)
) {
items(orderUiState.sessionList) { session ->
val count = remember { mutableStateOf(session.count) }
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val formattedDate = dateFormatter.format(session.dateTime)
Text(
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.value}",
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 OrderViewPreview() {
PmudemoTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
OrderView(id = 1)
}
}
}

View File

@ -0,0 +1,31 @@
package com.example.myapplication.database.entities.composeui
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.myapplication.database.AppContainer
import com.example.myapplication.database.AppDataContainer
import com.example.myapplication.database.entities.model.SessionFromOrder
import com.example.myapplication.database.entities.repository.OrderRepository
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
class OrderViewModel(
savedStateHandle: SavedStateHandle,
private val orderRepository: OrderRepository
) : ViewModel() {
private val orderUid: Int = checkNotNull(savedStateHandle["id"])
val orderUiState: StateFlow<OrderUiState> = flow{emit(orderRepository.getOrder(orderUid))} .map {
OrderUiState(it)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = AppContainer.TIMEOUT),
initialValue = OrderUiState()
)
}
data class OrderUiState(val sessionList: List<SessionFromOrder> = listOf())

View File

@ -0,0 +1,144 @@
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.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.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.example.myapplication.R
import com.example.myapplication.composeui.navigation.Screen
import kotlinx.coroutines.launch
import org.threeten.bp.format.DateTimeFormatter
@Composable
fun SessionList(
cinemaWithSessionsViewModel: CinemaViewModel,
navController: NavController,
viewModel: SessionListViewModel = viewModel(factory = AppViewModelProvider.Factory)
) {
val coroutineScope = rememberCoroutineScope()
val cinemaWithSessions = cinemaWithSessionsViewModel.cinemaUiState.cinemaWithSessions!!
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 {
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
)
}
IconButton(
onClick = {
coroutineScope.launch {
if (session.availableCount != 0)
viewModel.addSessionInCart(sessionId = session.uid)
}
},
) {
Icon(
imageVector = Icons.Filled.ShoppingCart,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSecondary
)
}
IconButton(
onClick = {
coroutineScope.launch {
viewModel.deleteSession(session = session)
cinemaWithSessionsViewModel.refreshState()
}
},
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSecondary
)
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,33 @@
package com.example.myapplication.database.entities.composeui
import androidx.lifecycle.ViewModel
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.SessionRepository
import com.example.myapplication.database.entities.repository.UserSessionRepository
class SessionListViewModel(
private val sessionRepository: SessionRepository,
private val userSessionRepository: UserSessionRepository
) : ViewModel() {
suspend fun deleteSession(session: SessionFromCinema) {
sessionRepository.deleteSession(
Session(
uid = session.uid,
dateTime = session.dateTime,
price = session.price,
maxCount = 0,
cinemaId = 0
)
)
}
suspend fun addSessionInCart(sessionId: Int, count: Int = 1) {
try {
userSessionRepository.insertUserSession(UserSessionCrossRef(1, sessionId, count))
} catch (_: Exception) {
}
}
}

View File

@ -0,0 +1,165 @@
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.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import com.example.myapplication.datastore.DataStoreManager
import com.example.myapplication.datastore.SettingData
import kotlinx.coroutines.launch
@Composable
fun UserProfile(
isDarkTheme: MutableState<Boolean>,
dataStoreManager: DataStoreManager
) {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var isRegistration by remember { mutableStateOf(false) }
LazyColumn {
item {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Логин",
modifier = Modifier.align(Alignment.CenterHorizontally)
)
BasicTextField(
value = username,
onValueChange = { newValue -> username = newValue },
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 = password,
onValueChange = { newValue -> password = newValue },
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 = { },
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Text("Регистрация")
}
Text(
text = "Уже есть аккаунт? Войти",
modifier = Modifier
.clickable {
isRegistration = false
}
.align(Alignment.CenterHorizontally),
color = MaterialTheme.colorScheme.onBackground
)
} else {
Button(
onClick = { },
modifier = Modifier
.fillMaxWidth()
.padding(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)
)
val coroutine = rememberCoroutineScope()
Switch(
checked = isDarkTheme.value,
onCheckedChange = {
isDarkTheme.value = !isDarkTheme.value
coroutine.launch {
dataStoreManager.saveSettings(SettingData(isDarkTheme = isDarkTheme.value))
}
},
colors = switchColors
)
}
}
}
}
}
/*@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun UserProfilePreview() {
PmudemoTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
UserProfile(navController = null, isDarkTheme = remember { mutableStateOf(true) })
}
}
}*/

View File

@ -0,0 +1,127 @@
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.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()
CinemaEdit(
cinemaUiState = viewModel.cinemaUiState,
onClick = {
coroutineScope.launch {
viewModel.saveCinema()
navController.popBackStack()
}
},
onUpdate = viewModel::updateUiState,
)
}
@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,
shape = MaterialTheme.shapes.small,
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,99 @@
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 androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class CinemaEditViewModel(
savedStateHandle: SavedStateHandle,
private val cinemaRepository: CinemaRepository
) : ViewModel() {
var cinemaUiState by mutableStateOf(CinemaUiState())
private set
private val cinemaUid: Int = checkNotNull(savedStateHandle["id"])
init {
viewModelScope.launch {
if (cinemaUid > 0) {
cinemaUiState = cinemaRepository.getCinema(cinemaUid)
.toUiState(true)
}
}
}
fun updateUiState(cinemaDetails: CinemaDetails) {
cinemaUiState = CinemaUiState(
cinemaDetails = cinemaDetails,
isEntryValid = validateInput(cinemaDetails)
)
}
suspend fun saveCinema() {
if (validateInput()) {
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,150 @@
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.MaterialTheme
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.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()
SessionEdit(
sessionUiState = viewModel.sessionUiState,
onClick = {
coroutineScope.launch {
viewModel.saveSession()
navController.popBackStack()
}
},
onUpdate = viewModel::updateUiState
)
}
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,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(R.string.Save_button))
}
}
}
}

View File

@ -0,0 +1,108 @@
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 androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.myapplication.database.entities.model.Session
import com.example.myapplication.database.entities.repository.SessionRepository
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.threeten.bp.LocalDateTime
class SessionEditViewModel(
savedStateHandle: SavedStateHandle,
private val sessionRepository: SessionRepository
) : ViewModel() {
var sessionUiState by mutableStateOf(SessionUiState())
private set
private val sessionUid: Int = checkNotNull(savedStateHandle["id"])
private val cinemaUid: Int = checkNotNull(savedStateHandle["cinemaId"])
init {
viewModelScope.launch {
if (sessionUid > 0) {
sessionUiState = sessionRepository.getSession(sessionUid)
.toUiState(true)
}
}
}
fun updateUiState(sessionDetails: SessionDetails) {
sessionUiState = SessionUiState(
sessionDetails = sessionDetails,
isEntryValid = validateInput(sessionDetails)
)
}
suspend fun saveSession() {
if (validateInput()) {
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,39 @@
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.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 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
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,37 @@
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.Query
import androidx.room.Update
import com.example.myapplication.database.entities.model.Order
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, Order>
@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
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,35 @@
package com.example.myapplication.database.entities.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
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
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,38 @@
package com.example.myapplication.database.entities.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
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 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
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(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,37 @@
package com.example.myapplication.database.entities.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
tableName = "orders", foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["uid"],
childColumns = ["user_id"],
onDelete = ForeignKey.RESTRICT,
onUpdate = ForeignKey.RESTRICT
)
]
)
data class Order(
@PrimaryKey(autoGenerate = true)
val uid: Int,
@ColumnInfo(name = "user_id", index = true)
val userId: Int?,
) {
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,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,24 @@
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
) {
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,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,15 @@
package com.example.myapplication.database.entities.repository
import androidx.paging.PagingData
import androidx.paging.PagingSource
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(): 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,50 @@
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(): Flow<PagingData<Cinema>> = Pager(
config = PagingConfig(
pageSize = AppContainer.LIMIT,
enablePlaceholders = false
),
pagingSourceFactory = cinemaDao::getAll
).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()
suspend fun insertCinemas(cinemas: List<Cinema>) =
cinemaDao.insert(*cinemas.toTypedArray())
suspend fun clearCinemas() = cinemaDao.deleteAll()
}

View File

@ -0,0 +1,31 @@
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.OrderDao
import com.example.myapplication.database.entities.model.Order
import com.example.myapplication.database.entities.model.SessionFromOrder
import kotlinx.coroutines.flow.Flow
class OfflineOrderRepository(private val orderDao: OrderDao) : OrderRepository {
override fun getAllOrders(userId: Int?): Flow<PagingData<Order>> = Pager(
config = PagingConfig(
pageSize = AppContainer.LIMIT,
enablePlaceholders = false
),
pagingSourceFactory = { orderDao.getAll(userId) }
).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(userId: Int?): PagingSource<Int, Order> = orderDao.getAll(userId)
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,24 @@
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 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,20 @@
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)
suspend fun deleteSessionsByUid(sessionId: Int) = userSessionDao.deleteBySessionUid(sessionId)
}

View File

@ -0,0 +1,12 @@
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.SessionFromOrder
import kotlinx.coroutines.flow.Flow
interface OrderRepository {
fun getAllOrders(userId: Int?): Flow<PagingData<Order>>
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,11 @@
package com.example.myapplication.database.entities.repository
import com.example.myapplication.database.entities.model.Session
import kotlinx.coroutines.flow.Flow
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,13 @@
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 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,10 @@
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)
}

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,30 @@
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
import com.example.myapplication.database.entities.model.Session
enum class RemoteKeyType(private val type: String) {
CINEMA(Cinema::class.simpleName ?: "Cinema"),
ORDER(Order::class.simpleName ?: "Order"),
SESSION(Session::class.simpleName ?: "Session");
@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,25 @@
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.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.map
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore("data_store")
class DataStoreManager(private val context: Context) {
suspend fun saveSettings(settingData: SettingData) {
context.dataStore.edit { pref ->
pref[booleanPreferencesKey("isDarkTheme")] = settingData.isDarkTheme
}
}
fun getSettings() = context.dataStore.data.map { pref ->
return@map SettingData(
pref[booleanPreferencesKey("isDarkTheme")] ?: true
)
}
}

View File

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

View File

@ -0,0 +1,11 @@
package com.example.myapplication.ui.theme
import androidx.compose.ui.graphics.Color
val LightGray = Color(0xFFB2CCD6)
val LightBlueGray = Color(0xFF70A3B2)
val LightBgGray = Color(0xFFCED6DC)
val Gray = Color(0xFFD6D6D6)
val DarkGray = Color(0xFF191A1F)
val BgGray = Color(0xFF2A2D32)

View File

@ -10,21 +10,31 @@ import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme( private val DarkColorScheme = darkColorScheme(
primary = Purple80, primary = DarkGray,
secondary = PurpleGrey80, onPrimary = Color.White,
tertiary = Pink80
secondary = Gray,
onSecondary = Color.Black,
background = BgGray,
onBackground = Color.White,
) )
private val LightColorScheme = lightColorScheme( private val LightColorScheme = lightColorScheme(
primary = Purple40, primary = LightBlueGray,
secondary = PurpleGrey40, onPrimary = Color.White,
tertiary = Pink40
secondary = LightGray,
onSecondary = Color.Black,
onBackground = Color.Black,
/* Other default colors to override /* Other default colors to override
background = Color(0xFFFFFBFE), background = Color(0xFFFFFBFE),
@ -38,10 +48,10 @@ private val LightColorScheme = lightColorScheme(
) )
@Composable @Composable
fun MyApplicationTheme( fun PmudemoTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+ // Dynamic color is available on Android 12+
dynamicColor: Boolean = true, dynamicColor: Boolean = false,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val colorScheme = when { val colorScheme = when {

View File

@ -1,11 +0,0 @@
package com.example.myapplication.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M19,13H5v-2h14v2z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -1,3 +1,22 @@
<resources> <resources>
<string name="app_name">My Application</string> <string name="app_name">pmu-demo</string>
<string name="Cinema_main_title">Фильмы</string>
<string name="Cinema_view_title">Фильм</string>
<string name="Session_view_title">Сеанс</string>
<string name="Order_view_title">Заказ</string>
<string name="Cinema_name">Название</string>
<string name="Cinema_year">Год</string>
<string name="Cinema_description">Описание</string>
<string name="Cinema_image">Изображение</string>
<string name="Cart_title">Корзина</string>
<string name="Order_title">Мои заказы</string>
<string name="Profile_title">Профиль</string>
<string name="Sessions_title">Сеансы</string>
<string name="Session_dateTime">Время</string>
<string name="Save_button">Сохранить</string>
<string name="Cinema_empty_description">Записи о фильмах отсутствуют</string>
<string name="Session_empty_description">Записи о сеансах отсутствуют</string>
<string name="session_cinema_not_select">Фильм не указан</string>
<string name="size">Размер загруженного изображения: %1$dx%2$d</string>
<string name="not_uploaded">Загрузите изображение</string>
</resources> </resources>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.MyApplication" parent="android:Theme.Material.Light.NoActionBar" /> <style name="Theme.Pmudemo" parent="android:Theme.Material.Light.NoActionBar" />
</resources> </resources>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<!-- <domain includeSubdomains="true">192.168.154.166</domain>-->
<domain includeSubdomains="true">192.168.0.101</domain>
</domain-config>
</network-security-config>

View File

@ -1,9 +1,8 @@
package com.example.myapplication package com.example.myapplication
import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.junit.Assert.*
/** /**
* Example local unit test, which will execute on the development machine (host). * Example local unit test, which will execute on the development machine (host).
* *

View File

@ -1,5 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id("com.android.application") version "8.1.1" apply false id("com.android.application") version "8.1.2" apply false
id("org.jetbrains.kotlin.android") version "1.8.10" apply false id("org.jetbrains.kotlin.android") version "1.8.20" apply false
id("com.google.devtools.ksp") version "1.8.20-1.0.11" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "1.8.20" apply false
} }

View File

@ -1,4 +1,4 @@
#Mon Sep 18 13:48:19 GMT+04:00 2023 #Sun Oct 01 15:07:08 GMT+04:00 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip

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