commit 365a84b1a8c3d4443e6f62357f351d29807b3145 Author: Stranni15k Date: Mon Jan 8 22:10:47 2024 +0400 coursework diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de25127 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +.idea \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..0384dbe --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,98 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("com.google.devtools.ksp") + id("org.jetbrains.kotlin.plugin.serialization") +} + +android { + namespace = "ru.ulstu.is.pmu" + compileSdk = 34 + + defaultConfig { + applicationId = "ru.ulstu.is.pmu" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.4.5" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} +kotlin { + jvmToolchain(17) +} + +dependencies { + // Core + implementation("androidx.core:core-ktx:1.9.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") + + // UI + implementation("androidx.activity:activity-compose:1.7.2") + 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-graphics") + implementation("androidx.compose.ui:ui-tooling-preview") + implementation("androidx.compose.material3:material3:1.1.2") + implementation("androidx.compose.material:material:1.4.3") + implementation ("com.itextpdf:itextpdf:5.5.13") + + + // Room + val roomVersion = "2.5.2" + implementation("androidx.room:room-runtime:$roomVersion") + annotationProcessor("androidx.room:room-compiler:$roomVersion") + ksp("androidx.room:room-compiler:$roomVersion") + implementation("androidx.room:room-ktx:$roomVersion") + implementation("androidx.room:room-paging:$roomVersion") + + // 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") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00")) + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-tooling") + debugImplementation("androidx.compose.ui:ui-test-manifest") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/ru/ulstu/is/pmu/ExampleInstrumentedTest.kt b/app/src/androidTest/java/ru/ulstu/is/pmu/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..abd2614 --- /dev/null +++ b/app/src/androidTest/java/ru/ulstu/is/pmu/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package ru.ulstu.`is`.pmu + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("ru.ulstu.is.pmu", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..602dc6f --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/MainComposeActivity.kt b/app/src/main/java/ru/ulstu/is/pmu/MainComposeActivity.kt new file mode 100644 index 0000000..9e1e3cf --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/MainComposeActivity.kt @@ -0,0 +1,27 @@ +package ru.ulstu.`is`.pmu + +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.ui.Modifier +import ru.ulstu.`is`.pmu.ui.navigation.MainNavbar +import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme + +class MainComposeActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + PmudemoTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + MainNavbar() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/StudentApplication.kt b/app/src/main/java/ru/ulstu/is/pmu/StudentApplication.kt new file mode 100644 index 0000000..61af241 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/StudentApplication.kt @@ -0,0 +1,14 @@ +package ru.ulstu.`is`.pmu + +import android.app.Application +import ru.ulstu.`is`.pmu.common.AppContainer +import ru.ulstu.`is`.pmu.common.AppDataContainer + +class StudentApplication : Application() { + lateinit var container: AppContainer + + override fun onCreate() { + super.onCreate() + container = AppDataContainer(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/api/ApiStatus.kt b/app/src/main/java/ru/ulstu/is/pmu/api/ApiStatus.kt new file mode 100644 index 0000000..522877b --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/api/ApiStatus.kt @@ -0,0 +1,3 @@ +package ru.ulstu.`is`.pmu.api + +enum class ApiStatus { LOADING, ERROR, DONE } \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/api/MyServerService.kt b/app/src/main/java/ru/ulstu/is/pmu/api/MyServerService.kt new file mode 100644 index 0000000..f1e7671 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/api/MyServerService.kt @@ -0,0 +1,94 @@ +package ru.ulstu.`is`.pmu.api + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlinx.serialization.json.Json +import okhttp3.Interceptor.* +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query +import ru.ulstu.`is`.pmu.api.model.UserRemote +import ru.ulstu.`is`.pmu.api.model.PetRemote +import ru.ulstu.`is`.pmu.api.report.ReportRemote + + +interface MyServerService { + + @GET("report") + suspend fun getReportInfo( + @Query("fromDate") fromDate: String, + @Query("toDate") toDate: String + ): List + + @GET("users") + suspend fun getUsers(): List + + @GET("users/{id}") + suspend fun getUser( + @Path("id") id: Int, + ): UserRemote + + @GET("users") + suspend fun getUsers( + @Query("_page") page: Int, + @Query("_limit") limit: Int, + ): List + + @GET("pets") + suspend fun getPets( + @Query("_page") page: Int, + @Query("_limit") limit: Int, + ): List + + @GET("pets/{id}") + suspend fun getPet( + @Path("id") id: Int, + ): PetRemote + + @POST("pets") + suspend fun createPet( + @Body pet: PetRemote, + ): PetRemote + + @PUT("pets/{id}") + suspend fun updatePet( + @Path("id") id: Int, + @Body pet: PetRemote, + ): PetRemote + + @DELETE("pets/{id}") + suspend fun deletePet( + @Path("id") id: Int, + ): PetRemote + + companion object { + private const val BASE_URL = "http://10.0.2.2: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() + return Retrofit.Builder() + .baseUrl(BASE_URL) + .client(client) + .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .build() + .create(MyServerService::class.java) + .also { INSTANCE = it } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/api/model/PetRemote.kt b/app/src/main/java/ru/ulstu/is/pmu/api/model/PetRemote.kt new file mode 100644 index 0000000..22dc1c2 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/api/model/PetRemote.kt @@ -0,0 +1,32 @@ +package ru.ulstu.`is`.pmu.api.model + +import kotlinx.serialization.Serializable +import ru.ulstu.`is`.pmu.database.entyties.model.Pet + +@Serializable +data class PetRemote( + val id: Int = 0, + val name: String = "", + val note: String = "", + val birthday: String = "", + val userId: Int=0, + val imageResourceId: Int=0 +) + +fun PetRemote.toPet(): Pet = Pet( + id, + name, + note, + birthday, + userId, + imageResourceId +) + +fun Pet.toPetRemote(): PetRemote = PetRemote( + uid, + name, + note, + birthday, + userId, + imageResourceId +) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/api/model/UserRemote.kt b/app/src/main/java/ru/ulstu/is/pmu/api/model/UserRemote.kt new file mode 100644 index 0000000..8ad3311 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/api/model/UserRemote.kt @@ -0,0 +1,19 @@ +package ru.ulstu.`is`.pmu.api.model + +import kotlinx.serialization.Serializable +import ru.ulstu.`is`.pmu.database.entyties.model.User + +@Serializable +data class UserRemote( + val id: Int = 0, + val login: String, + val nameFirst: String, + val nameLast: String +) + +fun UserRemote.toUser(): User = User( + id, + login, + nameFirst, + nameLast +) diff --git a/app/src/main/java/ru/ulstu/is/pmu/api/pet/PetRemoteMediator.kt b/app/src/main/java/ru/ulstu/is/pmu/api/pet/PetRemoteMediator.kt new file mode 100644 index 0000000..61eb178 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/api/pet/PetRemoteMediator.kt @@ -0,0 +1,111 @@ +package ru.ulstu.`is`.pmu.api.pet + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import androidx.room.withTransaction +import retrofit2.HttpException +import ru.ulstu.`is`.pmu.api.MyServerService +import ru.ulstu.`is`.pmu.api.model.toPet +import ru.ulstu.`is`.pmu.api.user.RestUserRepository +import ru.ulstu.`is`.pmu.database.AppDatabase +import ru.ulstu.`is`.pmu.database.pet.repository.OfflinePetRepository +import ru.ulstu.`is`.pmu.database.remotekeys.model.RemoteKeyType +import ru.ulstu.`is`.pmu.database.remotekeys.model.RemoteKeys +import ru.ulstu.`is`.pmu.database.remotekeys.repository.OfflineRemoteKeyRepository +import ru.ulstu.`is`.pmu.database.entyties.model.Pet + +import java.io.IOException + +@OptIn(ExperimentalPagingApi::class) +class PetRemoteMediator( + private val service: MyServerService, + private val dbPetRepository: OfflinePetRepository, + private val dbRemoteKeyRepository: OfflineRemoteKeyRepository, + private val userRestRepository: RestUserRepository, + private val database: AppDatabase +) : RemoteMediator() { + + override suspend fun initialize(): InitializeAction { + return InitializeAction.LAUNCH_INITIAL_REFRESH + } + + override suspend fun load( + loadType: LoadType, + state: PagingState + ): 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 pets = service.getPets(page, state.config.pageSize).map { it.toPet() } + val endOfPaginationReached = pets.isEmpty() + database.withTransaction { + if (loadType == LoadType.REFRESH) { + dbRemoteKeyRepository.deleteRemoteKey(RemoteKeyType.PET) + dbPetRepository.clearPets() + } + val prevKey = if (page == 1) null else page - 1 + val nextKey = if (endOfPaginationReached) null else page + 1 + val keys = pets.map { + RemoteKeys( + entityId = it.uid, + type = RemoteKeyType.PET, + prevKey = prevKey, + nextKey = nextKey + ) + } + userRestRepository.getAllInList() + dbRemoteKeyRepository.createRemoteKeys(keys) + dbPetRepository.insertPets(pets) + } + 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): RemoteKeys? { + return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull() + ?.let { pet -> + dbRemoteKeyRepository.getAllRemoteKeys(pet.uid, RemoteKeyType.PET) + } + } + + private suspend fun getRemoteKeyForFirstItem(state: PagingState): RemoteKeys? { + return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull() + ?.let { pet -> + dbRemoteKeyRepository.getAllRemoteKeys(pet.uid, RemoteKeyType.PET) + } + } + + private suspend fun getRemoteKeyClosestToCurrentPosition( + state: PagingState + ): RemoteKeys? { + return state.anchorPosition?.let { position -> + state.closestItemToPosition(position)?.uid?.let { petUid -> + dbRemoteKeyRepository.getAllRemoteKeys(petUid, RemoteKeyType.PET) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/api/pet/RestPetRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/api/pet/RestPetRepository.kt new file mode 100644 index 0000000..c3ba137 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/api/pet/RestPetRepository.kt @@ -0,0 +1,91 @@ +package ru.ulstu.`is`.pmu.api.pet + +import android.util.Log +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.pmu.api.MyServerService +import ru.ulstu.`is`.pmu.api.model.toPet +import ru.ulstu.`is`.pmu.api.model.toPetRemote +import ru.ulstu.`is`.pmu.api.report.ReportRemote +import ru.ulstu.`is`.pmu.api.user.RestUserRepository +import ru.ulstu.`is`.pmu.common.AppContainer +import ru.ulstu.`is`.pmu.common.PetRepository +import ru.ulstu.`is`.pmu.database.AppDatabase +import ru.ulstu.`is`.pmu.database.remotekeys.repository.OfflineRemoteKeyRepository +import ru.ulstu.`is`.pmu.database.pet.repository.OfflinePetRepository +import ru.ulstu.`is`.pmu.database.entyties.model.Pet + +class RestPetRepository( + private val service: MyServerService, + private val dbPetRepository: OfflinePetRepository, + private val dbRemoteKeyRepository: OfflineRemoteKeyRepository, + private val userRestRepository: RestUserRepository, + private val database: AppDatabase +) : PetRepository { + override fun getAllPets(): Flow> { + Log.d(RestPetRepository::class.simpleName, "Get pets") + + val pagingSourceFactory = { dbPetRepository.getAllPetsPagingSource() } + + @OptIn(ExperimentalPagingApi::class) + return Pager( + config = PagingConfig( + pageSize = AppContainer.LIMIT, + enablePlaceholders = false + ), + remoteMediator = PetRemoteMediator( + service, + dbPetRepository, + dbRemoteKeyRepository, + userRestRepository, + database, + ), + pagingSourceFactory = pagingSourceFactory + ).flow + } + + override fun getByUser(uid: Int): Flow> { + Log.d(RestPetRepository::class.simpleName, "Get 4444444444444444444444444444444444444444444444444444444") + + val pagingSourceFactory = { dbPetRepository.getByUserPetsPagingSource(uid) } + + @OptIn(ExperimentalPagingApi::class) + return Pager( + config = PagingConfig( + pageSize = AppContainer.LIMIT, + enablePlaceholders = false + ), + remoteMediator = PetRemoteMediator( + service, + dbPetRepository, + dbRemoteKeyRepository, + userRestRepository, + database, + ), + pagingSourceFactory = pagingSourceFactory + ).flow + } + + override suspend fun getPet(uid: Int): Pet = + service.getPet(uid).toPet() + + override suspend fun insertPet(pet: Pet) { + service.createPet(pet.toPetRemote()).toPet() + } + + override suspend fun updatePet(pet: Pet) { + service.updatePet(pet.uid, pet.toPetRemote()).toPet() + } + + override suspend fun deletePet(pet: Pet) { + service.deletePet(pet.uid).toPet() + } + suspend fun getReport(fromDate: String, toDate: String):List + { + return service.getReportInfo(fromDate,toDate) + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/api/report/ReportRemote.kt b/app/src/main/java/ru/ulstu/is/pmu/api/report/ReportRemote.kt new file mode 100644 index 0000000..88fa7fe --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/api/report/ReportRemote.kt @@ -0,0 +1,12 @@ +package ru.ulstu.`is`.pmu.api.report +import kotlinx.serialization.Serializable + +@Serializable +data class ReportRemote( + val reportId: Int = 0, + val petid: Int = 0, + val petname: String = "", + val birthday: String = "", + val userid: Int = 0, + val login: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/api/user/RestUserRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/api/user/RestUserRepository.kt new file mode 100644 index 0000000..fd0754d --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/api/user/RestUserRepository.kt @@ -0,0 +1,69 @@ +package ru.ulstu.`is`.pmu.api.user + +import android.util.Log +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.pmu.api.MyServerService +import ru.ulstu.`is`.pmu.api.model.toUser +import ru.ulstu.`is`.pmu.api.pet.RestPetRepository +import ru.ulstu.`is`.pmu.common.AppContainer +import ru.ulstu.`is`.pmu.common.UserRepository +import ru.ulstu.`is`.pmu.database.AppDatabase +import ru.ulstu.`is`.pmu.database.remotekeys.repository.OfflineRemoteKeyRepository +import ru.ulstu.`is`.pmu.database.entyties.model.User +import ru.ulstu.`is`.pmu.database.entyties.repository.OfflineUserRepository + +class RestUserRepository( + private val service: MyServerService, + private val dbUserRepository: OfflineUserRepository, + private val dbRemoteKeyRepository: OfflineRemoteKeyRepository, + private val database: AppDatabase +): UserRepository { + override suspend fun getAllInList(): List { + Log.d(RestUserRepository::class.simpleName, "Get users") + + val existUsers = dbUserRepository.getAllInList().associateBy { it.uid }.toMutableMap() + + service.getUsers() + .map { it.toUser() } + .forEach { user -> + val existUser = existUsers[user.uid] + if (existUser == null) { + dbUserRepository.createUser(user) + } else if (existUser != user) { + dbUserRepository.updateUser(user) + } + existUsers[user.uid] = user + } + Log.d(RestUserRepository::class.simpleName, "Geted users") + + return existUsers.map { it.value }.sortedBy { it.uid } + } + + override fun getAllUsers(): Flow> { + Log.d(RestPetRepository::class.simpleName, "Get pets") + + val pagingSourceFactory = { dbUserRepository.getAllUsersPagingSource() } + + @OptIn(ExperimentalPagingApi::class) + return Pager( + config = PagingConfig( + pageSize = AppContainer.LIMIT, + enablePlaceholders = false + ), + remoteMediator = UserRemoteMediator( + service, + dbUserRepository, + dbRemoteKeyRepository, + database + ), + pagingSourceFactory = pagingSourceFactory + ).flow + } + + override suspend fun getUser(uid: Int): User = + service.getUser(uid).toUser() +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/api/user/UserRemoteMediator.kt b/app/src/main/java/ru/ulstu/is/pmu/api/user/UserRemoteMediator.kt new file mode 100644 index 0000000..8ba2cb0 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/api/user/UserRemoteMediator.kt @@ -0,0 +1,108 @@ +package ru.ulstu.`is`.pmu.api.user + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import androidx.room.withTransaction +import retrofit2.HttpException +import ru.ulstu.`is`.pmu.api.MyServerService +import ru.ulstu.`is`.pmu.api.model.toUser +import ru.ulstu.`is`.pmu.database.AppDatabase +import ru.ulstu.`is`.pmu.database.remotekeys.model.RemoteKeyType +import ru.ulstu.`is`.pmu.database.remotekeys.model.RemoteKeys +import ru.ulstu.`is`.pmu.database.remotekeys.repository.OfflineRemoteKeyRepository +import ru.ulstu.`is`.pmu.database.entyties.model.User +import ru.ulstu.`is`.pmu.database.entyties.repository.OfflineUserRepository + +import java.io.IOException + +@OptIn(ExperimentalPagingApi::class) +class UserRemoteMediator( + private val service: MyServerService, + private val dbUserRepository: OfflineUserRepository, + private val dbRemoteKeyRepository: OfflineRemoteKeyRepository, + private val database: AppDatabase +) : RemoteMediator() { + + override suspend fun initialize(): InitializeAction { + return InitializeAction.LAUNCH_INITIAL_REFRESH + } + + override suspend fun load( + loadType: LoadType, + state: PagingState + ): 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 users = service.getUsers(page, state.config.pageSize).map { it.toUser() } + val endOfPaginationReached = users.isEmpty() + database.withTransaction { + if (loadType == LoadType.REFRESH) { + dbRemoteKeyRepository.deleteRemoteKey(RemoteKeyType.USER) + dbUserRepository.clearUsers() + } + val prevKey = if (page == 1) null else page - 1 + val nextKey = if (endOfPaginationReached) null else page + 1 + val keys = users.map { + RemoteKeys( + entityId = it.uid, + type = RemoteKeyType.USER, + prevKey = prevKey, + nextKey = nextKey + ) + } + dbRemoteKeyRepository.createRemoteKeys(keys) + dbUserRepository.insertUsers(users) + } + return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) + } catch (exception: IOException) { + return MediatorResult.Error(exception) + } catch (exception: HttpException) { + return MediatorResult.Error(exception) + } + } + + private suspend fun getRemoteKeyForLastItem(state: PagingState): RemoteKeys? { + return state.pages.lastOrNull { it.data.isNotEmpty() }?.data?.lastOrNull() + ?.let { user -> + dbRemoteKeyRepository.getAllRemoteKeys(user.uid, RemoteKeyType.USER) + } + } + + private suspend fun getRemoteKeyForFirstItem(state: PagingState): RemoteKeys? { + return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull() + ?.let { user -> + dbRemoteKeyRepository.getAllRemoteKeys(user.uid, RemoteKeyType.USER) + } + } + + private suspend fun getRemoteKeyClosestToCurrentPosition( + state: PagingState + ): RemoteKeys? { + return state.anchorPosition?.let { position -> + state.closestItemToPosition(position)?.uid?.let { userUid -> + dbRemoteKeyRepository.getAllRemoteKeys(userUid, RemoteKeyType.USER) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/common/AppContainer.kt b/app/src/main/java/ru/ulstu/is/pmu/common/AppContainer.kt new file mode 100644 index 0000000..7cb86cf --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/common/AppContainer.kt @@ -0,0 +1,50 @@ +package ru.ulstu.`is`.pmu.common + +import android.content.Context +import ru.ulstu.`is`.pmu.api.MyServerService +import ru.ulstu.`is`.pmu.api.user.RestUserRepository +import ru.ulstu.`is`.pmu.api.pet.RestPetRepository +import ru.ulstu.`is`.pmu.database.AppDatabase +import ru.ulstu.`is`.pmu.database.pet.repository.OfflinePetRepository +import ru.ulstu.`is`.pmu.database.remotekeys.repository.OfflineRemoteKeyRepository +import ru.ulstu.`is`.pmu.database.entyties.repository.OfflineUserRepository + + +interface AppContainer { + val petRestRepository: RestPetRepository + val userRestRepository: RestUserRepository + + companion object { + const val TIMEOUT = 5000L + const val LIMIT = 10 + } +} + +class AppDataContainer(private val context: Context) : AppContainer { + private val petRepository: OfflinePetRepository by lazy { + OfflinePetRepository(AppDatabase.getInstance(context).petDao()) + } + private val userRepository: OfflineUserRepository by lazy { + OfflineUserRepository(AppDatabase.getInstance(context).userDao()) + } + private val remoteKeyRepository: OfflineRemoteKeyRepository by lazy { + OfflineRemoteKeyRepository(AppDatabase.getInstance(context).remoteKeysDao()) + } + override val petRestRepository: RestPetRepository by lazy { + RestPetRepository( + MyServerService.getInstance(), + petRepository, + remoteKeyRepository, + userRestRepository, + AppDatabase.getInstance(context) + ) + } + override val userRestRepository: RestUserRepository by lazy { + RestUserRepository( + MyServerService.getInstance(), + userRepository, + remoteKeyRepository, + AppDatabase.getInstance(context) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/common/AppViewModelProvider.kt b/app/src/main/java/ru/ulstu/is/pmu/common/AppViewModelProvider.kt new file mode 100644 index 0000000..1ab1298 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/common/AppViewModelProvider.kt @@ -0,0 +1,43 @@ +package ru.ulstu.`is`.pmu.common + +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewmodel.CreationExtras +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import ru.ulstu.`is`.pmu.StudentApplication +import ru.ulstu.`is`.pmu.ui.pet.edit.PetEditViewModel +import ru.ulstu.`is`.pmu.ui.pet.edit.UserDropDownViewModel +import ru.ulstu.`is`.pmu.ui.pet.list.PetListViewModel +import ru.ulstu.`is`.pmu.ui.pet.list.UserListViewModel +import ru.ulstu.`is`.pmu.ui.pet.report.ReportViewModel + +object AppViewModelProvider { + val Factory = viewModelFactory { + initializer { + PetListViewModel(petApplication().container.petRestRepository) + } + initializer { + UserListViewModel(petApplication().container.userRestRepository) + } + initializer { + PetEditViewModel( + this.createSavedStateHandle(), + petApplication().container.petRestRepository + ) + } + initializer { + UserDropDownViewModel(petApplication().container.userRestRepository + ) + } + initializer { + ReportViewModel( + petApplication().container.petRestRepository, + ) + } + } +} + + +fun CreationExtras.petApplication(): StudentApplication = + (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as StudentApplication) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/common/MyViewModel.kt b/app/src/main/java/ru/ulstu/is/pmu/common/MyViewModel.kt new file mode 100644 index 0000000..6ee43f0 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/common/MyViewModel.kt @@ -0,0 +1,48 @@ +package ru.ulstu.`is`.pmu.common + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import retrofit2.HttpException +import ru.ulstu.`is`.pmu.api.ApiStatus +import java.io.IOException + +open class MyViewModel : ViewModel() { + var apiStatus by mutableStateOf(ApiStatus.DONE) + private set + + var apiError by mutableStateOf("") + private set + + fun runInScope( + actionSuccess: suspend () -> Unit, + actionError: suspend () -> Unit + ) { + viewModelScope.launch { + apiStatus = ApiStatus.LOADING + runCatching { + actionSuccess() + apiStatus = ApiStatus.DONE + apiError = "" + }.onFailure { e: Throwable -> + when (e) { + is IOException, + is HttpException -> { + actionError() + apiStatus = ApiStatus.ERROR + apiError = e.localizedMessage ?: e.toString() + } + + else -> throw e + } + } + } + } + + fun runInScope(actionSuccess: suspend () -> Unit) { + runInScope(actionSuccess, actionError = {}) + } +} diff --git a/app/src/main/java/ru/ulstu/is/pmu/common/PetRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/common/PetRepository.kt new file mode 100644 index 0000000..167ed7e --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/common/PetRepository.kt @@ -0,0 +1,14 @@ +package ru.ulstu.`is`.pmu.common + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.pmu.database.entyties.model.Pet + +interface PetRepository { + fun getAllPets(): Flow> + fun getByUser(uid: Int): Flow> + suspend fun getPet(uid: Int): Pet + suspend fun insertPet(student: Pet) + suspend fun updatePet(student: Pet) + suspend fun deletePet(student: Pet) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/common/UserRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/common/UserRepository.kt new file mode 100644 index 0000000..581a681 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/common/UserRepository.kt @@ -0,0 +1,11 @@ +package ru.ulstu.`is`.pmu.common + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.pmu.database.entyties.model.User + +interface UserRepository { + suspend fun getAllInList(): List + fun getAllUsers(): Flow> + suspend fun getUser(uid: Int): User +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/AppDatabase.kt b/app/src/main/java/ru/ulstu/is/pmu/database/AppDatabase.kt new file mode 100644 index 0000000..9f92bd0 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/AppDatabase.kt @@ -0,0 +1,59 @@ +package ru.ulstu.`is`.pmu.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import ru.ulstu.`is`.pmu.database.remotekeys.dao.RemoteKeysDao +import ru.ulstu.`is`.pmu.database.remotekeys.model.RemoteKeys +import ru.ulstu.`is`.pmu.database.entyties.dao.PetDao +import ru.ulstu.`is`.pmu.database.entyties.dao.UserDao +import ru.ulstu.`is`.pmu.database.entyties.model.Pet +import ru.ulstu.`is`.pmu.database.entyties.model.User + +@Database( + entities = [ + Pet::class, + User::class, + RemoteKeys::class, + ], + version = 1, + exportSchema = false +) +abstract class AppDatabase : RoomDatabase() { + abstract fun userDao(): UserDao + abstract fun petDao(): PetDao + 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 -> + + val userDao = database.userDao() + val user1 = User(1, "ivan","Иван","Иванов") + val user2 = User(2, "max","Макс","Максов") + val user3 = User(3, "ann","Анна","Аннова") + userDao.insert(user1) + userDao.insert(user2) + userDao.insert(user3) + } + } + + fun getInstance(appContext: Context): AppDatabase { + return INSTANCE ?: synchronized(this) { + Room.databaseBuilder( + appContext, + AppDatabase::class.java, + DB_NAME + ) + .build() + .also { INSTANCE = it } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/entyties/dao/PetDao.kt b/app/src/main/java/ru/ulstu/is/pmu/database/entyties/dao/PetDao.kt new file mode 100644 index 0000000..e4c5ddb --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/entyties/dao/PetDao.kt @@ -0,0 +1,35 @@ +package ru.ulstu.`is`.pmu.database.entyties.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 kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.pmu.database.entyties.model.Pet + +@Dao +interface PetDao { + @Query("select * from pets") + fun getAll(): PagingSource + + @Query("select * from pets where pets.user_id = :uid") + fun getByUser(uid: Int): PagingSource + + + @Query("select * from pets where pets.uid = :uid") + fun getByUid(uid: Int): Flow + + @Insert + suspend fun insert(vararg pet: Pet) + + @Update + suspend fun update(pet: Pet) + + @Delete + suspend fun delete(pet: Pet) + @Query("DELETE FROM pets") + + suspend fun deleteAll() +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/entyties/dao/UserDao.kt b/app/src/main/java/ru/ulstu/is/pmu/database/entyties/dao/UserDao.kt new file mode 100644 index 0000000..6611f29 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/entyties/dao/UserDao.kt @@ -0,0 +1,33 @@ +package ru.ulstu.`is`.pmu.database.entyties.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 kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.pmu.database.entyties.model.User + +@Dao +interface UserDao { + @Query("select * from users") + fun getAllInList(): List + @Query("select * from users") + fun getAll(): PagingSource + + @Query("select * from users where users.uid = :uid") + fun getByUid(uid: Int): Flow + + @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() +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/entyties/model/Pet.kt b/app/src/main/java/ru/ulstu/is/pmu/database/entyties/model/Pet.kt new file mode 100644 index 0000000..7f6e3a0 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/entyties/model/Pet.kt @@ -0,0 +1,79 @@ +package ru.ulstu.`is`.pmu.database.entyties.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Ignore +import androidx.room.PrimaryKey +import ru.ulstu.`is`.pmu.R + +@Entity( + tableName = "pets", foreignKeys = [ + ForeignKey( + entity = User::class, + parentColumns = ["uid"], + childColumns = ["user_id"], + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE + ) + ] +) +data class Pet( + @PrimaryKey(autoGenerate = true) + val uid: Int = 0, + @ColumnInfo(name = "name") + val name: String, + @ColumnInfo(name = "note") + val note: String, + @ColumnInfo(name = "birthday") + val birthday: String, + @ColumnInfo(name = "user_id", index = true) + val userId: Int, + @ColumnInfo(name = "image_resource_id") // Добавляем поле для хранения ресурса изображения + val imageResourceId: Int +) { + + @Ignore + constructor( + name: String, + note: String, + birthday: String, + user: User, + imageResourceId: Int // Добавляем параметр для изображения + ) : this(0, name, note,birthday, user.uid, imageResourceId) + + + companion object { + fun getPet(index: Int = 0): Pet { + return Pet( + index, + "name", + "note", + "01.01.2024", + 0, + R.drawable.pet1 // Задаем значение по умолчанию для изображения + ) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Pet + if (uid != other.uid) return false + if (name != other.name) return false + if (note != other.note) return false + if (userId != other.userId) return false + if (imageResourceId != other.imageResourceId) return false // Сравниваем изображения + return true + } + + override fun hashCode(): Int { + var result = uid + result = 31 * result + name.hashCode() + result = 31 * result + note.hashCode() + result = 31 * result + userId + result = 31 * result + imageResourceId // Включаем изображение в вычисление хэш-кода + return result + } +} diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/entyties/model/User.kt b/app/src/main/java/ru/ulstu/is/pmu/database/entyties/model/User.kt new file mode 100644 index 0000000..31d3a92 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/entyties/model/User.kt @@ -0,0 +1,56 @@ +package ru.ulstu.`is`.pmu.database.entyties.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.PrimaryKey + +@Entity(tableName = "users") +data class User( + @PrimaryKey(autoGenerate = true) + val uid: Int = 0, + @ColumnInfo(name = "login") + val login: String, + @ColumnInfo(name = "first_name") + val nameFirst: String, + @ColumnInfo(name = "last_name") + val nameLast: String +) { + @Ignore + constructor( + login: String, + nameFirst: String, + nameLast: String, + ) : this(0, login, nameFirst, nameLast) + + + companion object { + fun getUser(index: Int = 0): User { + return User( + index, + "lodin", + "first", + "last" + ) + } + val DEMO_USER = User( + 0, + "login", + "first", + "last" + ) + } + 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 + if (login != other.login) return false + return true + } + + override fun hashCode(): Int { + return uid + } + +} diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/entyties/repository/OfflinePetRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/database/entyties/repository/OfflinePetRepository.kt new file mode 100644 index 0000000..2421f37 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/entyties/repository/OfflinePetRepository.kt @@ -0,0 +1,47 @@ +package ru.ulstu.`is`.pmu.database.pet.repository + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import ru.ulstu.`is`.pmu.common.AppContainer +import ru.ulstu.`is`.pmu.common.PetRepository +import ru.ulstu.`is`.pmu.database.entyties.dao.PetDao +import ru.ulstu.`is`.pmu.database.entyties.model.Pet + +class OfflinePetRepository(private val petDao: PetDao) : PetRepository { + override fun getAllPets(): Flow> = Pager( + config = PagingConfig( + pageSize = AppContainer.LIMIT, + enablePlaceholders = false + ), + pagingSourceFactory = petDao::getAll + ).flow + + override fun getByUser(uid: Int): Flow> = Pager( + config = PagingConfig( + pageSize = AppContainer.LIMIT, + enablePlaceholders = false + ), + pagingSourceFactory = petDao::getAll + ).flow + + override suspend fun getPet(uid: Int): Pet = petDao.getByUid(uid).first() + + override suspend fun insertPet(pet: Pet) = petDao.insert(pet) + + override suspend fun updatePet(pet: Pet) = petDao.update(pet) + + override suspend fun deletePet(pet: Pet) = petDao.delete(pet) + + fun getAllPetsPagingSource(): PagingSource = petDao.getAll() + fun getByUserPetsPagingSource(uid:Int): PagingSource = petDao.getByUser(uid) + + + suspend fun insertPets(pets: List) = + petDao.insert(*pets.toTypedArray()) + + suspend fun clearPets() = petDao.deleteAll() +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/entyties/repository/OfflineUserRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/database/entyties/repository/OfflineUserRepository.kt new file mode 100644 index 0000000..af27e2c --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/entyties/repository/OfflineUserRepository.kt @@ -0,0 +1,35 @@ +package ru.ulstu.`is`.pmu.database.entyties.repository + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import ru.ulstu.`is`.pmu.common.AppContainer +import ru.ulstu.`is`.pmu.common.UserRepository +import ru.ulstu.`is`.pmu.database.entyties.dao.UserDao +import ru.ulstu.`is`.pmu.database.entyties.model.User + +class OfflineUserRepository(private val userDao: UserDao) : UserRepository { + + override suspend fun getAllInList(): List = userDao.getAllInList() + override fun getAllUsers(): Flow> = Pager( + config = PagingConfig( + pageSize = AppContainer.LIMIT, + enablePlaceholders = false + ), + pagingSourceFactory = userDao::getAll + ).flow + suspend fun createUser(user: User) = userDao.insert(user) + suspend fun updateUser(user: User) = userDao.update(user) + fun getAllUsersPagingSource(): PagingSource = userDao.getAll() + + suspend fun insertUsers(users: List) = + userDao.insert(*users.toTypedArray()) + + suspend fun clearUsers() = userDao.deleteAll() + override suspend fun getUser(uid: Int): User = userDao.getByUid(uid).first() + + +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/remotekeys/dao/RemoteKeysDao.kt b/app/src/main/java/ru/ulstu/is/pmu/database/remotekeys/dao/RemoteKeysDao.kt new file mode 100644 index 0000000..11704b4 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/remotekeys/dao/RemoteKeysDao.kt @@ -0,0 +1,20 @@ +package ru.ulstu.`is`.pmu.database.remotekeys.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import ru.ulstu.`is`.pmu.database.remotekeys.model.RemoteKeyType +import ru.ulstu.`is`.pmu.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) + + @Query("DELETE FROM remote_keys WHERE type = :type") + suspend fun clearRemoteKeys(type: RemoteKeyType) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/remotekeys/model/RemoteKeys.kt b/app/src/main/java/ru/ulstu/is/pmu/database/remotekeys/model/RemoteKeys.kt new file mode 100644 index 0000000..71e8b67 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/remotekeys/model/RemoteKeys.kt @@ -0,0 +1,28 @@ +package ru.ulstu.`is`.pmu.database.remotekeys.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverter +import androidx.room.TypeConverters +import ru.ulstu.`is`.pmu.database.entyties.model.Pet +import ru.ulstu.`is`.pmu.database.entyties.model.User + +enum class RemoteKeyType(private val type: String) { + PET(Pet::class.simpleName ?: "Pet"), + USER(User::class.simpleName ?: "User"); + + @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? +) diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/remotekeys/repository/OfflineRemoteKeyRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/database/remotekeys/repository/OfflineRemoteKeyRepository.kt new file mode 100644 index 0000000..018e862 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/remotekeys/repository/OfflineRemoteKeyRepository.kt @@ -0,0 +1,16 @@ +package ru.ulstu.`is`.pmu.database.remotekeys.repository + +import ru.ulstu.`is`.pmu.database.remotekeys.dao.RemoteKeysDao +import ru.ulstu.`is`.pmu.database.remotekeys.model.RemoteKeyType +import ru.ulstu.`is`.pmu.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) = + remoteKeysDao.insertAll(remoteKeys) + + override suspend fun deleteRemoteKey(type: RemoteKeyType) = + remoteKeysDao.clearRemoteKeys(type) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/remotekeys/repository/RemoteKeyRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/database/remotekeys/repository/RemoteKeyRepository.kt new file mode 100644 index 0000000..cef47f9 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/remotekeys/repository/RemoteKeyRepository.kt @@ -0,0 +1,10 @@ +package ru.ulstu.`is`.pmu.database.remotekeys.repository + +import ru.ulstu.`is`.pmu.database.remotekeys.model.RemoteKeyType +import ru.ulstu.`is`.pmu.database.remotekeys.model.RemoteKeys + +interface RemoteKeyRepository { + suspend fun getAllRemoteKeys(id: Int, type: RemoteKeyType): RemoteKeys? + suspend fun createRemoteKeys(remoteKeys: List) + suspend fun deleteRemoteKey(type: RemoteKeyType) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/NetworkUi.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/NetworkUi.kt new file mode 100644 index 0000000..874f844 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/NetworkUi.kt @@ -0,0 +1,69 @@ +package ru.ulstu.`is`.pmu.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import ru.ulstu.`is`.pmu.R + + +@Composable +fun ErrorPlaceholder(message: String, onBack: () -> Unit) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(10.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + fontSize = TextUnit(value = 20F, type = TextUnitType.Sp), + text = message, + color = Color(0xFFFF1744) + ) + Spacer(modifier = Modifier.padding(bottom = 10.dp)) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { onBack() } + ) { + Text(stringResource(id = R.string.back)) + } + } +} + +@Composable +fun LoadingPlaceholder() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(10.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + fontSize = TextUnit(value = 25F, type = TextUnitType.Sp), + text = stringResource(id = R.string.loading) + ) + } +} diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/MainNavbar.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/MainNavbar.kt new file mode 100644 index 0000000..409c0de --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/MainNavbar.kt @@ -0,0 +1,146 @@ +package ru.ulstu.`is`.pmu.ui.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +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.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +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 ru.ulstu.`is`.pmu.R +import ru.ulstu.`is`.pmu.ui.pet.edit.PetEdit +import ru.ulstu.`is`.pmu.ui.pet.list.PetList +import ru.ulstu.`is`.pmu.ui.pet.list.PetUserList +import ru.ulstu.`is`.pmu.ui.pet.list.UserList +import ru.ulstu.`is`.pmu.ui.pet.report.ReportPage + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Topbar( + navController: NavHostController, + currentScreen: Screen? +) { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + titleContentColor = MaterialTheme.colorScheme.onPrimary, + ), + title = { + Text(stringResource(currentScreen?.resourceId ?: R.string.app_name)) + }, + navigationIcon = { + if ( + navController.previousBackStackEntry != null + && (currentScreen == null || !currentScreen.showInBottomBar) + ) { + IconButton(onClick = { navController.navigateUp() }) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary + ) + } + } + } + ) +} + +@Composable +fun Navbar( + navController: NavHostController, + currentDestination: NavDestination?, + modifier: Modifier = Modifier +) { + NavigationBar(modifier) { + Screen.bottomBarItems.forEach { screen -> + NavigationBarItem( + icon = { Icon(screen.icon, contentDescription = null) }, + label = { Text(stringResource(screen.resourceId)) }, + selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true, + onClick = { + navController.navigate(screen.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + ) + } + } +} + +@Composable +fun Navhost( + navController: NavHostController, + innerPadding: PaddingValues, modifier: + Modifier = Modifier +) { + NavHost( + navController, + startDestination = Screen.PetList.route, + modifier.padding(innerPadding) + ) { + composable(Screen.Report.route) { ReportPage(navController = navController) } + composable(Screen.PetList.route) { PetList(navController) } + composable(Screen.UserList.route) { UserList(navController) } + composable( + Screen.PetEdit.route, + arguments = listOf(navArgument("id") { type = NavType.IntType }) + ) { + PetEdit(navController) + } + composable( + Screen.PetUserList.route, + arguments = listOf(navArgument("id") { type = NavType.IntType }) + ) { backStackEntry -> + val id = backStackEntry.arguments?.getInt("id") ?: throw IllegalStateException("ID is not found") + PetUserList(navController, id) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainNavbar() { + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/Screen.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/Screen.kt new file mode 100644 index 0000000..6a86f1e --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/Screen.kt @@ -0,0 +1,46 @@ +package ru.ulstu.`is`.pmu.ui.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.Info +import androidx.compose.material.icons.filled.List +import androidx.compose.ui.graphics.vector.ImageVector +import ru.ulstu.`is`.pmu.R + +enum class Screen( + val route: String, + @StringRes val resourceId: Int, + val icon: ImageVector = Icons.Filled.Favorite, + val showInBottomBar: Boolean = true +) { + PetList( + "pet-list", R.string.list_pet, Icons.Filled.Home + ), + Report( + "report", R.string.report, Icons.Filled.Info + ), + PetUserList( + "pet-user-list/{id}", R.string.list_pet, showInBottomBar = false + ), + UserList( + "user-list", R.string.list_user, Icons.Filled.List + ), + PetEdit( + "pet-edit/{id}", R.string.pet_view, showInBottomBar = false + ); + + companion object { + val bottomBarItems = listOf( + UserList, + PetList, + Report + ) + + fun getItem(route: String): Screen? { + val findRoute = route.split("/").first() + return values().find { value -> value.route.startsWith(findRoute) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/pet/edit/PetEdit.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/edit/PetEdit.kt new file mode 100644 index 0000000..a851dcf --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/edit/PetEdit.kt @@ -0,0 +1,360 @@ +package ru.ulstu.`is`.pmu.ui.pet.edit + +import android.content.res.Configuration +import android.widget.CalendarView +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults.TrailingIcon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import kotlinx.coroutines.launch +import ru.ulstu.`is`.pmu.R +import ru.ulstu.`is`.pmu.api.ApiStatus +import ru.ulstu.`is`.pmu.common.AppViewModelProvider +import ru.ulstu.`is`.pmu.database.entyties.model.Pet +import ru.ulstu.`is`.pmu.database.entyties.model.User +import ru.ulstu.`is`.pmu.ui.ErrorPlaceholder +import ru.ulstu.`is`.pmu.ui.LoadingPlaceholder +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + + +@Composable +fun PetEdit( + navController: NavController, + viewModel: PetEditViewModel = viewModel(factory = AppViewModelProvider.Factory), + userViewModel: UserDropDownViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + if (userViewModel.apiStatus == ApiStatus.ERROR) { + ErrorPlaceholder( + message = userViewModel.apiError, + onBack = { navController.popBackStack() } + ) + return + } + val coroutineScope = rememberCoroutineScope() + userViewModel.setCurrentUser(viewModel.petUiState.petDetails.userId) + when (viewModel.apiStatus) { + ApiStatus.DONE -> { + PetEdit( + petUiState = viewModel.petUiState, + userUiState = userViewModel.userUiState, + usersListUiState = userViewModel.usersListUiState, + onClick = { + coroutineScope.launch { + viewModel.savePet() + navController.popBackStack() + } + }, + onUpdate = viewModel::updateUiState, + onUserUpdate = userViewModel::updateUiState + ) + } + + ApiStatus.LOADING -> LoadingPlaceholder() + else -> ErrorPlaceholder( + message = viewModel.apiError, + onBack = { navController.popBackStack() } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun UserDropDown( + userUiState: UserUiState, + usersListUiState: UsersListUiState, + onUserUpdate: (User) -> Unit +) { + var expanded: Boolean by remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + modifier = Modifier + .padding(top = 7.dp), + expanded = expanded, + onExpandedChange = { + expanded = !expanded + } + ) { + TextField( + value = userUiState.user?.nameFirst + ?: stringResource(id = R.string.user_is_not_selested), + onValueChange = {}, + readOnly = true, + trailingIcon = { + TrailingIcon(expanded = expanded) + }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor() + ) + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier + .background(Color.White) + .exposedDropdownSize() + ) { + usersListUiState.userList.forEach { user -> + DropdownMenuItem( + text = { + Text(text = user.nameFirst) + }, + onClick = { + onUserUpdate(user) + expanded = false + } + ) + } + } + } +} + +@Composable +private fun PetEdit( + petUiState: PetUiState, + userUiState: UserUiState, + usersListUiState: UsersListUiState, + onClick: () -> Unit, + onUpdate: (PetDetails) -> Unit, + onUserUpdate: (User) -> Unit +) { + var selectedImage by remember { mutableStateOf(petUiState.petDetails.imageResourceId) } // Выбранное изображение + if (selectedImage ==0) selectedImage=R.drawable.pet1 + if (selectedImage==R.drawable.pet1) onUpdate(petUiState.petDetails.copy(imageResourceId = R.drawable.pet1)) + if (petUiState.petDetails.birthday=="") onUpdate(petUiState.petDetails.copy(birthday = SimpleDateFormat("dd.MM.yyyy").format(Date()).toString())) + val imageIds = intArrayOf( + R.drawable.pet1, + R.drawable.pet2, + R.drawable.pet3, + R.drawable.pet4, + R.drawable.pet5, + R.drawable.pet6, + R.drawable.pet7, + R.drawable.pet8 + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp) + ) { + LazyRow( + modifier = Modifier.fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 8.dp) + ) { + items(imageIds.size) { index -> + val imageId = imageIds[index] + val isSelected = selectedImage == imageId // Проверка, выбрано ли изображение + + Image( + painter = painterResource(id = imageId), + contentDescription = null, + modifier = Modifier + .size(100.dp) + .padding(4.dp) + .clickable { + selectedImage = imageId + onUpdate(petUiState.petDetails.copy(imageResourceId = selectedImage)) + } + .background(if (isSelected) Color.Gray.copy(alpha = 0.5f) else Color.Transparent) + ) + } + } + + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = petUiState.petDetails.name, + onValueChange = { onUpdate(petUiState.petDetails.copy(name = it)) }, + label = { Text(stringResource(id = R.string.name) )}, + singleLine = true + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = petUiState.petDetails.note, + onValueChange = { onUpdate(petUiState.petDetails.copy(note = it)) }, + label = { Text(stringResource(id = R.string.note)) }, + singleLine = false + ) + UserDropDown( + userUiState = userUiState, + usersListUiState = usersListUiState, + onUserUpdate = { + onUpdate(petUiState.petDetails.copy(userId = it.uid)) + onUserUpdate(it) + } + ) + var showDatePicker by remember { mutableStateOf(false) } + if (showDatePicker) { + DatePicker( + onDateSelected = { selectedDate -> + onUpdate(petUiState.petDetails.copy(birthday = SimpleDateFormat("dd.MM.yyyy").format(selectedDate))) + showDatePicker = false + }, + onDismissRequest = { + showDatePicker = false + } + ) + } + + Button( + onClick = { showDatePicker = true }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Выбрать дату рождения") + } + Text( modifier = Modifier + .align(alignment = Alignment.CenterHorizontally), + text = "Дата рождения:" + petUiState.petDetails.birthday) + + + Button( + onClick = onClick, + enabled = petUiState.isEntryValid, + shape = MaterialTheme.shapes.small, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(R.string.save)) + } + } +} + + +@Composable +fun CustomCalendarView(onDateSelected: (Date) -> Unit) { + AndroidView( + modifier = Modifier.wrapContentSize(), + factory = { context -> + // Используем стандартный контекст, без применения кастомной темы + CalendarView(context).apply { + // Настройки CalendarView + val calendar = Calendar.getInstance() + + setOnDateChangeListener { _, year, month, dayOfMonth -> + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, month) + calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth) + onDateSelected(calendar.time) + } + } + } + ) +} + +@Composable +fun DatePicker(onDateSelected: (Date) -> Unit, onDismissRequest: () -> Unit) { + val selDate = remember { mutableStateOf(Date()) } + + //todo - add strings to resource after POC + Dialog(onDismissRequest = { onDismissRequest() }, properties = DialogProperties()) { + Column( + modifier = Modifier + .wrapContentSize() + .background( + color = Color.White + ) + ) { + Column( + Modifier + .defaultMinSize(minHeight = 72.dp) + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.primary + ) + .padding(16.dp) + ) { + Text( + text = "Выберите дату" + ) + + Spacer(modifier = Modifier.size(24.dp)) + + Text( + text = SimpleDateFormat("MMM d, yyyy", Locale.getDefault()).format(selDate.value) + ) + + Spacer(modifier = Modifier.size(16.dp)) + } + + CustomCalendarView(onDateSelected = { + selDate.value = it + }) + + Spacer(modifier = Modifier.size(8.dp)) + + Row( + modifier = Modifier + .align(Alignment.End) + .padding(bottom = 16.dp, end = 16.dp) + .background( + color = Color.White, + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) + ) + ) { + TextButton( + onClick = onDismissRequest + ) { + //TODO - hardcode string + Text( + text = "Отмена" + ) + } + + TextButton( + onClick = { + onDateSelected(selDate.value) + onDismissRequest() + } + ) { + //TODO - hardcode string + Text( + text = "Подтвердить" + ) + } + + } + } + } +} + + diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/pet/edit/PetEditViewModel.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/edit/PetEditViewModel.kt new file mode 100644 index 0000000..6167a4b --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/edit/PetEditViewModel.kt @@ -0,0 +1,99 @@ +package ru.ulstu.`is`.pmu.ui.pet.edit + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import ru.ulstu.`is`.pmu.common.MyViewModel +import ru.ulstu.`is`.pmu.common.PetRepository +import ru.ulstu.`is`.pmu.database.entyties.model.Pet + +class PetEditViewModel( + savedStateHandle: SavedStateHandle, + private val petRepository: PetRepository +) : MyViewModel() { + + var petUiState by mutableStateOf(PetUiState()) + private set + + private val petUid: Int = checkNotNull(savedStateHandle["id"]) + + init { + if (petUid > 0) { + runInScope( + actionSuccess = { + petUiState = petRepository.getPet(petUid) + .toUiState(true) + }, + actionError = { + petUiState = PetUiState() + } + ) + } + } + + + fun updateUiState(petDetails: PetDetails) { + petUiState = PetUiState( + petDetails = petDetails, + isEntryValid = validateInput(petDetails) + ) + } + + suspend fun savePet() { + if (validateInput()) { + runInScope( + actionSuccess = { + if (petUid > 0) { + petRepository.updatePet(petUiState.petDetails.toPet(petUid)) + } else { + petRepository.insertPet(petUiState.petDetails.toPet()) + } + } + ) + } + } + + private fun validateInput(uiState: PetDetails = petUiState.petDetails): Boolean { + return with(uiState) { + name.isNotBlank() + && note.isNotBlank() + && userId > 0 + } + } +} + +data class PetUiState( + val petDetails: PetDetails = PetDetails(), + val isEntryValid: Boolean = false +) + +data class PetDetails( + val name: String = "", + val note: String = "", + val birthday: String = "", + val userId: Int = 0, + val imageResourceId: Int =0 +) + +fun PetDetails.toPet(uid: Int = 0): Pet = Pet( + uid = uid, + name = name, + note = note, + birthday =birthday, + userId = userId, + imageResourceId=imageResourceId +) + +fun Pet.toDetails(): PetDetails = PetDetails( + name = name, + note = note, + birthday =birthday, + userId = userId, + imageResourceId=imageResourceId +) + +fun Pet.toUiState(isEntryValid: Boolean = false): PetUiState = PetUiState( + petDetails = this.toDetails(), + isEntryValid = isEntryValid +) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/pet/edit/UserDropDownViewModel.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/edit/UserDropDownViewModel.kt new file mode 100644 index 0000000..72ca57b --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/edit/UserDropDownViewModel.kt @@ -0,0 +1,53 @@ +package ru.ulstu.`is`.pmu.ui.pet.edit + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import ru.ulstu.`is`.pmu.common.MyViewModel +import ru.ulstu.`is`.pmu.common.UserRepository +import ru.ulstu.`is`.pmu.database.entyties.model.User + +class UserDropDownViewModel( + private val userRepository: UserRepository +) : MyViewModel() { + var usersListUiState by mutableStateOf(UsersListUiState()) + private set + + var userUiState by mutableStateOf(UserUiState()) + private set + + init { + viewModelScope.launch { + val userList = withContext(Dispatchers.IO) { + userRepository.getAllInList() + } + usersListUiState = UsersListUiState(userList) + } + } + + + + fun setCurrentUser(groupId: Int) { + val user: User? = + usersListUiState.userList.firstOrNull { group -> group.uid == groupId } + user?.let { updateUiState(it) } + } + + fun updateUiState(user: User) { + userUiState = UserUiState( + user = user + ) + } +} + +data class UsersListUiState(val userList: List = listOf()) + +data class UserUiState( + val user: User? = null +) + +fun User.toUiState() = UserUiState(user = User(uid = uid, login=login,nameFirst=nameFirst, nameLast=nameLast)) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/pet/list/PetList.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/list/PetList.kt new file mode 100644 index 0000000..bb45aab --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/list/PetList.kt @@ -0,0 +1,265 @@ +package ru.ulstu.`is`.pmu.ui.pet.list + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeOut +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.fillMaxSize +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.lazy.LazyColumn +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DismissDirection +import androidx.compose.material3.DismissState +import androidx.compose.material3.DismissValue +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +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.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import ru.ulstu.`is`.pmu.common.AppViewModelProvider +import ru.ulstu.`is`.pmu.database.entyties.model.Pet +import ru.ulstu.`is`.pmu.ui.navigation.Screen + + +@Composable +fun PetList( + navController: NavController, + viewModel: PetListViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val petListUiState = viewModel.petListUiState.collectAsLazyPagingItems() + Scaffold( + topBar = {}, + floatingActionButton = { + FloatingActionButton( + onClick = { + val route = Screen.PetEdit.route.replace("{id}", 0.toString()) + navController.navigate(route) + }, + ) { + Icon(Icons.Filled.Add, "Добавить") + } + } + ) { innerPadding -> + PetList( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + petList = petListUiState, + onClick = { uid: Int -> + val route = Screen.PetEdit.route.replace("{id}", uid.toString()) + navController.navigate(route) + }, + onSwipe = { pet: Pet -> + viewModel.deletePet(pet) + } + + ) + } +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SwipeToDelete( + dismissState: DismissState, + pet: Pet, + onClick: (uid: 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 -> Color.White + }, 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 = { + PetListItem(pet = pet, + modifier = Modifier + .padding(vertical = 7.dp) + .clickable { onClick(pet.uid) }) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@Composable +private fun PetList( + modifier: Modifier = Modifier, + petList: LazyPagingItems, + onClick: (uid: Int) -> Unit, + onSwipe: (pet: Pet) -> Unit +) { + val refreshScope = rememberCoroutineScope() + var refreshing by remember { mutableStateOf(false) } + fun refresh() = refreshScope.launch { + refreshing = true + petList.refresh() + refreshing = false + } + + val state = rememberPullRefreshState(refreshing, ::refresh) + Box( + modifier = modifier.pullRefresh(state) + ) { + Column( + modifier = modifier + ) { + LazyColumn( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + items( + count = petList.itemCount, + key = petList.itemKey(), + contentType = petList.itemContentType() + ) { index -> + val pet = petList[index] + pet?.let { + var show by remember { mutableStateOf(true) } + val dismissState = rememberDismissState( + confirmValueChange = { + if (it == DismissValue.DismissedToStart || + it == DismissValue.DismissedToEnd + ) { + show = false + true + } else false + }, positionalThreshold = { 200.dp.toPx() } + ) + + AnimatedVisibility( + show, exit = fadeOut(spring()) + ) { + SwipeToDelete( + dismissState = dismissState, + pet = pet, + onClick = onClick + ) + } + + LaunchedEffect(show) { + if (!show) { + delay(800) + onSwipe(pet) + } + } + } + } + } + PullRefreshIndicator( + refreshing, state, + Modifier + .align(CenterHorizontally) + .zIndex(100f) + ) + } + } +} + +@Composable +private fun PetListItem( + pet: Pet, + modifier: Modifier = Modifier +) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + ) { + Card( + modifier = modifier + .padding(8.dp) + .width(100.dp) + .height(150.dp), // Устанавливаем ширину и высоту карточки + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Image( + painter = painterResource(id = pet.imageResourceId), + contentDescription = null, + modifier = Modifier + .size(100.dp) + .padding(8.dp) + ) + Text( + text = String.format("%s%n%s", pet.name, pet.birthday), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + } +} diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/pet/list/PetListViewModel.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/list/PetListViewModel.kt new file mode 100644 index 0000000..8686a83 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/list/PetListViewModel.kt @@ -0,0 +1,30 @@ +package ru.ulstu.`is`.pmu.ui.pet.list + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.pmu.common.PetRepository +import ru.ulstu.`is`.pmu.common.MyViewModel +import ru.ulstu.`is`.pmu.database.entyties.model.Pet + +class PetListViewModel( + private val petRepository: PetRepository +) : MyViewModel() { + + var petListUiState: Flow> = petRepository.getAllPets() + + fun deletePet(pet: Pet) { + runInScope( + actionSuccess = { + petRepository.deletePet(pet) + + } + ) + } + fun getByUser(id: Int) { + runInScope( + actionSuccess = { + petListUiState = petRepository.getByUser(id) + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/pet/list/PetUserList.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/list/PetUserList.kt new file mode 100644 index 0000000..47cb310 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/list/PetUserList.kt @@ -0,0 +1,241 @@ +package ru.ulstu.`is`.pmu.ui.pet.list + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeOut +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.fillMaxSize +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.lazy.LazyColumn +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +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.MaterialTheme +import androidx.compose.material3.Scaffold +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.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.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import ru.ulstu.`is`.pmu.common.AppViewModelProvider +import ru.ulstu.`is`.pmu.database.entyties.model.Pet +import ru.ulstu.`is`.pmu.ui.navigation.Screen + + +@Composable +fun PetUserList( + navController: NavController, + idUser: Int, + viewModel: PetListViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + viewModel.getByUser(idUser) + val petListUiState = viewModel.petListUiState.collectAsLazyPagingItems() + Scaffold( + topBar = {} + ) { innerPadding -> + PetUserList( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + petList = petListUiState, + onClick = { uid: Int -> + val route = Screen.PetEdit.route.replace("{id}", uid.toString()) + navController.navigate(route) + } + ) + } +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SwipeToDelete( + dismissState: DismissState, + pet: Pet, + onClick: (uid: 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 -> Color.White + }, 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 = { + PetUserListItem(pet = pet, + modifier = Modifier + .padding(vertical = 7.dp) + .clickable { onClick(pet.uid) }) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@Composable +private fun PetUserList( + modifier: Modifier = Modifier, + petList: LazyPagingItems, + onClick: (uid: Int) -> Unit, +) { + val refreshScope = rememberCoroutineScope() + var refreshing by remember { mutableStateOf(false) } + fun refresh() = refreshScope.launch { + refreshing = true + petList.refresh() + refreshing = false + } + + val state = rememberPullRefreshState(refreshing, ::refresh) + Box( + modifier = modifier.pullRefresh(state) + ) { + Column( + modifier = modifier + ) { + LazyColumn( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + items( + count = petList.itemCount, + key = petList.itemKey(), + contentType = petList.itemContentType() + ) { index -> + val pet = petList[index] + pet?.let { + var show by remember { mutableStateOf(true) } + val dismissState = rememberDismissState( + confirmValueChange = { + if (it == DismissValue.DismissedToStart || + it == DismissValue.DismissedToEnd + ) { + show = false + true + } else false + }, positionalThreshold = { 200.dp.toPx() } + ) + + AnimatedVisibility( + show, exit = fadeOut(spring()) + ) { + SwipeToDelete( + dismissState = dismissState, + pet = pet, + onClick = onClick + ) + } + } + } + } + PullRefreshIndicator( + refreshing, state, + Modifier + .align(CenterHorizontally) + .zIndex(100f) + ) + } + } +} + +@Composable +private fun PetUserListItem( + pet: Pet, + modifier: Modifier = Modifier +) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + ) { + Card( + modifier = modifier + .padding(8.dp) + .width(100.dp) + .height(150.dp), // Устанавливаем ширину и высоту карточки + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Image( + painter = painterResource(id = pet.imageResourceId), + contentDescription = null, + modifier = Modifier + .size(100.dp) + .padding(8.dp) + ) + Text( + text = String.format("%s%n%s", pet.name, pet.birthday), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/pet/list/UserList.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/list/UserList.kt new file mode 100644 index 0000000..4603158 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/list/UserList.kt @@ -0,0 +1,213 @@ +package ru.ulstu.`is`.pmu.ui.pet.list + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +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.Scaffold +import androidx.compose.material3.SwipeToDismiss +import androidx.compose.material3.Text +import androidx.compose.material3.rememberDismissState +import androidx.compose.runtime.Composable +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.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemContentType +import androidx.paging.compose.itemKey +import kotlinx.coroutines.launch +import ru.ulstu.`is`.pmu.common.AppViewModelProvider +import ru.ulstu.`is`.pmu.database.entyties.model.User +import ru.ulstu.`is`.pmu.ui.navigation.Screen + + +@Composable +fun UserList( + navController: NavController, + viewModel: UserListViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val petListUiState = viewModel.userListUiState.collectAsLazyPagingItems() + Scaffold( + topBar = {} + ) { innerPadding -> + UserList( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + userList = petListUiState, + onClick = { uid: Int -> + val route = Screen.PetUserList.route.replace("{id}", uid.toString()) + navController.navigate(route) + } + ) + } +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SwipeToDelete( + dismissState: DismissState, + user: User, + onClick: (uid: 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 -> Color.White + }, 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 = { + UserListItem(user = user, + modifier = Modifier + .padding(vertical = 7.dp) + .clickable { onClick(user.uid) }) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@Composable +private fun UserList( + modifier: Modifier = Modifier, + userList: LazyPagingItems, + onClick: (uid: Int) -> Unit, +) { + val refreshScope = rememberCoroutineScope() + var refreshing by remember { mutableStateOf(false) } + fun refresh() = refreshScope.launch { + refreshing = true + userList.refresh() + refreshing = false + } + + val state = rememberPullRefreshState(refreshing, ::refresh) + Box( + modifier = modifier.pullRefresh(state) + ) { + Column( + modifier = modifier + ) { + LazyColumn( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + items( + count = userList.itemCount, + key = userList.itemKey(), + contentType = userList.itemContentType() + ) { index -> + val user = userList[index] + user?.let { + var show by remember { mutableStateOf(true) } + val dismissState = rememberDismissState( + confirmValueChange = { + if (it == DismissValue.DismissedToStart || + it == DismissValue.DismissedToEnd + ) { + show = false + true + } else false + }, positionalThreshold = { 200.dp.toPx() } + ) + + AnimatedVisibility( + show, exit = fadeOut(spring()) + ) { + SwipeToDelete( + dismissState = dismissState, + user = user, + onClick = onClick + ) + } + } + } + } + PullRefreshIndicator( + refreshing, state, + Modifier + .align(CenterHorizontally) + .zIndex(100f) + ) + } + } +} + +@Composable +private fun UserListItem( + user: User, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = modifier.padding(all = 10.dp) + ) { + Text( + text = String.format("%s (%s %s)", user.login, user.nameFirst, user.nameLast) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/pet/list/UserListViewModel.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/list/UserListViewModel.kt new file mode 100644 index 0000000..989dc94 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/list/UserListViewModel.kt @@ -0,0 +1,16 @@ +package ru.ulstu.`is`.pmu.ui.pet.list + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.pmu.common.MyViewModel +import ru.ulstu.`is`.pmu.common.UserRepository +import ru.ulstu.`is`.pmu.database.entyties.model.User + +class UserListViewModel( + private val userRepository: UserRepository +) : MyViewModel() { + + var userListUiState: Flow> = userRepository.getAllUsers() + +} + diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/pet/report/ReportPage.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/report/ReportPage.kt new file mode 100644 index 0000000..65d59e5 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/report/ReportPage.kt @@ -0,0 +1,197 @@ +package ru.ulstu.`is`.pmu.ui.pet.report + +import android.content.ContentResolver +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.graphics.Paint +import android.graphics.pdf.PdfDocument +import android.net.Uri +import android.os.Environment +import android.provider.MediaStore +import android.widget.CalendarView +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +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.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +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.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import ru.ulstu.`is`.pmu.R +import ru.ulstu.`is`.pmu.api.report.ReportRemote +import ru.ulstu.`is`.pmu.common.AppViewModelProvider +import java.io.IOException +import java.io.OutputStream +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReportPage (navController: NavController?, viewModel: ReportViewModel = viewModel(factory = AppViewModelProvider.Factory)) +{ + val context = LocalContext.current + + + val coroutineScope = rememberCoroutineScope() + val reportResultPageState = viewModel.reportResultPageUiState + + // Здесь вы можете определить startDate и endDate + val startDate = viewModel.reportPageUiState.reportDetails.startDate + val endDate = viewModel.reportPageUiState.reportDetails.endDate + + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) + { + Text( + text = stringResource(id = R.string.startDate), + style = MaterialTheme.typography.headlineLarge + ) + + CustomCalendarView(selectedDate = startDate) { selectedDate -> + viewModel.onUpdate(viewModel.reportPageUiState.reportDetails.copy(startDate = selectedDate)) + } + + Text( + text = stringResource(id = R.string.endDate), + style = MaterialTheme.typography.headlineLarge + ) + + + + CustomCalendarView(selectedDate = endDate) { selectedDate -> + viewModel.onUpdate(viewModel.reportPageUiState.reportDetails.copy(endDate = selectedDate)) + } + + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = {coroutineScope.launch { viewModel.getReport()} }, + enabled = viewModel.reportPageUiState.isEntryValid, + modifier = Modifier + .fillMaxWidth() + .padding(all = 10.dp) + .border(4.dp, MaterialTheme.colorScheme.onPrimary, shape = RoundedCornerShape(10.dp)), + shape = RoundedCornerShape(10.dp), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary) + ) { + Text("Сформировать отчет") + } + Spacer(modifier = Modifier.height(32.dp)) + Text( + text = "Результат", + style = MaterialTheme.typography.headlineLarge + ) + TableScreen(reportData = reportResultPageState.resReport) + Spacer(modifier = Modifier.height(16.dp)) + } +} +@Composable +fun CustomCalendarView( + selectedDate: Date, + onDateSelected: (Date) -> Unit +) { + AndroidView( + + modifier = Modifier.wrapContentSize(), + factory = { context -> + CalendarView(context).apply { + // Настройка CalendarView + val calendar = Calendar.getInstance() + calendar.time = selectedDate + + // Установка начальной выбранной даты + date = calendar.timeInMillis + + setOnDateChangeListener { _, year, month, dayOfMonth -> + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, month) + calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth) + val newSelectedDate = calendar.time + + // Установка новой выбранной даты + date = calendar.timeInMillis + + // Вызов колбэка для передачи выбранной даты в родительский компонент + onDateSelected(newSelectedDate) + } + } + } + ) +} + +@Composable +fun RowScope.TableCell( + text: String, + weight: Float +) { + Text( + text = text, + Modifier + .border(1.dp, Color.Black) + .weight(weight) + .padding(8.dp) + ) +} + + +@Composable +fun TableScreen(reportData: List) { + Column( + Modifier + .padding(16.dp)) { + + Row(Modifier.background(Color.White)) { + TableCell(text = "№", weight = 1f) + TableCell(text = "Id питомца", weight = 1f) + TableCell(text = "Имя", weight = 1f) + TableCell(text = "Д/р", weight = 1f) + TableCell(text = "Id хозяина", weight = 1f) + TableCell(text = "Логин", weight = 1f) + } + + // Here are all the lines of your table. + reportData.forEach { + val (reportId, petid, petname, birthday, userid, login) = it + Row(Modifier.fillMaxWidth()) { + TableCell(text = reportId.toString(), weight = 1f) + TableCell(text = petid.toString(), weight = 1f) + TableCell(text = petname, weight = 1f) + TableCell(text = birthday, weight = 1f) + TableCell(text = userid.toString(), weight = 1f) + TableCell(text = login, weight = 1f) + + } + } + } +} + diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/pet/report/ReportViewModel.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/report/ReportViewModel.kt new file mode 100644 index 0000000..1385786 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/pet/report/ReportViewModel.kt @@ -0,0 +1,53 @@ +package ru.ulstu.`is`.pmu.ui.pet.report + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import ru.ulstu.`is`.pmu.api.pet.RestPetRepository +import ru.ulstu.`is`.pmu.api.report.ReportRemote +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date + +class ReportViewModel(private val restPetRepository: RestPetRepository): ViewModel() { + var reportPageUiState by mutableStateOf(ReportPageUiState()) + private set + + var reportResultPageUiState by mutableStateOf(ReportResultPageUiState()) + private set + + fun onUpdate(reportDetails: ReportDetails) { + reportPageUiState = ReportPageUiState(reportDetails = reportDetails, isEntryValid = validateInput(reportDetails)) + } + + private fun validateInput(uiState: ReportDetails = reportPageUiState.reportDetails): Boolean { + return with(uiState) { + startDate != Date(0) && endDate != Date(0) && startDate < endDate + } + } + + suspend fun getReport() { + + val startDateQueryParam = SimpleDateFormat("dd.MM.yyyy").format(reportPageUiState.reportDetails.startDate) + val endDateQueryParam = SimpleDateFormat("dd.MM.yyyy").format(reportPageUiState.reportDetails.endDate) + + // Вызов репозитория с отформатированными датами + val res = restPetRepository.getReport(startDateQueryParam, endDateQueryParam) + reportResultPageUiState = ReportResultPageUiState(res) + } +} + +data class ReportDetails( + val startDate: Date = Calendar.getInstance().time, + val endDate: Date = Calendar.getInstance().time +) + +data class ReportPageUiState( + val reportDetails: ReportDetails = ReportDetails(), + val isEntryValid: Boolean = false +) + +data class ReportResultPageUiState( + var resReport: List = emptyList() +) \ No newline at end of file diff --git a/app/src/main/res/drawable/avatar.jpg b/app/src/main/res/drawable/avatar.jpg new file mode 100644 index 0000000..ac60efc Binary files /dev/null and b/app/src/main/res/drawable/avatar.jpg differ diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/pet1.jpg b/app/src/main/res/drawable/pet1.jpg new file mode 100644 index 0000000..570ddc4 Binary files /dev/null and b/app/src/main/res/drawable/pet1.jpg differ diff --git a/app/src/main/res/drawable/pet2.jpg b/app/src/main/res/drawable/pet2.jpg new file mode 100644 index 0000000..08e38a8 Binary files /dev/null and b/app/src/main/res/drawable/pet2.jpg differ diff --git a/app/src/main/res/drawable/pet3.jpg b/app/src/main/res/drawable/pet3.jpg new file mode 100644 index 0000000..89a4dfe Binary files /dev/null and b/app/src/main/res/drawable/pet3.jpg differ diff --git a/app/src/main/res/drawable/pet4.jpg b/app/src/main/res/drawable/pet4.jpg new file mode 100644 index 0000000..532cc95 Binary files /dev/null and b/app/src/main/res/drawable/pet4.jpg differ diff --git a/app/src/main/res/drawable/pet5.jpg b/app/src/main/res/drawable/pet5.jpg new file mode 100644 index 0000000..575296e Binary files /dev/null and b/app/src/main/res/drawable/pet5.jpg differ diff --git a/app/src/main/res/drawable/pet6.jpg b/app/src/main/res/drawable/pet6.jpg new file mode 100644 index 0000000..59bea24 Binary files /dev/null and b/app/src/main/res/drawable/pet6.jpg differ diff --git a/app/src/main/res/drawable/pet7.jpg b/app/src/main/res/drawable/pet7.jpg new file mode 100644 index 0000000..b062fd9 Binary files /dev/null and b/app/src/main/res/drawable/pet7.jpg differ diff --git a/app/src/main/res/drawable/pet8.jpg b/app/src/main/res/drawable/pet8.jpg new file mode 100644 index 0000000..92e90db Binary files /dev/null and b/app/src/main/res/drawable/pet8.jpg differ diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..14b8adc --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,16 @@ + + pmu-demo + Имя + Заметка + Питомцы + Пользователи + Назад + Загрузка… + Отчёт + Питомец + Записи о питомцых отсутствуют + Хозяин не указан + Сохранить + Дата начала + Дата конца + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..bf6a6ee --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +