From e694edc3342b967d807b872d32901c5f840ed2f4 Mon Sep 17 00:00:00 2001 From: n08i40k Date: Thu, 3 Oct 2024 01:49:22 +0400 Subject: [PATCH] 1.4.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Администраторам добавлена возможность заменять расписание на текущую неделю. Исправлены недочёты в меню авторизации и регистрации. Добавлен класс AuthorizedMultipartRequest для отправки multipart запросов. Файлы дата классов разбиты на ещё большее количество файлов. Переименовано большинство классов с сетевыми запросами и их файлов для подгонки под однородный вид. Убрано много мусора в коде. Я наконец-то плюс минус разобрался в ViewModel'ах. Очень важная информация. --- .gitignore | 2 + .idea/appInsightsSettings.xml | 6 +- .idea/misc.xml | 2 +- app/build.gradle.kts | 4 +- .../polytechnic/next/data/AppContainer.kt | 48 ++-- .../n08i40k/polytechnic/next/data/MyResult.kt | 1 - .../schedule/impl/RemoteScheduleRepository.kt | 8 +- .../ScheduleReplacerRepository.kt | 16 ++ .../impl/FakeScheduleReplacerRepository.kt | 31 +++ .../impl/RemoteScheduleReplacerRepository.kt | 41 +++ .../users/impl/RemoteProfileRepository.kt | 5 +- .../ru/n08i40k/polytechnic/next/model/Day.kt | 13 + .../n08i40k/polytechnic/next/model/Group.kt | 36 --- .../n08i40k/polytechnic/next/model/Lesson.kt | 13 + .../polytechnic/next/model/LessonTime.kt | 6 + .../polytechnic/next/model/LessonType.kt | 15 ++ .../n08i40k/polytechnic/next/model/Profile.kt | 23 -- .../next/model/ScheduleReplacer.kt | 9 + .../polytechnic/next/model/UserRole.kt | 29 +++ .../network/AuthorizedMultipartRequest.kt | 130 ++++++++++ .../{Request.kt => NetworkConnection.kt} | 50 ---- .../polytechnic/next/network/RequestBase.kt | 27 ++ .../polytechnic/next/network/RequestUtils.kt | 27 ++ .../next/network/data/CachedRequest.kt | 16 +- .../next/network/data/auth/Login.kt | 2 +- .../next/network/data/auth/Register.kt | 2 +- .../next/network/data/profile/UsersMe.kt | 10 +- ...equest.kt => ScheduleGetCacheStatusReq.kt} | 12 +- ...se.kt => ScheduleGetCacheStatusResData.kt} | 2 +- ...upNames.kt => ScheduleGetGroupNamesReq.kt} | 12 +- ...ata.kt => ScheduleGetGroupNamesResData.kt} | 2 +- .../{ScheduleGet.kt => ScheduleGetReq.kt} | 14 +- ...etRequestData.kt => ScheduleGetReqData.kt} | 2 +- ...leGetResponse.kt => ScheduleGetResData.kt} | 2 +- ...ScheduleUpdate.kt => ScheduleUpdateReq.kt} | 14 +- ...equestData.kt => ScheduleUpdateReqData.kt} | 2 +- .../ScheduleReplacerClearReq.kt | 18 ++ .../ScheduleReplacerClearResData.kt | 8 + .../ScheduleReplacerGetReq.kt | 18 ++ .../ScheduleReplacerGetResData.kt | 5 + .../ScheduleReplacerSetReq.kt | 28 ++ .../polytechnic/next/ui/LoadingContent.kt | 22 +- .../polytechnic/next/ui/auth/AuthScreen.kt | 54 ++-- .../polytechnic/next/ui/main/Constants.kt | 7 +- .../polytechnic/next/ui/main/MainScreen.kt | 46 +++- .../next/ui/main/profile/ChangeGroupDialog.kt | 4 +- .../next/ui/main/replacer/ReplacerScreen.kt | 245 ++++++++++++++++++ .../next/ui/main/schedule/DayPager.kt | 2 +- .../ui/model/ScheduleReplacerViewModel.kt | 115 ++++++++ .../next/ui/model/ScheduleViewModel.kt | 5 +- app/src/main/res/values-ru/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + 52 files changed, 981 insertions(+), 238 deletions(-) create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/ScheduleReplacerRepository.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/impl/FakeScheduleReplacerRepository.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/impl/RemoteScheduleReplacerRepository.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/model/Day.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/model/Lesson.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/model/LessonTime.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/model/LessonType.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/model/ScheduleReplacer.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/model/UserRole.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/network/AuthorizedMultipartRequest.kt rename app/src/main/java/ru/n08i40k/polytechnic/next/network/{Request.kt => NetworkConnection.kt} (53%) create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/network/RequestBase.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/network/RequestUtils.kt rename app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/{ScheduleGetCacheStatusRequest.kt => ScheduleGetCacheStatusReq.kt} (51%) rename app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/{ScheduleGetCacheStatusResponse.kt => ScheduleGetCacheStatusResData.kt} (85%) rename app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/{ScheduleGetGroupNames.kt => ScheduleGetGroupNamesReq.kt} (50%) rename app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/{ScheduleGetGroupNamesResponseData.kt => ScheduleGetGroupNamesResData.kt} (76%) rename app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/{ScheduleGet.kt => ScheduleGetReq.kt} (59%) rename app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/{ScheduleGetRequestData.kt => ScheduleGetReqData.kt} (69%) rename app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/{ScheduleGetResponse.kt => ScheduleGetResData.kt} (88%) rename app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/{ScheduleUpdate.kt => ScheduleUpdateReq.kt} (58%) rename app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/{ScheduleUpdateRequestData.kt => ScheduleUpdateReqData.kt} (66%) create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerClearReq.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerClearResData.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerGetReq.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerGetResData.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerSetReq.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/replacer/ReplacerScreen.kt create mode 100644 app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ScheduleReplacerViewModel.kt diff --git a/.gitignore b/.gitignore index aa724b7..32aa5b0 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ .externalNativeBuild .cxx local.properties +.kotlin +app/release diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml index f23fb71..a1d7d06 100644 --- a/.idea/appInsightsSettings.xml +++ b/.idea/appInsightsSettings.xml @@ -20,9 +20,9 @@ diff --git a/.idea/misc.xml b/.idea/misc.xml index 4c85420..2904b84 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -7,7 +7,7 @@ - + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0b7bda7..2f3aee5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,8 +32,8 @@ android { applicationId = "ru.n08i40k.polytechnic.next" minSdk = 26 targetSdk = 35 - versionCode = 8 - versionName = "1.3.2" + versionCode = 9 + versionName = "1.4.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/AppContainer.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/AppContainer.kt index e67388b..0d2a0a9 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/AppContainer.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/data/AppContainer.kt @@ -10,8 +10,11 @@ import ru.n08i40k.polytechnic.next.data.cache.NetworkCacheRepository import ru.n08i40k.polytechnic.next.data.cache.impl.FakeNetworkCacheRepository import ru.n08i40k.polytechnic.next.data.cache.impl.LocalNetworkCacheRepository import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository +import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleReplacerRepository import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository +import ru.n08i40k.polytechnic.next.data.schedule.impl.RemoteScheduleReplacerRepository import ru.n08i40k.polytechnic.next.data.schedule.impl.RemoteScheduleRepository +import ru.n08i40k.polytechnic.next.data.scheduleReplacer.ScheduleReplacerRepository import ru.n08i40k.polytechnic.next.data.users.ProfileRepository import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository import ru.n08i40k.polytechnic.next.data.users.impl.RemoteProfileRepository @@ -19,33 +22,42 @@ import javax.inject.Singleton interface AppContainer { val applicationContext: Context + val networkCacheRepository: NetworkCacheRepository + val scheduleRepository: ScheduleRepository + + val scheduleReplacerRepository: ScheduleReplacerRepository + val profileRepository: ProfileRepository } class MockAppContainer(override val applicationContext: Context) : AppContainer { - override val networkCacheRepository: NetworkCacheRepository by lazy { FakeNetworkCacheRepository() } - override val scheduleRepository: ScheduleRepository by lazy { FakeScheduleRepository() } - override val profileRepository: ProfileRepository by lazy { FakeProfileRepository() } + override val networkCacheRepository: NetworkCacheRepository + by lazy { FakeNetworkCacheRepository() } + + override val scheduleRepository: ScheduleRepository + by lazy { FakeScheduleRepository() } + + override val scheduleReplacerRepository: ScheduleReplacerRepository + by lazy { FakeScheduleReplacerRepository() } + + override val profileRepository: ProfileRepository + by lazy { FakeProfileRepository() } } class RemoteAppContainer(override val applicationContext: Context) : AppContainer { - override val networkCacheRepository: NetworkCacheRepository by lazy { - LocalNetworkCacheRepository( - applicationContext - ) - } - override val scheduleRepository: ScheduleRepository by lazy { - RemoteScheduleRepository( - applicationContext - ) - } - override val profileRepository: ProfileRepository by lazy { - RemoteProfileRepository( - applicationContext - ) - } + override val networkCacheRepository: NetworkCacheRepository + by lazy { LocalNetworkCacheRepository(applicationContext) } + + override val scheduleRepository: ScheduleRepository + by lazy { RemoteScheduleRepository(applicationContext) } + + override val scheduleReplacerRepository: ScheduleReplacerRepository + by lazy { RemoteScheduleReplacerRepository(applicationContext) } + + override val profileRepository: ProfileRepository + by lazy { RemoteProfileRepository(applicationContext) } } @Module diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/MyResult.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/MyResult.kt index 79c83c9..0876334 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/MyResult.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/data/MyResult.kt @@ -4,4 +4,3 @@ sealed interface MyResult { data class Success(val data: T) : MyResult data class Failure(val exception: Exception) : MyResult } - diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/RemoteScheduleRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/RemoteScheduleRepository.kt index 9d42c3b..e2c2449 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/RemoteScheduleRepository.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/RemoteScheduleRepository.kt @@ -9,8 +9,8 @@ import kotlinx.coroutines.withContext import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository import ru.n08i40k.polytechnic.next.model.Group -import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetRequest -import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetRequestData +import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetReq +import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetReqData import ru.n08i40k.polytechnic.next.network.tryFuture import ru.n08i40k.polytechnic.next.settings.settingsDataStore @@ -27,8 +27,8 @@ class RemoteScheduleRepository(private val context: Context) : ScheduleRepositor return@withContext MyResult.Failure(IllegalArgumentException("No group name provided!")) val response = tryFuture { - ScheduleGetRequest( - ScheduleGetRequestData(groupName), + ScheduleGetReq( + ScheduleGetReqData(groupName), context, it, it diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/ScheduleReplacerRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/ScheduleReplacerRepository.kt new file mode 100644 index 0000000..9d22296 --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/ScheduleReplacerRepository.kt @@ -0,0 +1,16 @@ +package ru.n08i40k.polytechnic.next.data.scheduleReplacer + +import ru.n08i40k.polytechnic.next.data.MyResult +import ru.n08i40k.polytechnic.next.model.ScheduleReplacer + +interface ScheduleReplacerRepository { + suspend fun getAll(): MyResult> + + suspend fun setCurrent( + fileName: String, + fileData: ByteArray, + fileType: String + ): MyResult + + suspend fun clear(): MyResult +} \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/impl/FakeScheduleReplacerRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/impl/FakeScheduleReplacerRepository.kt new file mode 100644 index 0000000..27aa71e --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/impl/FakeScheduleReplacerRepository.kt @@ -0,0 +1,31 @@ +package ru.n08i40k.polytechnic.next.data.schedule.impl + +import ru.n08i40k.polytechnic.next.data.MyResult +import ru.n08i40k.polytechnic.next.data.scheduleReplacer.ScheduleReplacerRepository +import ru.n08i40k.polytechnic.next.model.ScheduleReplacer + +class FakeScheduleReplacerRepository : ScheduleReplacerRepository { + companion object { + @Suppress("SpellCheckingInspection") + val exampleReplacers: List = listOf( + ScheduleReplacer("test-etag", 236 * 1024), + ScheduleReplacer("frgsjkfhg", 623 * 1024), + ) + } + + override suspend fun getAll(): MyResult> { + return MyResult.Success(exampleReplacers) + } + + override suspend fun setCurrent( + fileName: String, + fileData: ByteArray, + fileType: String + ): MyResult { + return MyResult.Success(Unit) + } + + override suspend fun clear(): MyResult { + return MyResult.Success(1) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/impl/RemoteScheduleReplacerRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/impl/RemoteScheduleReplacerRepository.kt new file mode 100644 index 0000000..6a95dd0 --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/impl/RemoteScheduleReplacerRepository.kt @@ -0,0 +1,41 @@ +package ru.n08i40k.polytechnic.next.data.schedule.impl + +import android.content.Context +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import ru.n08i40k.polytechnic.next.data.MyResult +import ru.n08i40k.polytechnic.next.data.scheduleReplacer.ScheduleReplacerRepository +import ru.n08i40k.polytechnic.next.model.ScheduleReplacer +import ru.n08i40k.polytechnic.next.network.data.scheduleReplacer.ScheduleReplacerClearReq +import ru.n08i40k.polytechnic.next.network.data.scheduleReplacer.ScheduleReplacerGetReq +import ru.n08i40k.polytechnic.next.network.data.scheduleReplacer.ScheduleReplacerSetReq +import ru.n08i40k.polytechnic.next.network.tryFuture + +class RemoteScheduleReplacerRepository(private val context: Context) : ScheduleReplacerRepository { + override suspend fun getAll(): MyResult> = + withContext(Dispatchers.IO) { + tryFuture { ScheduleReplacerGetReq(context, it, it) } + } + + + override suspend fun setCurrent( + fileName: String, + fileData: ByteArray, + fileType: String + ): MyResult = + withContext(Dispatchers.IO) { + tryFuture { ScheduleReplacerSetReq(context, fileName, fileData, fileType, it, it) } + } + + override suspend fun clear(): MyResult { + val response = withContext(Dispatchers.IO) { + tryFuture { ScheduleReplacerClearReq(context, it, it) } + } + + return when (response) { + is MyResult.Failure -> response + is MyResult.Success -> MyResult.Success(response.data.count) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/users/impl/RemoteProfileRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/users/impl/RemoteProfileRepository.kt index 53916ec..85108ed 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/users/impl/RemoteProfileRepository.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/data/users/impl/RemoteProfileRepository.kt @@ -10,8 +10,8 @@ import ru.n08i40k.polytechnic.next.network.data.profile.UsersMeRequest import ru.n08i40k.polytechnic.next.network.tryFuture class RemoteProfileRepository(private val context: Context) : ProfileRepository { - override suspend fun getProfile(): MyResult { - return withContext(Dispatchers.IO) { + override suspend fun getProfile(): MyResult = + withContext(Dispatchers.IO) { tryFuture { UsersMeRequest( context, @@ -20,5 +20,4 @@ class RemoteProfileRepository(private val context: Context) : ProfileRepository ) } } - } } \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/model/Day.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/model/Day.kt new file mode 100644 index 0000000..9b32e4a --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/model/Day.kt @@ -0,0 +1,13 @@ +package ru.n08i40k.polytechnic.next.model + +import kotlinx.serialization.Serializable + +@Suppress("unused") +@Serializable +class Day( + val name: String, + val nonNullIndices: ArrayList, + val defaultIndices: ArrayList, + val customIndices: ArrayList, + val lessons: ArrayList +) \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/model/Group.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/model/Group.kt index 225f4c5..71542de 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/model/Group.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/model/Group.kt @@ -1,42 +1,6 @@ -@file:Suppress("unused") - package ru.n08i40k.polytechnic.next.model import kotlinx.serialization.Serializable -import ru.n08i40k.polytechnic.next.utils.EnumAsIntSerializer - -@Serializable -data class LessonTime(val start: Int, val end: Int) - -private class LessonTypeIntSerializer : EnumAsIntSerializer( - "LessonType", - { it.value }, - { v -> LessonType.entries.first { it.value == v } } -) - -@Serializable(with = LessonTypeIntSerializer::class) -enum class LessonType(val value: Int) { - DEFAULT(0), CUSTOM(1) -} - -@Serializable -data class Lesson( - val type: LessonType, - val defaultIndex: Int, - val name: String, - val time: LessonTime?, - val cabinets: ArrayList, - val teacherNames: ArrayList -) - -@Serializable -class Day( - val name: String, - val nonNullIndices: ArrayList, - val defaultIndices: ArrayList, - val customIndices: ArrayList, - val lessons: ArrayList -) @Serializable class Group( diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/model/Lesson.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/model/Lesson.kt new file mode 100644 index 0000000..e942679 --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/model/Lesson.kt @@ -0,0 +1,13 @@ +package ru.n08i40k.polytechnic.next.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Lesson( + val type: LessonType, + val defaultIndex: Int, + val name: String, + val time: LessonTime?, + val cabinets: ArrayList, + val teacherNames: ArrayList +) \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/model/LessonTime.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/model/LessonTime.kt new file mode 100644 index 0000000..0dcb84b --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/model/LessonTime.kt @@ -0,0 +1,6 @@ +package ru.n08i40k.polytechnic.next.model + +import kotlinx.serialization.Serializable + +@Serializable +data class LessonTime(val start: Int, val end: Int) \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/model/LessonType.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/model/LessonType.kt new file mode 100644 index 0000000..9af9e4d --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/model/LessonType.kt @@ -0,0 +1,15 @@ +package ru.n08i40k.polytechnic.next.model + +import kotlinx.serialization.Serializable +import ru.n08i40k.polytechnic.next.utils.EnumAsIntSerializer + +private class LessonTypeIntSerializer : EnumAsIntSerializer( + "LessonType", + { it.value }, + { v -> LessonType.entries.first { it.value == v } } +) + +@Serializable(with = LessonTypeIntSerializer::class) +enum class LessonType(val value: Int) { + DEFAULT(0), CUSTOM(1) +} \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/model/Profile.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/model/Profile.kt index d2b5928..cff0dbf 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/model/Profile.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/model/Profile.kt @@ -1,29 +1,6 @@ package ru.n08i40k.polytechnic.next.model -import androidx.annotation.StringRes -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Face -import androidx.compose.material.icons.filled.Person -import androidx.compose.material.icons.filled.Settings -import androidx.compose.ui.graphics.vector.ImageVector import kotlinx.serialization.Serializable -import ru.n08i40k.polytechnic.next.R -import ru.n08i40k.polytechnic.next.utils.EnumAsStringSerializer - -private class UserRoleStringSerializer : EnumAsStringSerializer( - "UserRole", - { it.value }, - { v -> UserRole.entries.first { it.value == v } } -) - -@Serializable(with = UserRoleStringSerializer::class) -enum class UserRole(val value: String, val icon: ImageVector, @StringRes val stringId: Int) { - STUDENT("STUDENT", Icons.Filled.Face, R.string.role_student), - TEACHER("TEACHER", Icons.Filled.Person, R.string.role_teacher), - ADMIN("ADMIN", Icons.Filled.Settings, R.string.role_admin) -} - -val AcceptableUserRoles = listOf(UserRole.STUDENT, UserRole.TEACHER) @Serializable data class Profile( diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/model/ScheduleReplacer.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/model/ScheduleReplacer.kt new file mode 100644 index 0000000..0ac8f86 --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/model/ScheduleReplacer.kt @@ -0,0 +1,9 @@ +package ru.n08i40k.polytechnic.next.model + +import kotlinx.serialization.Serializable + +@Serializable +data class ScheduleReplacer( + val etag: String, + val size: Int +) \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/model/UserRole.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/model/UserRole.kt new file mode 100644 index 0000000..f25a153 --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/model/UserRole.kt @@ -0,0 +1,29 @@ +package ru.n08i40k.polytechnic.next.model + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Face +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Settings +import androidx.compose.ui.graphics.vector.ImageVector +import kotlinx.serialization.Serializable +import ru.n08i40k.polytechnic.next.R +import ru.n08i40k.polytechnic.next.utils.EnumAsStringSerializer + +private class UserRoleStringSerializer : EnumAsStringSerializer( + "UserRole", + { it.value }, + { v -> UserRole.entries.first { it.value == v } } +) + + +@Serializable(with = UserRoleStringSerializer::class) +enum class UserRole(val value: String, val icon: ImageVector, @StringRes val stringId: Int) { + STUDENT("STUDENT", Icons.Filled.Face, R.string.role_student), + TEACHER("TEACHER", Icons.Filled.Person, R.string.role_teacher), + ADMIN("ADMIN", Icons.Filled.Settings, R.string.role_admin); + + companion object { + val AcceptableUserRoles = listOf(STUDENT, TEACHER) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/AuthorizedMultipartRequest.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/AuthorizedMultipartRequest.kt new file mode 100644 index 0000000..4a907a5 --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/AuthorizedMultipartRequest.kt @@ -0,0 +1,130 @@ +package ru.n08i40k.polytechnic.next.network + +import android.content.Context +import com.android.volley.Response +import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.DataOutputStream +import java.io.IOException +import java.io.UnsupportedEncodingException +import kotlin.math.min + +open class AuthorizedMultipartRequest( + context: Context, + method: Int, + url: String, + listener: Response.Listener, + errorListener: Response.ErrorListener?, + canBeUnauthorized: Boolean = false +) : AuthorizedRequest(context, method, url, listener, errorListener, canBeUnauthorized) { + private val twoHyphens = "--" + private val lineEnd = "\r\n" + private val boundary = "apiclient-" + System.currentTimeMillis() + + protected open val byteData: Map? get() = null + + override fun getBodyContentType(): String { + return "multipart/form-data;boundary=$boundary" + } + + override fun getHeaders(): MutableMap { + val headers = super.getHeaders() + headers["Content-Type"] = bodyContentType + + return headers + } + + override fun getBody(): ByteArray { + val bos = ByteArrayOutputStream() + val dos = DataOutputStream(bos) + + try { + val params = params + if (!params.isNullOrEmpty()) { + textParse(dos, params, paramsEncoding) + } + + val data = byteData + if (!data.isNullOrEmpty()) { + dataParse(dos, data) + } + + dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd) + + return bos.toByteArray() + } catch (e: IOException) { + e.printStackTrace() + } + return ByteArray(0) + } + + @Throws(IOException::class) + private fun textParse( + dataOutputStream: DataOutputStream, params: Map, encoding: String + ) { + try { + for ((key, value) in params) { + buildTextPart(dataOutputStream, key, value) + } + } catch (uee: UnsupportedEncodingException) { + throw RuntimeException("Encoding not supported: $encoding", uee) + } + } + + @Throws(IOException::class) + private fun dataParse(dataOutputStream: DataOutputStream, data: Map) { + for ((key, value) in data) { + buildDataPart(dataOutputStream, value, key) + } + } + + @Throws(IOException::class) + private fun buildTextPart( + dataOutputStream: DataOutputStream, parameterName: String, parameterValue: String + ) { + dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd) + dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"$parameterName\"$lineEnd") + dataOutputStream.writeBytes(lineEnd) + dataOutputStream.writeBytes(parameterValue + lineEnd) + } + + @Throws(IOException::class) + private fun buildDataPart( + dataOutputStream: DataOutputStream, dataFile: DataPart, inputName: String + ) { + dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd) + dataOutputStream.writeBytes( + "Content-Disposition: form-data; name=\"" + inputName + "\"; filename=\"" + dataFile.fileName + "\"" + lineEnd + ) + + if (dataFile.type != null && dataFile.type!!.trim { it <= ' ' } + .isNotEmpty()) dataOutputStream.writeBytes("Content-Type: " + dataFile.type + lineEnd) + + dataOutputStream.writeBytes(lineEnd) + + val fileInputStream = ByteArrayInputStream(dataFile.content) + var bytesAvailable = fileInputStream.available() + + val maxBufferSize = 1024 * 1024 + var bufferSize = min(bytesAvailable.toDouble(), maxBufferSize.toDouble()).toInt() + val buffer = ByteArray(bufferSize) + + var bytesRead = fileInputStream.read(buffer, 0, bufferSize) + + while (bytesRead > 0) { + dataOutputStream.write(buffer, 0, bufferSize) + bytesAvailable = fileInputStream.available() + bufferSize = min(bytesAvailable.toDouble(), maxBufferSize.toDouble()).toInt() + bytesRead = fileInputStream.read(buffer, 0, bufferSize) + } + + dataOutputStream.writeBytes(lineEnd) + } + + inner class DataPart(name: String?, data: ByteArray, mimeType: String? = null) { + var fileName: String? = name + var content: ByteArray = data + var type: String? = mimeType + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/Request.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/NetworkConnection.kt similarity index 53% rename from app/src/main/java/ru/n08i40k/polytechnic/next/network/Request.kt rename to app/src/main/java/ru/n08i40k/polytechnic/next/network/NetworkConnection.kt index f296e89..af2e1a1 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/Request.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/NetworkConnection.kt @@ -4,23 +4,14 @@ import android.annotation.SuppressLint import android.content.Context import com.android.volley.Request import com.android.volley.RequestQueue -import com.android.volley.Response -import com.android.volley.VolleyError import com.android.volley.toolbox.HurlStack -import com.android.volley.toolbox.RequestFuture -import com.android.volley.toolbox.StringRequest import com.android.volley.toolbox.Volley -import ru.n08i40k.polytechnic.next.data.MyResult import java.security.cert.X509Certificate -import java.util.concurrent.ExecutionException -import java.util.concurrent.TimeoutException -import java.util.logging.Logger import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager - class NetworkConnection(ctx: Context) { companion object { @Volatile @@ -65,45 +56,4 @@ class NetworkConnection(ctx: Context) { fun addToRequestQueue(req: Request) { requestQueue.add(req) } -} - -open class RequestBase( - protected val context: Context, - method: Int, - url: String?, - listener: Response.Listener, - errorListener: Response.ErrorListener? -) : StringRequest(method, NetworkValues.API_HOST + url, listener, errorListener) { - open fun send() { - Logger.getLogger("RequestBase").info("Sending request to $url") - NetworkConnection.getInstance(context).addToRequestQueue(this) - } - - override fun getHeaders(): MutableMap { - val headers = mutableMapOf() - headers["Content-Type"] = "application/json; charset=utf-8" - headers["version"] = "1" - - return headers - } -} - -fun tryFuture( - buildRequest: (RequestFuture) -> RequestT -): MyResult { - val future = RequestFuture.newFuture() - buildRequest(future).send() - return tryGet(future) -} - -fun tryGet(future: RequestFuture): MyResult { - return try { - MyResult.Success(future.get()) - } catch (exception: VolleyError) { - MyResult.Failure(exception) - } catch (exception: ExecutionException) { - MyResult.Failure(exception.cause as VolleyError) - } catch (exception: TimeoutException) { - MyResult.Failure(exception) - } } \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/RequestBase.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/RequestBase.kt new file mode 100644 index 0000000..d24a638 --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/RequestBase.kt @@ -0,0 +1,27 @@ +package ru.n08i40k.polytechnic.next.network + +import android.content.Context +import com.android.volley.Response +import com.android.volley.toolbox.StringRequest +import java.util.logging.Logger + +open class RequestBase( + protected val context: Context, + method: Int, + url: String?, + listener: Response.Listener, + errorListener: Response.ErrorListener? +) : StringRequest(method, NetworkValues.API_HOST + url, listener, errorListener) { + open fun send() { + Logger.getLogger("RequestBase").info("Sending request to $url") + NetworkConnection.getInstance(context).addToRequestQueue(this) + } + + override fun getHeaders(): MutableMap { + val headers = mutableMapOf() + headers["Content-Type"] = "application/json; charset=utf-8" + headers["version"] = "1" + + return headers + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/RequestUtils.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/RequestUtils.kt new file mode 100644 index 0000000..27d5fdf --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/RequestUtils.kt @@ -0,0 +1,27 @@ +package ru.n08i40k.polytechnic.next.network + +import com.android.volley.VolleyError +import com.android.volley.toolbox.RequestFuture +import ru.n08i40k.polytechnic.next.data.MyResult +import java.util.concurrent.ExecutionException +import java.util.concurrent.TimeoutException + +fun tryFuture( + buildRequest: (RequestFuture) -> RequestT +): MyResult { + val future = RequestFuture.newFuture() + buildRequest(future).send() + return tryGet(future) +} + +fun tryGet(future: RequestFuture): MyResult { + return try { + MyResult.Success(future.get()) + } catch (exception: VolleyError) { + MyResult.Failure(exception) + } catch (exception: ExecutionException) { + MyResult.Failure(exception.cause as VolleyError) + } catch (exception: TimeoutException) { + MyResult.Failure(exception) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/CachedRequest.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/CachedRequest.kt index 0012fc6..1a60fda 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/CachedRequest.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/CachedRequest.kt @@ -11,10 +11,10 @@ import ru.n08i40k.polytechnic.next.PolytechnicApplication import ru.n08i40k.polytechnic.next.data.AppContainer import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.network.NetworkConnection -import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusRequest -import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusResponse -import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateRequest -import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateRequestData +import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusReq +import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetCacheStatusResData +import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateReq +import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleUpdateReqData import ru.n08i40k.polytechnic.next.network.tryFuture import ru.n08i40k.polytechnic.next.network.tryGet import java.util.logging.Logger @@ -58,14 +58,14 @@ open class CachedRequest( } } - private suspend fun updateMainPage(): MyResult { + private suspend fun updateMainPage(): MyResult { return withContext(Dispatchers.IO) { when (val mainPage = getMainPage()) { is MyResult.Failure -> mainPage is MyResult.Success -> { tryFuture { - ScheduleUpdateRequest( - ScheduleUpdateRequestData(mainPage.data), + ScheduleUpdateReq( + ScheduleUpdateReqData(mainPage.data), context, it, it @@ -83,7 +83,7 @@ open class CachedRequest( logger.info("Getting cache status...") val cacheStatusResult = tryFuture { - ScheduleGetCacheStatusRequest(context, it, it) + ScheduleGetCacheStatusReq(context, it, it) } if (cacheStatusResult is MyResult.Success) { diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/auth/Login.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/auth/Login.kt index c0539bd..ddd1479 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/auth/Login.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/auth/Login.kt @@ -15,7 +15,7 @@ class LoginRequest( context, Method.POST, "auth/sign-in", - Response.Listener { response -> listener.onResponse(Json.decodeFromString(response)) }, + { listener.onResponse(Json.decodeFromString(it)) }, errorListener ) { override fun getBody(): ByteArray { diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/auth/Register.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/auth/Register.kt index a48aaad..d82baf8 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/auth/Register.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/auth/Register.kt @@ -15,7 +15,7 @@ class RegisterRequest( context, Method.POST, "auth/sign-up", - Response.Listener { response -> listener.onResponse(Json.decodeFromString(response)) }, + { listener.onResponse(Json.decodeFromString(it)) }, errorListener ) { override fun getBody(): ByteArray { diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/profile/UsersMe.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/profile/UsersMe.kt index 175beb2..3c4b83c 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/profile/UsersMe.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/profile/UsersMe.kt @@ -11,9 +11,9 @@ class UsersMeRequest( listener: Response.Listener, errorListener: Response.ErrorListener? ) : AuthorizedRequest( - context, Method.GET, "users/me", Response.Listener { response -> - listener.onResponse( - Json.decodeFromString(response) - ) - }, errorListener + context, + Method.GET, + "users/me", + { listener.onResponse(Json.decodeFromString(it)) }, + errorListener ) \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusRequest.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusReq.kt similarity index 51% rename from app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusRequest.kt rename to app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusReq.kt index a6717f1..3b2f592 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusRequest.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusReq.kt @@ -5,12 +5,14 @@ import com.android.volley.Response import kotlinx.serialization.json.Json import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest -class ScheduleGetCacheStatusRequest( +class ScheduleGetCacheStatusReq( context: Context, - listener: Response.Listener, + listener: Response.Listener, errorListener: Response.ErrorListener? = null ) : AuthorizedRequest( - context, Method.GET, "schedule/cache-status", Response.Listener { response -> - listener.onResponse(Json.decodeFromString(response)) - }, errorListener + context, + Method.GET, + "schedule/cache-status", + { listener.onResponse(Json.decodeFromString(it)) }, + errorListener ) \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusResponse.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusResData.kt similarity index 85% rename from app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusResponse.kt rename to app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusResData.kt index 6d8de40..741a44c 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusResponse.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetCacheStatusResData.kt @@ -3,7 +3,7 @@ package ru.n08i40k.polytechnic.next.network.data.schedule import kotlinx.serialization.Serializable @Serializable -data class ScheduleGetCacheStatusResponse( +data class ScheduleGetCacheStatusResData( val cacheUpdateRequired: Boolean, val cacheHash: String, val lastCacheUpdate: Long, diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetGroupNames.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetGroupNamesReq.kt similarity index 50% rename from app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetGroupNames.kt rename to app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetGroupNamesReq.kt index 44c4289..a61ba11 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetGroupNames.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetGroupNamesReq.kt @@ -5,12 +5,14 @@ import com.android.volley.Response import kotlinx.serialization.json.Json import ru.n08i40k.polytechnic.next.network.data.CachedRequest -class ScheduleGetGroupNamesRequest( +class ScheduleGetGroupNamesReq( context: Context, - listener: Response.Listener, + listener: Response.Listener, errorListener: Response.ErrorListener? = null ) : CachedRequest( - context, Method.GET, "schedule/get-group-names", Response.Listener { response -> - listener.onResponse(Json.decodeFromString(response)) - }, errorListener + context, + Method.GET, + "schedule/get-group-names", + { listener.onResponse(Json.decodeFromString(it)) }, + errorListener ) \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetGroupNamesResponseData.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetGroupNamesResData.kt similarity index 76% rename from app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetGroupNamesResponseData.kt rename to app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetGroupNamesResData.kt index 221d698..1ab02f2 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetGroupNamesResponseData.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetGroupNamesResData.kt @@ -3,6 +3,6 @@ package ru.n08i40k.polytechnic.next.network.data.schedule import kotlinx.serialization.Serializable @Serializable -data class ScheduleGetGroupNamesResponseData( +data class ScheduleGetGroupNamesResData( val names: ArrayList, ) \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGet.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetReq.kt similarity index 59% rename from app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGet.kt rename to app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetReq.kt index b24aabc..03937ee 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGet.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetReq.kt @@ -6,15 +6,17 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import ru.n08i40k.polytechnic.next.network.data.CachedRequest -class ScheduleGetRequest( - private val data: ScheduleGetRequestData, +class ScheduleGetReq( + private val data: ScheduleGetReqData, context: Context, - listener: Response.Listener, + listener: Response.Listener, errorListener: Response.ErrorListener? = null ) : CachedRequest( - context, Method.POST, "schedule/get-group", Response.Listener { response -> - listener.onResponse(Json.decodeFromString(response)) - }, errorListener + context, + Method.POST, + "schedule/get-group", + { listener.onResponse(Json.decodeFromString(it)) }, + errorListener ) { override fun getBody(): ByteArray { return Json.encodeToString(data).toByteArray() diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetRequestData.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetReqData.kt similarity index 69% rename from app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetRequestData.kt rename to app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetReqData.kt index 2d29190..44af8f1 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetRequestData.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetReqData.kt @@ -3,4 +3,4 @@ package ru.n08i40k.polytechnic.next.network.data.schedule import kotlinx.serialization.Serializable @Serializable -data class ScheduleGetRequestData(val name: String) \ No newline at end of file +data class ScheduleGetReqData(val name: String) \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetResponse.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetResData.kt similarity index 88% rename from app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetResponse.kt rename to app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetResData.kt index d2885f4..a9ccfe0 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetResponse.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleGetResData.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.Serializable import ru.n08i40k.polytechnic.next.model.Group @Serializable -data class ScheduleGetResponse( +data class ScheduleGetResData( val updatedAt: String, val group: Group, val lastChangedDays: ArrayList, diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleUpdate.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleUpdateReq.kt similarity index 58% rename from app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleUpdate.kt rename to app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleUpdateReq.kt index e4e1942..8e13f7c 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleUpdate.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleUpdateReq.kt @@ -6,15 +6,17 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest -class ScheduleUpdateRequest( - private val data: ScheduleUpdateRequestData, +class ScheduleUpdateReq( + private val data: ScheduleUpdateReqData, context: Context, - listener: Response.Listener, + listener: Response.Listener, errorListener: Response.ErrorListener? = null ) : AuthorizedRequest( - context, Method.POST, "schedule/update-site-main-page", Response.Listener { - listener.onResponse(Json.decodeFromString(it)) - }, errorListener + context, + Method.POST, + "schedule/update-site-main-page", + { listener.onResponse(Json.decodeFromString(it)) }, + errorListener ) { override fun getBody(): ByteArray { return Json.encodeToString(data).toByteArray() diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleUpdateRequestData.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleUpdateReqData.kt similarity index 66% rename from app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleUpdateRequestData.kt rename to app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleUpdateReqData.kt index 092c41d..a703f70 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleUpdateRequestData.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/schedule/ScheduleUpdateReqData.kt @@ -3,4 +3,4 @@ package ru.n08i40k.polytechnic.next.network.data.schedule import kotlinx.serialization.Serializable @Serializable -data class ScheduleUpdateRequestData(val mainPage: String) \ No newline at end of file +data class ScheduleUpdateReqData(val mainPage: String) \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerClearReq.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerClearReq.kt new file mode 100644 index 0000000..23b10d0 --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerClearReq.kt @@ -0,0 +1,18 @@ +package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer + +import android.content.Context +import com.android.volley.Response +import kotlinx.serialization.json.Json +import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest + +class ScheduleReplacerClearReq( + context: Context, + listener: Response.Listener, + errorListener: Response.ErrorListener? +) : AuthorizedRequest( + context, + Method.POST, + "schedule-replacer/clear", + { listener.onResponse(Json.decodeFromString(it)) }, + errorListener +) \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerClearResData.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerClearResData.kt new file mode 100644 index 0000000..1218ed0 --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerClearResData.kt @@ -0,0 +1,8 @@ +package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer + +import kotlinx.serialization.Serializable + +@Serializable +data class ScheduleReplacerClearResData( + val count: Int +) \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerGetReq.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerGetReq.kt new file mode 100644 index 0000000..7ea030e --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerGetReq.kt @@ -0,0 +1,18 @@ +package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer + +import android.content.Context +import com.android.volley.Response +import kotlinx.serialization.json.Json +import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest + +class ScheduleReplacerGetReq( + context: Context, + listener: Response.Listener, + errorListener: Response.ErrorListener? +) : AuthorizedRequest( + context, + Method.GET, + "schedule-replacer/get", + { listener.onResponse(Json.decodeFromString(it)) }, + errorListener +) \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerGetResData.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerGetResData.kt new file mode 100644 index 0000000..799e22d --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerGetResData.kt @@ -0,0 +1,5 @@ +package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer + +import ru.n08i40k.polytechnic.next.model.ScheduleReplacer + +typealias ScheduleReplacerGetResData = List diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerSetReq.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerSetReq.kt new file mode 100644 index 0000000..771f372 --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/data/scheduleReplacer/ScheduleReplacerSetReq.kt @@ -0,0 +1,28 @@ +package ru.n08i40k.polytechnic.next.network.data.scheduleReplacer + +import android.content.Context +import com.android.volley.Response +import ru.n08i40k.polytechnic.next.network.AuthorizedMultipartRequest + +class ScheduleReplacerSetReq( + context: Context, + private val fileName: String, + private val fileData: ByteArray, + private val fileType: String, + private val listener: Response.Listener, + errorListener: Response.ErrorListener? +) : AuthorizedMultipartRequest( + context, + Method.POST, + "schedule-replacer/set", + { listener.onResponse(null) }, + errorListener +) { + override val byteData: Map + get() = mapOf( + Pair( + "file", + DataPart(fileName, fileData, fileType) + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/LoadingContent.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/LoadingContent.kt index a3e9b2a..33cb13e 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/LoadingContent.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/LoadingContent.kt @@ -23,15 +23,15 @@ fun LoadingContent( verticalArrangement: Arrangement.Vertical = Arrangement.Center, content: @Composable () -> Unit ) { - if (empty) emptyContent() - else { - PullToRefreshBox( - isRefreshing = loading, onRefresh = onRefresh - ) { - LazyColumn(Modifier.fillMaxSize(), verticalArrangement = verticalArrangement) { - item { - content() - } + if (empty) { + emptyContent() + return + } + + PullToRefreshBox(isRefreshing = loading, onRefresh = onRefresh) { + LazyColumn(Modifier.fillMaxSize(), verticalArrangement = verticalArrangement) { + item { + content() } } } @@ -44,7 +44,5 @@ fun FullScreenLoading() { modifier = Modifier .fillMaxSize() .wrapContentSize(Alignment.Center) - ) { - CircularProgressIndicator() - } + ) { CircularProgressIndicator() } } \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/AuthScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/AuthScreen.kt index 4f9f7bb..0cea497 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/AuthScreen.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/AuthScreen.kt @@ -57,8 +57,8 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import ru.n08i40k.polytechnic.next.R -import ru.n08i40k.polytechnic.next.model.AcceptableUserRoles import ru.n08i40k.polytechnic.next.model.UserRole +import ru.n08i40k.polytechnic.next.model.UserRole.Companion.AcceptableUserRoles import ru.n08i40k.polytechnic.next.network.data.auth.LoginRequest import ru.n08i40k.polytechnic.next.network.data.auth.LoginRequestData import ru.n08i40k.polytechnic.next.network.data.auth.RegisterRequest @@ -125,27 +125,29 @@ private fun LoginForm( Text(text = stringResource(R.string.not_registered)) } - Button(onClick = { - if (username.length < 4) usernameError = true - if (password.isEmpty()) passwordError = true + Button( + enabled = !mutableIsLoading.value, + onClick = { + if (username.length < 4) usernameError = true + if (password.isEmpty()) passwordError = true - if (usernameError || passwordError) return@Button + if (usernameError || passwordError) return@Button - tryLogin( - username, - password, - mutableUsernameError, - mutablePasswordError, - mutableIsLoading, - context, - snackbarHostState, - scope, - navController - ) + tryLogin( + username, + password, + mutableUsernameError, + mutablePasswordError, + mutableIsLoading, + context, + snackbarHostState, + scope, + navController + ) - mutableIsLoading.value = true - focusManager.clearFocus() - }) { + focusManager.clearFocus() + } + ) { Text( text = stringResource(R.string.login), style = MaterialTheme.typography.bodyLarge @@ -253,9 +255,9 @@ private fun RegisterForm( navController ) - mutableIsLoading.value = true focusManager.clearFocus() - }) { + } + ) { Text( text = stringResource(R.string.register), style = MaterialTheme.typography.bodyLarge @@ -364,9 +366,9 @@ fun tryLogin( ) { var isLoading by mutableIsLoading - LoginRequest(LoginRequestData(username, password), context, { - scope.launch { snackbarHostState.showSnackbar("Cool!") } + isLoading = true + LoginRequest(LoginRequestData(username, password), context, { runBlocking { context.settingsDataStore.updateData { currentSettings -> currentSettings @@ -378,6 +380,8 @@ fun tryLogin( } UsersMeRequest(context, { + scope.launch { snackbarHostState.showSnackbar("Cool!") } + runBlocking { context.settingsDataStore.updateData { currentSettings -> currentSettings @@ -388,7 +392,7 @@ fun tryLogin( } navController.navigate("main") - }, {}).send() + }, null).send() }, { isLoading = false @@ -431,6 +435,8 @@ fun tryRegister( ) { var isLoading by mutableIsLoading + isLoading = true + RegisterRequest( RegisterRequestData( username, diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/Constants.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/Constants.kt index 467caf2..feda080 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/Constants.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/Constants.kt @@ -3,17 +3,22 @@ package ru.n08i40k.polytechnic.next.ui.main import androidx.annotation.StringRes import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Create import androidx.compose.material.icons.filled.DateRange import androidx.compose.ui.graphics.vector.ImageVector import ru.n08i40k.polytechnic.next.R data class BottomNavItem( - @StringRes val label: Int, val icon: ImageVector, val route: String + @StringRes val label: Int, + val icon: ImageVector, + val route: String, + val isAdmin: Boolean = false ) object Constants { val bottomNavItem = listOf( BottomNavItem(R.string.profile, Icons.Filled.AccountCircle, "profile"), + BottomNavItem(R.string.replacer, Icons.Filled.Create, "replacer", true), BottomNavItem(R.string.schedule, Icons.Filled.DateRange, "schedule") ) } \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/MainScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/MainScreen.kt index c1db41e..8ae780d 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/MainScreen.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/MainScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntOffset import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -31,10 +32,14 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking import ru.n08i40k.polytechnic.next.MainViewModel +import ru.n08i40k.polytechnic.next.model.UserRole import ru.n08i40k.polytechnic.next.settings.settingsDataStore import ru.n08i40k.polytechnic.next.ui.main.profile.ProfileScreen +import ru.n08i40k.polytechnic.next.ui.main.replacer.ReplacerScreen import ru.n08i40k.polytechnic.next.ui.main.schedule.ScheduleScreen +import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel +import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerViewModel import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel import ru.n08i40k.polytechnic.next.ui.model.profileViewModel @@ -43,13 +48,14 @@ import ru.n08i40k.polytechnic.next.ui.model.profileViewModel private fun NavHostContainer( navController: NavHostController, padding: PaddingValues, - scheduleViewModel: ScheduleViewModel + scheduleViewModel: ScheduleViewModel, + scheduleReplacerViewModel: ScheduleReplacerViewModel? ) { val context = LocalContext.current NavHost( navController = navController, - startDestination = Constants.bottomNavItem[1].route, + startDestination = "schedule", modifier = Modifier.padding(paddingValues = padding), enterTransition = { slideIn( @@ -76,27 +82,36 @@ private fun NavHostContainer( composable("schedule") { ScheduleScreen(scheduleViewModel) { scheduleViewModel.refreshGroup() } } + + if (scheduleReplacerViewModel != null) { + composable("replacer") { + ReplacerScreen(scheduleReplacerViewModel) { scheduleReplacerViewModel.refresh() } + } + } }) } @Composable -private fun BottomNavBar(navController: NavHostController) { +private fun BottomNavBar(navController: NavHostController, isAdmin: Boolean) { NavigationBar { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route - Constants.bottomNavItem.forEach { navItem -> + Constants.bottomNavItem.forEach { + if (it.isAdmin && !isAdmin) + return@forEach + NavigationBarItem( - selected = navItem.route == currentRoute, - onClick = { if (navItem.route != currentRoute) navController.navigate(navItem.route) }, + selected = it.route == currentRoute, + onClick = { if (it.route != currentRoute) navController.navigate(it.route) }, icon = { Icon( - imageVector = navItem.icon, - contentDescription = stringResource(navItem.label) + imageVector = it.icon, + contentDescription = stringResource(it.label) ) }, - label = { Text(stringResource(navItem.label)) }) + label = { Text(stringResource(it.label)) }) } } } @@ -126,14 +141,23 @@ fun MainScreen( onUnauthorized = { appNavController.navigate("auth") }) ) + val profileUiState by LocalContext.current.profileViewModel!!.uiState.collectAsStateWithLifecycle() + val isAdmin = (profileUiState is ProfileUiState.HasProfile) && + (profileUiState as ProfileUiState.HasProfile).profile.role == UserRole.ADMIN + + val scheduleReplacerViewModel: ScheduleReplacerViewModel? = + if (isAdmin) hiltViewModel(LocalContext.current as ComponentActivity) + else null + val navController = rememberNavController() Scaffold( - bottomBar = { BottomNavBar(navController = navController) } + bottomBar = { BottomNavBar(navController, isAdmin) } ) { paddingValues -> NavHostContainer( navController, paddingValues, - scheduleViewModel + scheduleViewModel, + scheduleReplacerViewModel ) } } \ No newline at end of file diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ChangeGroupDialog.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ChangeGroupDialog.kt index b53c4bb..e0af3d9 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ChangeGroupDialog.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ChangeGroupDialog.kt @@ -37,7 +37,7 @@ import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository import ru.n08i40k.polytechnic.next.model.Profile import ru.n08i40k.polytechnic.next.network.data.profile.ChangeGroupRequest import ru.n08i40k.polytechnic.next.network.data.profile.ChangeGroupRequestData -import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetGroupNamesRequest +import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetGroupNamesReq private enum class ChangeGroupError { NOT_EXISTS @@ -65,7 +65,7 @@ private fun getGroups(context: Context): ArrayList { val groups = remember { arrayListOf(groupPlaceholder) } LaunchedEffect(groups) { - ScheduleGetGroupNamesRequest(context, { + ScheduleGetGroupNamesReq(context, { groups.clear() groups.addAll(it.names) }, { diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/replacer/ReplacerScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/replacer/ReplacerScreen.kt new file mode 100644 index 0000000..d00f03a --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/replacer/ReplacerScreen.kt @@ -0,0 +1,245 @@ +package ru.n08i40k.polytechnic.next.ui.main.replacer + +import android.net.Uri +import android.provider.OpenableColumns +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import ru.n08i40k.polytechnic.next.R +import ru.n08i40k.polytechnic.next.data.MockAppContainer +import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleReplacerRepository +import ru.n08i40k.polytechnic.next.model.ScheduleReplacer +import ru.n08i40k.polytechnic.next.ui.LoadingContent +import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerUiState +import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerViewModel + +@Preview(showBackground = true, showSystemUi = true) +@Composable +fun ReplacerScreen( + scheduleReplacerViewModel: ScheduleReplacerViewModel = ScheduleReplacerViewModel( + MockAppContainer( + LocalContext.current + ) + ), + refresh: () -> Unit = {} +) { + val uiState by scheduleReplacerViewModel.uiState.collectAsStateWithLifecycle() + + var uri by remember { mutableStateOf(null) } + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { + uri = it + } + + UploadFile(scheduleReplacerViewModel, uri) { uri = null } + + LoadingContent( + empty = when (uiState) { + is ScheduleReplacerUiState.NoData -> uiState.isLoading + is ScheduleReplacerUiState.HasData -> false + }, + loading = uiState.isLoading, + onRefresh = refresh, + verticalArrangement = Arrangement.Top, + content = { + when (uiState) { + is ScheduleReplacerUiState.NoData -> { + if (!uiState.isLoading) { + TextButton(onClick = refresh, modifier = Modifier.fillMaxSize()) { + Text(stringResource(R.string.reload), textAlign = TextAlign.Center) + } + } + } + + is ScheduleReplacerUiState.HasData -> { + Column { + Row(modifier = Modifier.fillMaxWidth()) { + ClearButton(Modifier.fillMaxWidth(0.5F)) { + scheduleReplacerViewModel.clear() + } + + SetNewButton(Modifier.fillMaxWidth()) { + launcher.launch(arrayOf("application/vnd.ms-excel")) + } + } + + ReplacerList((uiState as ScheduleReplacerUiState.HasData).replacers) + } + } + } + } + ) +} + +@Composable +fun UploadFile( + scheduleReplacerViewModel: ScheduleReplacerViewModel, + uri: Uri?, + onFinish: () -> Unit +) { + if (uri == null) + return + + val context = LocalContext.current + val contentResolver = context.contentResolver + + // get file name + val query = contentResolver.query(uri, null, null, null, null) + if (query == null) { + onFinish() + return + } + + val fileName = query.use { cursor -> + val nameIdx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor.moveToFirst() + + cursor.getString(nameIdx) + } + + // get file type + val fileType: String? = contentResolver.getType(uri) + if (fileType == null) { + onFinish() + return + } + + // get file data + val inputStream = contentResolver.openInputStream(uri) + if (inputStream == null) { + onFinish() + return + } + + val fileData = inputStream.readBytes() + + inputStream.close() + + scheduleReplacerViewModel.set(fileName, fileData, fileType) + onFinish() +} + +//@Preview(showBackground = true) +//@Composable +//private fun UploadFileDialog( +// opened: Boolean = true, +// onClose: () -> Unit = {} +//) { +// Dialog(onDismissRequest = onClose) { +// Card { +// Button +// } +// } +//} + +@Preview(showBackground = true) +@Composable +private fun SetNewButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}) { + Button(modifier = modifier, onClick = onClick) { + val setReplacerText = stringResource(R.string.set_replacer) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(0.dp, 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Icon(imageVector = Icons.Filled.Add, contentDescription = setReplacerText) + Text(text = setReplacerText) + Icon(imageVector = Icons.Filled.Add, contentDescription = setReplacerText) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ClearButton(modifier: Modifier = Modifier, onClick: () -> Unit = {}) { + Button(modifier = modifier, onClick = onClick) { + val clearReplacersText = stringResource(R.string.clear_replacers) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(0.dp, 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Icon(imageVector = Icons.Filled.Delete, contentDescription = clearReplacersText) + Text(text = clearReplacersText) + Icon(imageVector = Icons.Filled.Delete, contentDescription = clearReplacersText) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ReplacerElement(replacer: ScheduleReplacer = FakeScheduleReplacerRepository.exampleReplacers[0]) { + Column( + modifier = Modifier.border( + BorderStroke( + Dp.Hairline, + MaterialTheme.colorScheme.inverseSurface + ) + ) + ) { + val modifier = Modifier.fillMaxWidth() + + Text(modifier = modifier, textAlign = TextAlign.Center, text = replacer.etag) + Text(modifier = modifier, textAlign = TextAlign.Center, text = buildString { + append(replacer.size) + append(" ") + append(stringResource(R.string.bytes)) + }) + } +} + +@Preview(showBackground = true) +@Composable +fun ReplacerList(replacers: List = FakeScheduleReplacerRepository.exampleReplacers) { + Surface { + LazyColumn( + contentPadding = PaddingValues(0.dp, 5.dp), + modifier = Modifier + .fillMaxWidth() + .height(500.dp) + ) { + items(replacers) { + ReplacerElement(it) + } + } + } +} diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayPager.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayPager.kt index d1a41a3..3e0c0af 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayPager.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayPager.kt @@ -16,7 +16,7 @@ import ru.n08i40k.polytechnic.next.model.Group import java.util.Calendar import kotlin.math.absoluteValue -@Preview(showBackground = true, showSystemUi = true) +@Preview @Composable fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) { val currentDay = (Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 2) diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ScheduleReplacerViewModel.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ScheduleReplacerViewModel.kt new file mode 100644 index 0000000..6ad8245 --- /dev/null +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ScheduleReplacerViewModel.kt @@ -0,0 +1,115 @@ +package ru.n08i40k.polytechnic.next.ui.model + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import ru.n08i40k.polytechnic.next.data.AppContainer +import ru.n08i40k.polytechnic.next.data.MyResult +import ru.n08i40k.polytechnic.next.model.ScheduleReplacer +import javax.inject.Inject + +sealed interface ScheduleReplacerUiState { + val isLoading: Boolean + + data class NoData( + override val isLoading: Boolean, + ) : ScheduleReplacerUiState + + data class HasData( + override val isLoading: Boolean, + val replacers: List, + ) : ScheduleReplacerUiState +} + +private data class ScheduleReplacerViewModelState( + val isLoading: Boolean = false, + val replacers: List? = null, +) { + fun toUiState(): ScheduleReplacerUiState = + if (replacers == null) + ScheduleReplacerUiState.NoData(isLoading) + else + ScheduleReplacerUiState.HasData(isLoading, replacers) +} + +@HiltViewModel +class ScheduleReplacerViewModel @Inject constructor( + appContainer: AppContainer +) : ViewModel() { + private val scheduleReplacerRepository = appContainer.scheduleReplacerRepository + private val viewModelState = MutableStateFlow(ScheduleReplacerViewModelState(isLoading = true)) + + val uiState = viewModelState + .map(ScheduleReplacerViewModelState::toUiState) + .stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState()) + + init { + refresh() + } + + fun refresh() { + setLoading() + + viewModelScope.launch { update() } + } + + fun set( + fileName: String, + fileData: ByteArray, + fileType: String + ) { + setLoading() + + viewModelScope.launch { + val result = scheduleReplacerRepository.setCurrent(fileName, fileData, fileType) + + if (result is MyResult.Success) update() + else setLoading(false) + } + } + + fun clear() { + setLoading() + + viewModelScope.launch { + val result = scheduleReplacerRepository.clear() + + viewModelState.update { + when (result) { + is MyResult.Failure -> it.copy(isLoading = false) + is MyResult.Success -> it.copy(isLoading = false, replacers = emptyList()) + } + } + } + } + + private fun setLoading(loading: Boolean = true) { + viewModelState.update { it.copy(isLoading = loading) } + } + + private suspend fun update() { + val result = scheduleReplacerRepository.getAll() + + viewModelState.update { + when (result) { + is MyResult.Success -> { + it.copy( + replacers = result.data, + isLoading = false + ) + } + + is MyResult.Failure -> it.copy( + replacers = null, + isLoading = false + ) + } + } + } +} diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ScheduleViewModel.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ScheduleViewModel.kt index 7e45e7c..c3e73b4 100644 --- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ScheduleViewModel.kt +++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ScheduleViewModel.kt @@ -1,6 +1,6 @@ package ru.n08i40k.polytechnic.next.ui.model -import androidx.lifecycle.ViewModel +кimport androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -14,7 +14,6 @@ import ru.n08i40k.polytechnic.next.data.AppContainer import ru.n08i40k.polytechnic.next.data.MyResult import ru.n08i40k.polytechnic.next.model.Group import java.util.Date -import java.util.logging.Logger import javax.inject.Inject sealed interface ScheduleUiState { @@ -72,8 +71,6 @@ class ScheduleViewModel @Inject constructor( is MyResult.Success -> { val updateDates = networkCacheRepository.getUpdateDates() - Logger.getLogger("ScheduleViewModel").info("Updating...") - it.copy( group = result.data, updateDates = updateDates, diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index e72358c..1d5faa3 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -36,4 +36,8 @@ Последнее обновление кеша Последнее обновление расписания Дополнительная информация + Заменитель + байт + Удалить всё + Загрузить новое расписание \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9acfb9c..8ed35e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,4 +36,8 @@ Last server cache update Last server schedule update Additional information + Replacer + bytes + Clear + Set new \ No newline at end of file