commit af8f416ea8aecd9a9192205c60a527587fa0955a Author: Stranni15k Date: Wed Dec 20 16:34:27 2023 +0400 work 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/.vs/LabWork5/v17/.wsuo b/.vs/LabWork5/v17/.wsuo new file mode 100644 index 0000000..7b2fd1e Binary files /dev/null and b/.vs/LabWork5/v17/.wsuo differ diff --git a/.vs/ProjectSettings.json b/.vs/ProjectSettings.json new file mode 100644 index 0000000..f8b4888 --- /dev/null +++ b/.vs/ProjectSettings.json @@ -0,0 +1,3 @@ +{ + "CurrentProjectSetting": null +} \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 0000000..6b61141 --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,6 @@ +{ + "ExpandedNodes": [ + "" + ], + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000..59f5c1e Binary files /dev/null and b/.vs/slnx.sqlite differ 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..4a8c9c5 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,97 @@ +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 = 33 + + defaultConfig { + applicationId = "ru.ulstu.is.pmu" + minSdk = 24 + targetSdk = 33 + 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") + + + // 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..a6f9344 --- /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..d6ae090 --- /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 TaskApplication : 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/MyServerService.kt b/app/src/main/java/ru/ulstu/is/pmu/api/MyServerService.kt new file mode 100644 index 0000000..633c626 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/api/MyServerService.kt @@ -0,0 +1,80 @@ +package ru.ulstu.`is`.pmu.api + +import android.util.Log +import androidx.core.content.PackageManagerCompat.LOG_TAG +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import kotlinx.serialization.json.Json +import okhttp3.Interceptor +import okhttp3.Interceptor.* +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +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.TaskRemote + + +interface MyServerService { + @GET("users") + suspend fun getUsers(): List + + @GET("tasks") + suspend fun getTasks( + @Query("_page") page: Int, + @Query("_limit") limit: Int, + ): List + + @GET("tasks/{id}") + suspend fun getTask( + @Path("id") id: Int, + ): TaskRemote + + @POST("tasks") + suspend fun createTask( + @Body task: TaskRemote, + ): TaskRemote + + @PUT("tasks/{id}") + suspend fun updateTask( + @Path("id") id: Int, + @Body task: TaskRemote, + ): TaskRemote + + @DELETE("tasks/{id}") + suspend fun deleteTask( + @Path("id") id: Int, + ): TaskRemote + + 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/TaskRemote.kt b/app/src/main/java/ru/ulstu/is/pmu/api/model/TaskRemote.kt new file mode 100644 index 0000000..6d02c10 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/api/model/TaskRemote.kt @@ -0,0 +1,32 @@ +package ru.ulstu.`is`.pmu.api.model + +import kotlinx.serialization.Serializable +import ru.ulstu.`is`.pmu.database.task.model.Task + +@Serializable +data class TaskRemote( + val id: Int = 0, + val name: String = "", + val description: String = "", + val endDate: String = "", + val favorite: Boolean, + val userId: Int, +) + +fun TaskRemote.toTask(): Task = Task( + id, + name, + description, + endDate, + favorite = false, + userId +) + +fun Task.toTaskRemote(): TaskRemote = TaskRemote( + uid, + name, + description, + endDate, + favorite = false, + userId = 1 +) \ 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..f66e940 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/api/model/UserRemote.kt @@ -0,0 +1,17 @@ +package ru.ulstu.`is`.pmu.api.model + +import kotlinx.serialization.Serializable +import ru.ulstu.`is`.pmu.database.task.model.User + +@Serializable +data class UserRemote( + val id: Int = 0, + val name: String, + val login: String +) + +fun UserRemote.toUser(): User = User( + id, + name, + login +) diff --git a/app/src/main/java/ru/ulstu/is/pmu/api/task/RestTaskRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/api/task/RestTaskRepository.kt new file mode 100644 index 0000000..18efb59 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/api/task/RestTaskRepository.kt @@ -0,0 +1,65 @@ +package ru.ulstu.`is`.pmu.api.task + +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.user.RestUserRepository +import ru.ulstu.`is`.pmu.api.model.toTask +import ru.ulstu.`is`.pmu.api.model.toTaskRemote +import ru.ulstu.`is`.pmu.common.AppContainer +import ru.ulstu.`is`.pmu.common.TaskRepository +import ru.ulstu.`is`.pmu.database.AppDatabase +import ru.ulstu.`is`.pmu.database.remotekeys.repository.OfflineRemoteKeyRepository +import ru.ulstu.`is`.pmu.database.task.model.Task +import ru.ulstu.`is`.pmu.database.task.repository.OfflineTaskRepository + +class RestTaskRepository( + private val service: MyServerService, + private val dbTaskRepository: OfflineTaskRepository, + private val dbRemoteKeyRepository: OfflineRemoteKeyRepository, + private val userRestRepository: RestUserRepository, + private val database: AppDatabase +) : TaskRepository { + override fun getAllTasks(): Flow> { + Log.d(RestTaskRepository::class.simpleName, "Get tasks") + + val pagingSourceFactory = { dbTaskRepository.getAllTasksPagingSource() } + + @OptIn(ExperimentalPagingApi::class) + return Pager( + config = PagingConfig( + pageSize = AppContainer.LIMIT, + enablePlaceholders = false + ), + remoteMediator = TaskRemoteMediator( + service, + dbTaskRepository, + dbRemoteKeyRepository, + userRestRepository, + database, + ), + pagingSourceFactory = pagingSourceFactory + ).flow + } + + override suspend fun getTask(uid: Int): Task = + service.getTask(uid).toTask() + + override suspend fun insertTask(task: Task) { + service.createTask(task.toTaskRemote()).toTask() + } + + override suspend fun updateTask(task: Task) { + service.updateTask(task.uid, task.toTaskRemote()).toTask() + } + + override suspend fun deleteTask(task: Task) { + service.deleteTask(task.uid).toTask() + } + + +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/api/task/StudentRemoteMediator.kt b/app/src/main/java/ru/ulstu/is/pmu/api/task/StudentRemoteMediator.kt new file mode 100644 index 0000000..b27cc15 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/api/task/StudentRemoteMediator.kt @@ -0,0 +1,110 @@ +package ru.ulstu.`is`.pmu.api.task + +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.user.RestUserRepository +import ru.ulstu.`is`.pmu.api.model.toTask +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.task.model.Task +import ru.ulstu.`is`.pmu.database.task.repository.OfflineTaskRepository +import java.io.IOException + +@OptIn(ExperimentalPagingApi::class) +class TaskRemoteMediator( + private val service: MyServerService, + private val dbTaskRepository: OfflineTaskRepository, + 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 tasks = service.getTasks(page, state.config.pageSize).map { it.toTask() } + val endOfPaginationReached = tasks.isEmpty() + database.withTransaction { + if (loadType == LoadType.REFRESH) { + dbRemoteKeyRepository.deleteRemoteKey(RemoteKeyType.STUDENT) + dbTaskRepository.clearTasks() + } + val prevKey = if (page == 1) null else page - 1 + val nextKey = if (endOfPaginationReached) null else page + 1 + val keys = tasks.map { + RemoteKeys( + entityId = it.uid, + type = RemoteKeyType.STUDENT, + prevKey = prevKey, + nextKey = nextKey + ) + } + userRestRepository.getAllUsers() + dbRemoteKeyRepository.createRemoteKeys(keys) + dbTaskRepository.insertTasks(tasks) + } + 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 { task -> + dbRemoteKeyRepository.getAllRemoteKeys(task.uid, RemoteKeyType.STUDENT) + } + } + + private suspend fun getRemoteKeyForFirstItem(state: PagingState): RemoteKeys? { + return state.pages.firstOrNull { it.data.isNotEmpty() }?.data?.firstOrNull() + ?.let { task -> + dbRemoteKeyRepository.getAllRemoteKeys(task.uid, RemoteKeyType.STUDENT) + } + } + + private suspend fun getRemoteKeyClosestToCurrentPosition( + state: PagingState + ): RemoteKeys? { + return state.anchorPosition?.let { position -> + state.closestItemToPosition(position)?.uid?.let { taskUid -> + dbRemoteKeyRepository.getAllRemoteKeys(taskUid, RemoteKeyType.STUDENT) + } + } + } + +} \ 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..e15e5e9 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/api/user/RestUserRepository.kt @@ -0,0 +1,34 @@ +package ru.ulstu.`is`.pmu.api.user + +import android.util.Log +import ru.ulstu.`is`.pmu.api.MyServerService +import ru.ulstu.`is`.pmu.api.task.RestTaskRepository +import ru.ulstu.`is`.pmu.database.task.model.User +import ru.ulstu.`is`.pmu.common.UserRepository +import ru.ulstu.`is`.pmu.database.task.repository.OfflineUserRepository +import ru.ulstu.`is`.pmu.api.model.toUser + +class RestUserRepository( + private val service: MyServerService, + private val dbUserRepository: OfflineUserRepository, +): UserRepository { + override suspend fun getAllUsers(): List { + Log.d(RestTaskRepository::class.simpleName, "Get users") + + val existUsers = dbUserRepository.getAllUsers().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.createUser(user) + } + existUsers[user.uid] = user + } + + return existUsers.map { it.value }.sortedBy { it.uid } + } +} \ 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..55f11e5 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/common/AppContainer.kt @@ -0,0 +1,47 @@ +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.task.RestTaskRepository +import ru.ulstu.`is`.pmu.database.AppDatabase +import ru.ulstu.`is`.pmu.database.remotekeys.repository.OfflineRemoteKeyRepository +import ru.ulstu.`is`.pmu.database.task.repository.OfflineUserRepository +import ru.ulstu.`is`.pmu.database.task.repository.OfflineTaskRepository + +interface AppContainer { + val taskRestRepository: RestTaskRepository + val userRestRepository: RestUserRepository + + companion object { + const val TIMEOUT = 5000L + const val LIMIT = 10 + } +} + +class AppDataContainer(private val context: Context) : AppContainer { + private val taskRepository: OfflineTaskRepository by lazy { + OfflineTaskRepository(AppDatabase.getInstance(context).taskDao()) + } + private val userRepository: OfflineUserRepository by lazy { + OfflineUserRepository(AppDatabase.getInstance(context).userDao()) + } + private val remoteKeyRepository: OfflineRemoteKeyRepository by lazy { + OfflineRemoteKeyRepository(AppDatabase.getInstance(context).remoteKeysDao()) + } + override val taskRestRepository: RestTaskRepository by lazy { + RestTaskRepository( + MyServerService.getInstance(), + taskRepository, + remoteKeyRepository, + userRestRepository, + AppDatabase.getInstance(context) + ) + } + override val userRestRepository: RestUserRepository by lazy { + RestUserRepository( + MyServerService.getInstance(), + userRepository + ) + } +} \ 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..bf8df32 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/common/AppViewModelProvider.kt @@ -0,0 +1,31 @@ +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.TaskApplication +import ru.ulstu.`is`.pmu.ui.task.edit.UserDropDownViewModel +import ru.ulstu.`is`.pmu.ui.task.edit.TaskEditViewModel +import ru.ulstu.`is`.pmu.ui.task.list.TaskListViewModel + +object AppViewModelProvider { + val Factory = viewModelFactory { + initializer { + TaskListViewModel(taskApplication().container.taskRestRepository) + } + initializer { + TaskEditViewModel( + this.createSavedStateHandle(), + taskApplication().container.taskRestRepository + ) + } + initializer { + UserDropDownViewModel(taskApplication().container.userRestRepository) + } + } +} + +fun CreationExtras.taskApplication(): TaskApplication = + (this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as TaskApplication) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/common/TaskRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/common/TaskRepository.kt new file mode 100644 index 0000000..4f40b17 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/common/TaskRepository.kt @@ -0,0 +1,13 @@ +package ru.ulstu.`is`.pmu.common + +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.pmu.database.task.model.Task + +interface TaskRepository { + fun getAllTasks(): Flow> + suspend fun getTask(uid: Int): Task + suspend fun insertTask(task: Task) + suspend fun updateTask(task: Task) + suspend fun deleteTask(task: Task) +} \ 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..aeb0e52 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/common/UserRepository.kt @@ -0,0 +1,7 @@ +package ru.ulstu.`is`.pmu.common + +import ru.ulstu.`is`.pmu.database.task.model.User + +interface UserRepository { + suspend fun getAllUsers(): List +} \ 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..e78aa69 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/AppDatabase.kt @@ -0,0 +1,75 @@ +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.task.dao.UserDao +import ru.ulstu.`is`.pmu.database.task.dao.TaskDao +import ru.ulstu.`is`.pmu.database.task.model.User +import ru.ulstu.`is`.pmu.database.task.model.Task + +@Database( + entities = [ + Task::class, + User::class, + RemoteKeys::class, + ], + version = 1, + exportSchema = false +) +abstract class AppDatabase : RoomDatabase() { + abstract fun taskDao(): TaskDao + abstract fun userDao(): UserDao + abstract fun remoteKeysDao(): RemoteKeysDao + + companion object { + private const val DB_NAME: String = "pmy-db" + + @Volatile + private var INSTANCE: AppDatabase? = null + + private suspend fun populateDatabase() { + INSTANCE?.let { database -> + // Users + val userDao = database.userDao() + val user1 = User(1, "Sergey", "brook.sergey@gmail.com") + userDao.insert(user1) + // Tasks + val taskDao = database.taskDao() + val task1 = Task("First1", "Last1","12.12.2023",false, user1) + val task2 = Task("First2", "Last2","15.12.2023",false, user1) + val task3 = Task("First3", "Last3","10.12.2023",false, user1) + val task4 = Task("First4", "Last4","31.12.2023",false, user1) + val task5 = Task("First5", "Last5","05.12.2023",false, user1) + taskDao.insert(task1) + taskDao.insert(task2) + taskDao.insert(task3) + taskDao.insert(task4) + taskDao.insert(task5) + } + } + + fun getInstance(appContext: Context): AppDatabase { + return INSTANCE ?: synchronized(this) { + Room.databaseBuilder( + appContext, + AppDatabase::class.java, + DB_NAME + ) +// .addCallback(object : Callback() { +// override fun onCreate(db: SupportSQLiteDatabase) { +// super.onCreate(db) +// CoroutineScope(Dispatchers.IO).launch { +// populateDatabase() +// } +// } +// }) + .build() + .also { INSTANCE = it } + } + } + } +} \ 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..1e095ea --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/remotekeys/model/RemoteKeys.kt @@ -0,0 +1,26 @@ +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.task.model.Task + +enum class RemoteKeyType(private val type: String) { + STUDENT(Task::class.simpleName ?: "Task"); + + @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/database/student/dao/TaskDao.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/TaskDao.kt new file mode 100644 index 0000000..4bfdb77 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/TaskDao.kt @@ -0,0 +1,31 @@ +package ru.ulstu.`is`.pmu.database.task.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.task.model.Task + +@Dao +interface TaskDao { + @Query("select * from tasks order by name collate nocase asc") + fun getAll(): PagingSource + + @Query("select * from tasks where tasks.uid = :uid") + fun getByUid(uid: Int): Flow + + @Insert + suspend fun insert(vararg task: Task) + + @Update + suspend fun update(task: Task) + + @Delete + suspend fun delete(task: Task) + + @Query("DELETE FROM tasks") + suspend fun deleteAll() +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/UserDao.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/UserDao.kt new file mode 100644 index 0000000..fb02a8d --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/student/dao/UserDao.kt @@ -0,0 +1,23 @@ +package ru.ulstu.`is`.pmu.database.task.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import ru.ulstu.`is`.pmu.database.task.model.User + +@Dao +interface UserDao { + @Query("select * from users order by login collate nocase asc") + suspend fun getAll(): List + + @Insert + suspend fun insert(user: User) + + @Update + suspend fun update(user: User) + + @Delete + suspend fun delete(user: User) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/model/Task.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/model/Task.kt new file mode 100644 index 0000000..863437c --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/student/model/Task.kt @@ -0,0 +1,80 @@ +package ru.ulstu.`is`.pmu.database.task.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Ignore +import androidx.room.PrimaryKey + +@Entity( + tableName = "tasks", foreignKeys = [ + ForeignKey( + entity = User::class, + parentColumns = ["uid"], + childColumns = ["user_id"], + onDelete = ForeignKey.RESTRICT, + onUpdate = ForeignKey.RESTRICT + ) + ] +) +data class Task( + @PrimaryKey(autoGenerate = true) + val uid: Int = 0, + @ColumnInfo(name = "name") + val name: String, + @ColumnInfo(name = "description") + val description: String, + @ColumnInfo(name = "endDate") + val endDate: String, + @ColumnInfo(name = "favorite") + val favorite: Boolean, + @ColumnInfo(name = "user_id", index = true) + val userId: Int, +) { + + @Ignore + constructor( + name: String, + description: String, + endDate: String, + favorite: Boolean, + user: User, + ) : this(0, name, description, endDate, favorite, user.uid) + + + companion object { + fun getTask(index: Int = 0): Task { + return Task( + index, + "Test1234567", + "Test1235", + "11.12.2023", + true, + 0, + ) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Task + if (uid != other.uid) return false + if (name != other.name) return false + if (description != other.description) return false + if (endDate != other.endDate) return false + if (favorite != other.favorite) return false + if (userId != other.userId) return false + return true + } + + override fun hashCode(): Int { + var result = uid + result = 31 * result + name.hashCode() + result = 31 * result + description.hashCode() + result = 31 * result + endDate.hashCode() + result = 31 * result + favorite.hashCode() + result = 31 * result + userId + return result + } +} diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/model/User.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/model/User.kt new file mode 100644 index 0000000..e1fb6cb --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/student/model/User.kt @@ -0,0 +1,41 @@ +package ru.ulstu.`is`.pmu.database.task.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "users") +data class User( + @PrimaryKey(autoGenerate = true) + val uid: Int = 0, + @ColumnInfo(name = "user") + val name: String, + @ColumnInfo(name = "login") + val login: String +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as User + + if (uid != other.uid) return false + if (name != other.name) return false + + return true + } + + override fun hashCode(): Int { + var result = uid + result = 31 * result + name.hashCode() + return result + } + + companion object { + val DEMO_User = User( + 0, + "User", + "Login" + ) + } +} diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineTaskRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineTaskRepository.kt new file mode 100644 index 0000000..108b496 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineTaskRepository.kt @@ -0,0 +1,37 @@ +package ru.ulstu.`is`.pmu.database.task.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.TaskRepository +import ru.ulstu.`is`.pmu.database.task.dao.TaskDao +import ru.ulstu.`is`.pmu.database.task.model.Task + +class OfflineTaskRepository(private val taskDao: TaskDao) : TaskRepository { + override fun getAllTasks(): Flow> = Pager( + config = PagingConfig( + pageSize = AppContainer.LIMIT, + enablePlaceholders = false + ), + pagingSourceFactory = taskDao::getAll + ).flow + + override suspend fun getTask(uid: Int): Task = taskDao.getByUid(uid).first() + + override suspend fun insertTask(task: Task) = taskDao.insert(task) + + override suspend fun updateTask(task: Task) = taskDao.update(task) + + override suspend fun deleteTask(task: Task) = taskDao.delete(task) + + fun getAllTasksPagingSource(): PagingSource = taskDao.getAll() + + suspend fun insertTasks(tasks: List) = + taskDao.insert(*tasks.toTypedArray()) + + suspend fun clearTasks() = taskDao.deleteAll() +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineUserRepository.kt b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineUserRepository.kt new file mode 100644 index 0000000..924675d --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/database/student/repository/OfflineUserRepository.kt @@ -0,0 +1,11 @@ +package ru.ulstu.`is`.pmu.database.task.repository + +import ru.ulstu.`is`.pmu.common.UserRepository +import ru.ulstu.`is`.pmu.database.task.dao.UserDao +import ru.ulstu.`is`.pmu.database.task.model.User + +class OfflineUserRepository(private val userDao: UserDao) : UserRepository { + override suspend fun getAllUsers(): List = userDao.getAll() + suspend fun createUser(user: User) = userDao.insert(user) + suspend fun updateUser(user: User) = userDao.update(user) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/about/About.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/about/About.kt new file mode 100644 index 0000000..f06726e --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/about/About.kt @@ -0,0 +1,66 @@ +package ru.ulstu.`is`.pmu.ui.about + +import android.content.Intent +import android.content.res.Configuration +import android.net.Uri +import android.widget.TextView +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.Modifier +import androidx.compose.ui.platform.LocalContext +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 ru.ulstu.`is`.pmu.R +import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme + +@Composable +fun About() { + val localContext = LocalContext.current + val aboutText = localContext.resources.getText(R.string.about_text) + + val urlOnClick = { + val openURL = Intent(Intent.ACTION_VIEW) + openURL.data = Uri.parse("https://ulstu.ru/") + localContext.startActivity(openURL) + } + + Column(Modifier.padding(all = 10.dp)) { + AndroidView( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = urlOnClick), + factory = { context -> TextView(context) }, + update = { it.text = aboutText } + ) + Spacer(Modifier.padding(bottom = 10.dp)) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = urlOnClick + ) { + Text(stringResource(id = R.string.about_title)) + } + } +} + +@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun AboutPreview() { + PmudemoTheme { + Surface( + color = MaterialTheme.colorScheme.background + ) { + About() + } + } +} \ No newline at end of file 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..905c136 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/MainNavbar.kt @@ -0,0 +1,135 @@ +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.about.About +import ru.ulstu.`is`.pmu.ui.task.edit.TaskEdit +import ru.ulstu.`is`.pmu.ui.task.list.TaskList + +@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.TaskList.route, + modifier.padding(innerPadding) + ) { + composable(Screen.TaskList.route) { TaskList(navController) } + composable(Screen.About.route) { About() } + composable( + Screen.TaskEdit.route, + arguments = listOf(navArgument("id") { type = NavType.IntType }) + ) { + TaskEdit(navController) + } + } +} + +@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..156244f --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/navigation/Screen.kt @@ -0,0 +1,38 @@ +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.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 +) { + TaskList( + "task-list", R.string.task_main_title, Icons.Filled.List + ), + About( + "about", R.string.about_title, Icons.Filled.Info + ), + TaskEdit( + "task-edit/{id}", R.string.task_view_title, showInBottomBar = false + ); + + companion object { + val bottomBarItems = listOf( + TaskList, + About, + ) + + 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/student/edit/GroupDropDownViewModel.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/GroupDropDownViewModel.kt new file mode 100644 index 0000000..2606a58 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/GroupDropDownViewModel.kt @@ -0,0 +1,46 @@ +package ru.ulstu.`is`.pmu.ui.task.edit + +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 ru.ulstu.`is`.pmu.common.UserRepository +import ru.ulstu.`is`.pmu.database.task.model.User + +class UserDropDownViewModel( + private val userRepository: UserRepository +) : ViewModel() { + var usersListUiState by mutableStateOf(UsersListUiState()) + private set + + var userUiState by mutableStateOf(UserUiState()) + private set + + init { + viewModelScope.launch { + usersListUiState = UsersListUiState(userRepository.getAllUsers()) + } + } + + fun setCurrentUser(userId: Int) { + val user: User? = + usersListUiState.userList.firstOrNull { user -> user.uid == userId } + 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, name = name, login = login)) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/StudentEdit.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/StudentEdit.kt new file mode 100644 index 0000000..d16067d --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/StudentEdit.kt @@ -0,0 +1,187 @@ +package ru.ulstu.`is`.pmu.ui.task.edit + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.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.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.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +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.database.task.model.User +import ru.ulstu.`is`.pmu.database.task.model.Task +import ru.ulstu.`is`.pmu.common.AppViewModelProvider +import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme + +@Composable +fun TaskEdit( + navController: NavController, + viewModel: TaskEditViewModel = viewModel(factory = AppViewModelProvider.Factory), + userViewModel: UserDropDownViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val coroutineScope = rememberCoroutineScope() + userViewModel.setCurrentUser(viewModel.taskUiState.taskDetails.userId) + TaskEdit( + taskUiState = viewModel.taskUiState, + userUiState = userViewModel.userUiState, + usersListUiState = userViewModel.usersListUiState, + onClick = { + coroutineScope.launch { + viewModel.saveTask() + navController.popBackStack() + } + }, + onUpdate = viewModel::updateUiState, + onUserUpdate = userViewModel::updateUiState + ) +} + +@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?.name + ?: stringResource(id = R.string.task_user_not_select), + 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.name) + }, + onClick = { + onUserUpdate(user) + expanded = false + } + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TaskEdit( + taskUiState: TaskUiState, + userUiState: UserUiState, + usersListUiState: UsersListUiState, + onClick: () -> Unit, + onUpdate: (TaskDetails) -> Unit, + onUserUpdate: (User) -> Unit +) { + Column( + Modifier + .fillMaxWidth() + .padding(all = 10.dp) + ) { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = taskUiState.taskDetails.name, + onValueChange = { onUpdate(taskUiState.taskDetails.copy(name = it)) }, + label = { Text(stringResource(id = R.string.task_firstname)) }, + singleLine = true + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = taskUiState.taskDetails.description, + onValueChange = { onUpdate(taskUiState.taskDetails.copy(description = it)) }, + label = { Text(stringResource(id = R.string.task_lastname)) }, + singleLine = true + ) + UserDropDown( + userUiState = userUiState, + usersListUiState = usersListUiState, + onUserUpdate = { + onUpdate(taskUiState.taskDetails.copy(userId = it.uid)) + onUserUpdate(it) + } + ) + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = taskUiState.taskDetails.endDate, + onValueChange = { onUpdate(taskUiState.taskDetails.copy(endDate = it)) }, + label = { Text(stringResource(id = R.string.task_phone)) }, + singleLine = true + ) + Button( + onClick = onClick, + enabled = taskUiState.isEntryValid, + shape = MaterialTheme.shapes.small, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(R.string.task_save_button)) + } + } +} + +@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun TaskEditPreview() { + PmudemoTheme { + Surface( + color = MaterialTheme.colorScheme.background + ) { + TaskEdit( + taskUiState = Task.getTask().toUiState(true), + userUiState = User.DEMO_User.toUiState(), + usersListUiState = UsersListUiState(listOf()), + onClick = {}, + onUpdate = {}, + onUserUpdate = {} + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/StudentEditViewModel.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/StudentEditViewModel.kt new file mode 100644 index 0000000..79cbb55 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/student/edit/StudentEditViewModel.kt @@ -0,0 +1,96 @@ +package ru.ulstu.`is`.pmu.ui.task.edit + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import ru.ulstu.`is`.pmu.common.TaskRepository +import ru.ulstu.`is`.pmu.database.task.model.Task + +class TaskEditViewModel( + savedStateHandle: SavedStateHandle, + private val taskRepository: TaskRepository +) : ViewModel() { + + var taskUiState by mutableStateOf(TaskUiState()) + private set + + private val taskUid: Int = checkNotNull(savedStateHandle["id"]) + + init { + viewModelScope.launch { + if (taskUid > 0) { + taskUiState = taskRepository.getTask(taskUid) + .toUiState(true) + } + } + } + + fun updateUiState(taskDetails: TaskDetails) { + taskUiState = TaskUiState( + taskDetails = taskDetails, + isEntryValid = validateInput(taskDetails) + ) + } + + suspend fun saveTask() { + if (validateInput()) { + if (taskUid > 0) { + taskRepository.updateTask( + taskUiState.taskDetails.toTask(taskUid) + ) + } else { + taskRepository.insertTask( + taskUiState.taskDetails.toTask() + ) + } + } + } + + private fun validateInput(uiState: TaskDetails = taskUiState.taskDetails): Boolean { + return with(uiState) { + name.isNotBlank() + && description.isNotBlank() + && endDate.isNotBlank() + && userId > 0 + } + } +} + +data class TaskUiState( + val taskDetails: TaskDetails = TaskDetails(), + val isEntryValid: Boolean = false +) + +data class TaskDetails( + val name: String = "", + val description: String = "", + val endDate: String = "", + val favorite: Boolean = false, + val userId: Int = 0 +) + +fun TaskDetails.toTask(uid: Int = 0): Task = Task( + uid = uid, + name = name, + description = description, + endDate = endDate, + favorite = favorite, + userId = userId +) + +fun Task.toDetails(): TaskDetails = TaskDetails( + name = name, + description = description, + endDate = endDate, + favorite = favorite, + userId = userId +) + +fun Task.toUiState(isEntryValid: Boolean = false): TaskUiState = TaskUiState( + taskDetails = this.toDetails(), + isEntryValid = isEntryValid +) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/StudentList.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/StudentList.kt new file mode 100644 index 0000000..2d5e8cb --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/StudentList.kt @@ -0,0 +1,284 @@ +package ru.ulstu.`is`.pmu.ui.task.list + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.filled.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.graphics.Color +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.task.model.Task +import ru.ulstu.`is`.pmu.ui.navigation.Screen +import ru.ulstu.`is`.pmu.ui.theme.PmudemoTheme + +@Composable +fun TaskList( + navController: NavController, + viewModel: TaskListViewModel = viewModel(factory = AppViewModelProvider.Factory) +) { + val coroutineScope = rememberCoroutineScope() + val taskListUiState = viewModel.taskListUiState.collectAsLazyPagingItems() + Scaffold( + topBar = {}, + floatingActionButton = { + FloatingActionButton( + onClick = { + val route = Screen.TaskEdit.route.replace("{id}", 0.toString()) + navController.navigate(route) + }, + ) { + Icon(Icons.Filled.Add, "Добавить") + } + } + ) { innerPadding -> + TaskList( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + taskList = taskListUiState, + onClick = { uid: Int -> + val route = Screen.TaskEdit.route.replace("{id}", uid.toString()) + navController.navigate(route) + }, + onSwipe = { task: Task -> + coroutineScope.launch { + viewModel.deleteTask(task) + } + } + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DismissBackground(dismissState: DismissState) { + val color = when (dismissState.dismissDirection) { + DismissDirection.StartToEnd -> Color.Transparent + DismissDirection.EndToStart -> Color(0xFFFF1744) + null -> Color.Transparent + } + val direction = dismissState.dismissDirection + + Row( + modifier = Modifier + .fillMaxSize() + .background(color) + .padding(12.dp, 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End + ) { + if (direction == DismissDirection.EndToStart) { + Icon( + Icons.Default.Delete, + contentDescription = "delete", + tint = Color.White + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SwipeToDelete( + dismissState: DismissState, + task: Task, + onClick: (uid: Int) -> Unit +) { + SwipeToDismiss( + modifier = Modifier.zIndex(1f), + state = dismissState, + directions = setOf( + DismissDirection.EndToStart + ), + background = { + DismissBackground(dismissState) + }, + dismissContent = { + TaskListItem(task = task, + modifier = Modifier + .padding(vertical = 7.dp) + .clickable { onClick(task.uid) }) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@Composable +private fun TaskList( + modifier: Modifier = Modifier, + taskList: LazyPagingItems, + onClick: (uid: Int) -> Unit, + onSwipe: (task: Task) -> Unit +) { + val refreshScope = rememberCoroutineScope() + var refreshing by remember { mutableStateOf(false) } + fun refresh() = refreshScope.launch { + refreshing = true + taskList.refresh() + refreshing = false + } + + val state = rememberPullRefreshState(refreshing, ::refresh) + Box( + modifier = modifier.pullRefresh(state) + ) { + Column( + modifier = modifier.fillMaxSize() + ) { + LazyColumn(modifier = Modifier.padding(all = 10.dp)) { + items( + count = taskList.itemCount, + key = taskList.itemKey(), + contentType = taskList.itemContentType() + ) { index -> + val task = taskList[index] + task?.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, + task = task, + onClick = onClick + ) + } + + LaunchedEffect(show) { + if (!show) { + delay(800) + onSwipe(task) + } + } + } + } + } + PullRefreshIndicator( + refreshing, state, + Modifier + .align(CenterHorizontally) + .zIndex(100f) + ) + } + } +} + +@Composable +private fun TaskListItem( + task: Task, 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", task.name, task.description) + ) + } + } +} + +@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun TaskListPreview() { + PmudemoTheme { + Surface( + color = MaterialTheme.colorScheme.background + ) { + TaskList( + taskList = MutableStateFlow( + PagingData.from((1..20).map { i -> Task.getTask(i) }) + ).collectAsLazyPagingItems(), + onClick = {}, + onSwipe = {} + ) + } + } +} + +@Preview(name = "Light Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun TaskEmptyListPreview() { + PmudemoTheme { + Surface( + color = MaterialTheme.colorScheme.background + ) { + TaskList( + taskList = MutableStateFlow( + PagingData.empty() + ).collectAsLazyPagingItems(), + onClick = {}, + onSwipe = {} + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/StudentListViewModel.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/StudentListViewModel.kt new file mode 100644 index 0000000..c2a0337 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/student/list/StudentListViewModel.kt @@ -0,0 +1,18 @@ +package ru.ulstu.`is`.pmu.ui.task.list + +import androidx.lifecycle.ViewModel +import androidx.paging.PagingData +import kotlinx.coroutines.flow.Flow +import ru.ulstu.`is`.pmu.common.TaskRepository +import ru.ulstu.`is`.pmu.database.task.model.Task + +class TaskListViewModel( + private val taskRepository: TaskRepository +) : ViewModel() { + + val taskListUiState: Flow> = taskRepository.getAllTasks() + + suspend fun deleteTask(task: Task) { + taskRepository.deleteTask(task) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/theme/Color.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/theme/Color.kt new file mode 100644 index 0000000..86a8023 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package ru.ulstu.`is`.pmu.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/theme/Theme.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/theme/Theme.kt new file mode 100644 index 0000000..78d4a93 --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/theme/Theme.kt @@ -0,0 +1,70 @@ +package ru.ulstu.`is`.pmu.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun PmudemoTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/ulstu/is/pmu/ui/theme/Type.kt b/app/src/main/java/ru/ulstu/is/pmu/ui/theme/Type.kt new file mode 100644 index 0000000..40dc6ca --- /dev/null +++ b/app/src/main/java/ru/ulstu/is/pmu/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package ru.ulstu.`is`.pmu.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file 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/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..0bfdf34 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,19 @@ + + pmu-demo + Имя + Фамилия + Группа + Телефон + e-mail + Список студентов + Профиль студента + Записи о студентах отсутствуют + Группа не указана + Сохранить + О нас + +

Это текст о нас!

\n\n +

Здесь могла быть Ваша реклама!

\n\n +

Наш сайт ulstu.ru

+
+
\ 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 @@ + + + +