diff --git a/.idea/.name b/.idea/.name
deleted file mode 100644
index f866a66..0000000
--- a/.idea/.name
+++ /dev/null
@@ -1 +0,0 @@
-PolytecnicNext
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index bce97c3..4bec4ea 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -1,9 +1,5 @@
-
-
-
-
@@ -117,8 +113,5 @@
-
-
-
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
index 79ee123..a55e7a1 100644
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index b86273d..0000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index e21cb14..8f96cb6 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -2,16 +2,8 @@
-
+
-
-
-
-
-
-
-
-
diff --git a/.idea/dictionaries/n08i40k.xml b/.idea/dictionaries/n08i40k.xml
deleted file mode 100644
index 24d5649..0000000
--- a/.idea/dictionaries/n08i40k.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/.idea/discord.xml b/.idea/discord.xml
deleted file mode 100644
index 912db82..0000000
--- a/.idea/discord.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 1dbfe25..18df225 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -1,5 +1,8 @@
+
+
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 5bb575c..b2c751a 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,11 +1,4 @@
-
-
-
-
-
-
-
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index aad2a64..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2024 Nikita
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 414f30a..1b976a0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,11 +1,11 @@
+import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import com.google.protobuf.gradle.id
import com.google.protobuf.gradle.proto
-
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
- alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.kotlin.compose)
kotlin("plugin.serialization") version "2.0.20"
id("kotlin-parcelize")
@@ -18,9 +18,22 @@ plugins {
alias(libs.plugins.google.firebase.crashlytics)
id("com.google.dagger.hilt.android")
+
+ id("vkid.manifest.placeholders") version "1.1.0" apply true
}
+val localProperties = gradleLocalProperties(rootDir, providers)
+
android {
+ signingConfigs {
+ create("release") {
+ storeFile = file(localProperties.getProperty("sign.storeFile"))
+ keyAlias = localProperties.getProperty("sign.keyAlias")
+ storePassword = localProperties.getProperty("sign.storePassword")
+ keyPassword = localProperties.getProperty("sign.keyPassword")
+ }
+ }
+
namespace = "ru.n08i40k.polytechnic.next"
compileSdk = 35
@@ -33,13 +46,10 @@ android {
applicationId = "ru.n08i40k.polytechnic.next"
minSdk = 26
targetSdk = 35
- versionCode = 23
- versionName = "2.3.0"
+ versionCode = 25
+ versionName = "3.0.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- vectorDrawables {
- useSupportLibrary = true
- }
}
buildTypes {
@@ -49,11 +59,14 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
+ signingConfig = signingConfigs.getByName("release")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
+
+ isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = "11"
@@ -64,11 +77,6 @@ android {
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
}
- packaging {
- resources {
- excludes += "/META-INF/{AL2.0,LGPL2.1}"
- }
- }
sourceSets {
getByName("main") {
@@ -90,6 +98,15 @@ android {
}
dependencies {
+ coreLibraryDesugaring(libs.desugar.jdk.libs)
+
+ // vk
+ implementation(libs.vk.vkid)
+ implementation(libs.vk.onetap.compose)
+
+ // internet
+ implementation(libs.volley)
+
// work manager
implementation(libs.androidx.work.runtime)
implementation(libs.androidx.work.runtime.ktx)
@@ -116,6 +133,7 @@ dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.datetime)
+ // default
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
@@ -124,13 +142,10 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
- implementation(libs.volley)
implementation(libs.androidx.navigation.compose)
+ // test
testImplementation(libs.junit)
- testImplementation(libs.mockito.kotlin)
-
-
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
@@ -157,4 +172,4 @@ protobuf {
}
}
}
-}
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 8a0dc3d..9c54e9a 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -18,4 +18,27 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
--renamesourcefileattribute SourceFile
\ No newline at end of file
+-renamesourcefileattribute SourceFile
+
+#noinspection ShrinkerUnresolvedReference
+-keep class com.vk.dto.common.id.UserId { *; }
+-keep class * extends com.vk.id.sample.app.util.carrying.CarryingCallable { *; }
+-keep class android.content.Context { *; }
+
+# OneTapStyle.Companion methods's arguments and return types
+-keep class com.vk.id.onetap.common.OneTapStyle { *; }
+-keep class com.vk.id.onetap.common.OneTapStyle$* { *; }
+-keep class com.vk.id.onetap.common.button.style.OneTapButtonCornersStyle { *; }
+-keep class com.vk.id.onetap.common.button.style.OneTapButtonSizeStyle { *; }
+-keep class com.vk.id.onetap.common.button.style.OneTapButtonElevationStyle { *; }
+# OAuthListWidgetStyle.Companion methods's arguments and return types
+-keep class com.vk.id.multibranding.common.style.OAuthListWidgetStyle { *; }
+-keep class com.vk.id.multibranding.common.style.OAuthListWidgetStyle$* { *; }
+-keep class com.vk.id.multibranding.common.style.OAuthListWidgetCornersStyle { *; }
+-keep class com.vk.id.multibranding.common.style.OAuthListWidgetSizeStyle { *; }
+# OneTapBottomSheetStyle.Companion methods's arguments and return types
+-keep class com.vk.id.onetap.compose.onetap.sheet.style.OneTapBottomSheetStyle { *; }
+-keep class com.vk.id.onetap.compose.onetap.sheet.style.OneTapBottomSheetStyle$* { *; }
+-keep class com.vk.id.onetap.compose.onetap.sheet.style.OneTapSheetCornersStyle { *; }
+-keep class com.vk.id.onetap.common.button.style.OneTapButtonCornersStyle { *; }
+-keep class com.vk.id.onetap.common.button.style.OneTapButtonSizeStyle { *; }
\ No newline at end of file
diff --git a/app/src/androidTest/java/ru/n08i40k/polytechnic/next/ExampleInstrumentedTest.kt b/app/src/androidTest/java/ru/n08i40k/polytechnic/next/ExampleInstrumentedTest.kt
index b4f211d..eefb4f0 100644
--- a/app/src/androidTest/java/ru/n08i40k/polytechnic/next/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/ru/n08i40k/polytechnic/next/ExampleInstrumentedTest.kt
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("ru.n08i40k.polytecnic.next", appContext.packageName)
+ assertEquals("ru.n08i40k.polytechnic.next", appContext.packageName)
}
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 75b9177..457e895 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,48 +2,40 @@
-
+
-
+
-
+
-
-
-
-
+ android:theme="@style/Theme.Polytechnic">
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
deleted file mode 100644
index 9630d62..0000000
Binary files a/app/src/main/ic_launcher-playstore.png and /dev/null differ
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/Application.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/Application.kt
new file mode 100644
index 0000000..d002d27
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/Application.kt
@@ -0,0 +1,89 @@
+package ru.n08i40k.polytechnic.next
+
+import android.app.Application
+import com.google.android.gms.tasks.OnCompleteListener
+import com.google.android.gms.tasks.Task
+import com.google.firebase.messaging.FirebaseMessaging
+import com.google.firebase.remoteconfig.ConfigUpdate
+import com.google.firebase.remoteconfig.ConfigUpdateListener
+import com.google.firebase.remoteconfig.FirebaseRemoteConfigException
+import com.google.firebase.remoteconfig.remoteConfigSettings
+import com.vk.id.VKID
+import dagger.hilt.android.HiltAndroidApp
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
+import ru.n08i40k.polytechnic.next.app.AppContainer
+import ru.n08i40k.polytechnic.next.settings.settings
+import ru.n08i40k.polytechnic.next.utils.Observable
+import ru.n08i40k.polytechnic.next.worker.UpdateFCMTokenWorker
+import ru.n08i40k.polytechnic.next.worker.UpdateLinkWorker
+import java.util.logging.Logger
+import javax.inject.Inject
+
+data class AppEvents(
+ val signOut: Observable = Observable()
+)
+
+@HiltAndroidApp
+class Application : Application() {
+ @Inject
+ lateinit var container: AppContainer
+
+ val events = AppEvents()
+
+ val version
+ get() = applicationContext.packageManager
+ .getPackageInfo(this.packageName, 0)
+ .versionName!!
+// val version
+// get() = "2.0.2"
+
+ private fun scheduleUpdateLinkWorker() {
+ container.remoteConfig.activate().addOnCompleteListener {
+ UpdateLinkWorker.schedule(this@Application)
+ }
+ }
+
+ private fun fixupToken() {
+ if (runBlocking { settings.data.map { it.fcmToken }.first() }.isNotEmpty())
+ return
+
+ FirebaseMessaging.getInstance().token.addOnCompleteListener(object :
+ OnCompleteListener {
+ override fun onComplete(token: Task) {
+ if (!token.isSuccessful)
+ return
+
+ UpdateFCMTokenWorker.schedule(applicationContext, token.result!!)
+ }
+ })
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ VKID.init(this)
+
+ val remoteConfig = container.remoteConfig
+
+ remoteConfig.setConfigSettingsAsync(remoteConfigSettings {
+ minimumFetchIntervalInSeconds = 3600
+ })
+ remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
+
+ remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener {
+ override fun onUpdate(configUpdate: ConfigUpdate) {
+ scheduleUpdateLinkWorker()
+ }
+
+ override fun onError(error: FirebaseRemoteConfigException) {
+ Logger.getLogger("Application")
+ .severe("Failed to fetch RemoteConfig update!")
+ }
+ })
+
+ scheduleUpdateLinkWorker()
+ fixupToken()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/MainActivity.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/MainActivity.kt
new file mode 100644
index 0000000..02354fd
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/MainActivity.kt
@@ -0,0 +1,116 @@
+package ru.n08i40k.polytechnic.next
+
+import android.Manifest
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.safeContent
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.material3.Surface
+import androidx.compose.ui.Modifier
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.lifecycleScope
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import ru.n08i40k.polytechnic.next.app.NotificationChannels
+import ru.n08i40k.polytechnic.next.settings.settings
+import ru.n08i40k.polytechnic.next.ui.PolytechnicApp
+import ru.n08i40k.polytechnic.next.ui.theme.AppTheme
+
+@AndroidEntryPoint
+class MainActivity : ComponentActivity() {
+ private fun createNotificationChannel(
+ notificationManager: NotificationManager,
+ name: String,
+ description: String,
+ channelId: String
+ ) {
+ val channel = NotificationChannel(channelId, name, NotificationManager.IMPORTANCE_DEFAULT)
+ channel.description = description
+
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ private fun createNotificationChannels() {
+ if (!hasNotificationPermission())
+ return
+
+ val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+
+ createNotificationChannel(
+ notificationManager,
+ getString(R.string.schedule_channel_name),
+ getString(R.string.schedule_channel_description),
+ NotificationChannels.SCHEDULE_UPDATE
+ )
+
+ createNotificationChannel(
+ notificationManager,
+ getString(R.string.app_update_channel_name),
+ getString(R.string.app_update_channel_description),
+ NotificationChannels.APP_UPDATE
+ )
+
+// createNotificationChannel(
+// notificationManager,
+// getString(R.string.lesson_view_channel_name),
+// getString(R.string.lesson_view_channel_description),
+// NotificationChannels.LESSON_VIEW
+// )
+ }
+
+ private val requestPermissionLauncher =
+ registerForActivityResult(ActivityResultContracts.RequestPermission()) {
+ if (it) createNotificationChannels()
+ }
+
+ private fun askNotificationPermission() {
+ if (hasNotificationPermission())
+ return
+
+ requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ }
+
+ private fun hasNotificationPermission(): Boolean =
+ (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
+ || ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
+ == PackageManager.PERMISSION_GRANTED)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+
+ askNotificationPermission()
+ createNotificationChannels()
+
+ lifecycleScope.launch {
+ settings.data.first()
+ }
+
+ setContent {
+ AppTheme {
+ Surface {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))
+ ) {
+ PolytechnicApp()
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/MainViewModel.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/MainViewModel.kt
deleted file mode 100644
index 4700123..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/MainViewModel.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package ru.n08i40k.polytechnic.next
-
-import androidx.lifecycle.ViewModel
-import dagger.hilt.android.lifecycle.HiltViewModel
-import ru.n08i40k.polytechnic.next.data.AppContainer
-import javax.inject.Inject
-
-@HiltViewModel
-class MainViewModel @Inject constructor(val appContainer: AppContainer) : ViewModel()
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/PolytechnicApplication.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/PolytechnicApplication.kt
deleted file mode 100644
index 1da63bf..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/PolytechnicApplication.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package ru.n08i40k.polytechnic.next
-
-import android.Manifest
-import android.app.Application
-import android.content.pm.PackageManager
-import android.os.Build
-import androidx.core.content.ContextCompat
-import dagger.hilt.android.HiltAndroidApp
-import ru.n08i40k.polytechnic.next.data.AppContainer
-import javax.inject.Inject
-
-@HiltAndroidApp
-class PolytechnicApplication : Application() {
- @Inject
- lateinit var container: AppContainer
-
- fun getAppVersion(): String {
- return applicationContext.packageManager
- .getPackageInfo(this.packageName, 0)
- .versionName!!
- }
-
- fun hasNotificationPermission(): Boolean {
- return (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU
- || ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
- == PackageManager.PERMISSION_GRANTED)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/app/AppContainer.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/app/AppContainer.kt
new file mode 100644
index 0000000..be3265c
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/app/AppContainer.kt
@@ -0,0 +1,63 @@
+package ru.n08i40k.polytechnic.next.app
+
+import android.app.Application
+import android.content.Context
+import com.google.firebase.Firebase
+import com.google.firebase.remoteconfig.FirebaseRemoteConfig
+import com.google.firebase.remoteconfig.remoteConfig
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import ru.n08i40k.polytechnic.next.repository.cache.NetworkCacheRepository
+import ru.n08i40k.polytechnic.next.repository.cache.impl.LocalNetworkCacheRepository
+import ru.n08i40k.polytechnic.next.repository.cache.impl.MockNetworkCacheRepository
+import ru.n08i40k.polytechnic.next.repository.profile.ProfileRepository
+import ru.n08i40k.polytechnic.next.repository.profile.impl.MockProfileRepository
+import ru.n08i40k.polytechnic.next.repository.profile.impl.RemoteProfileRepository
+import ru.n08i40k.polytechnic.next.repository.schedule.ScheduleRepository
+import ru.n08i40k.polytechnic.next.repository.schedule.impl.MockScheduleRepository
+import ru.n08i40k.polytechnic.next.repository.schedule.impl.RemoteScheduleRepository
+import javax.inject.Singleton
+
+interface AppContainer {
+ val context: Context
+
+ val remoteConfig: FirebaseRemoteConfig
+
+ val profileRepository: ProfileRepository
+ val scheduleRepository: ScheduleRepository
+ val networkCacheRepository: NetworkCacheRepository
+}
+
+abstract class SharedAppContainer(override val context: Context) : AppContainer {
+ override val remoteConfig: FirebaseRemoteConfig by lazy { Firebase.remoteConfig }
+}
+
+@Suppress("unused")
+class MockAppContainer(context: Context) : SharedAppContainer(context) {
+ override val profileRepository by lazy { MockProfileRepository() }
+ override val scheduleRepository by lazy { MockScheduleRepository() }
+ override val networkCacheRepository by lazy { MockNetworkCacheRepository() }
+}
+
+@Suppress("unused")
+class RemoteAppContainer(context: Context) : SharedAppContainer(context) {
+ override val profileRepository by lazy { RemoteProfileRepository(this) }
+ override val scheduleRepository by lazy { RemoteScheduleRepository(this) }
+ override val networkCacheRepository by lazy { LocalNetworkCacheRepository(this) }
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+object AppModule {
+ @Provides
+ @Singleton
+ fun provideAppContainer(application: Application): AppContainer {
+ return RemoteAppContainer(application.applicationContext)
+ }
+}
+
+val Context.appContainer
+ get() =
+ (this.applicationContext as ru.n08i40k.polytechnic.next.Application).container
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/NotificationChannels.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/app/NotificationChannels.kt
similarity index 61%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/NotificationChannels.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/app/NotificationChannels.kt
index 9cf4077..b221a47 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/NotificationChannels.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/app/NotificationChannels.kt
@@ -1,7 +1,6 @@
-package ru.n08i40k.polytechnic.next
+package ru.n08i40k.polytechnic.next.app
object NotificationChannels {
- const val LESSON_VIEW = "lesson-view"
const val SCHEDULE_UPDATE = "schedule-update"
const val APP_UPDATE = "app-update"
}
\ No newline at end of file
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
deleted file mode 100644
index 613b8e1..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/AppContainer.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-package ru.n08i40k.polytechnic.next.data
-
-import android.app.Application
-import android.content.Context
-import com.google.firebase.Firebase
-import com.google.firebase.remoteconfig.FirebaseRemoteConfig
-import com.google.firebase.remoteconfig.remoteConfig
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-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.FakeScheduleRepository
-import ru.n08i40k.polytechnic.next.data.schedule.impl.RemoteScheduleRepository
-import ru.n08i40k.polytechnic.next.data.scheduleReplacer.ScheduleReplacerRepository
-import ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl.FakeScheduleReplacerRepository
-import ru.n08i40k.polytechnic.next.data.scheduleReplacer.impl.RemoteScheduleReplacerRepository
-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
-import javax.inject.Singleton
-
-interface AppContainer {
- val applicationContext: Context
-
- val networkCacheRepository: NetworkCacheRepository
-
- val scheduleRepository: ScheduleRepository
-
- val scheduleReplacerRepository: ScheduleReplacerRepository
-
- val profileRepository: ProfileRepository
-
- val remoteConfig: FirebaseRemoteConfig
-}
-
-class MockAppContainer(override val applicationContext: Context) : AppContainer {
- 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() }
-
- override val remoteConfig: FirebaseRemoteConfig
- by lazy { Firebase.remoteConfig }
-}
-
-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 scheduleReplacerRepository: ScheduleReplacerRepository
- by lazy { RemoteScheduleReplacerRepository(applicationContext) }
-
- override val profileRepository: ProfileRepository
- by lazy { RemoteProfileRepository(applicationContext) }
-
- override val remoteConfig: FirebaseRemoteConfig
- by lazy { Firebase.remoteConfig }
-}
-
-@Module
-@InstallIn(SingletonComponent::class)
-object AppModule {
- @Provides
- @Singleton
- fun provideAppContainer(application: Application): AppContainer {
- return RemoteAppContainer(application.applicationContext)
- }
-}
\ No newline at end of file
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
deleted file mode 100644
index 0876334..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/MyResult.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package ru.n08i40k.polytechnic.next.data
-
-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/scheduleReplacer/ScheduleReplacerRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/ScheduleReplacerRepository.kt
deleted file mode 100644
index 9d22296..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/ScheduleReplacerRepository.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-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
deleted file mode 100644
index 8c083ed..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/impl/FakeScheduleReplacerRepository.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package ru.n08i40k.polytechnic.next.data.scheduleReplacer.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
deleted file mode 100644
index 70cd290..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/scheduleReplacer/impl/RemoteScheduleReplacerRepository.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package ru.n08i40k.polytechnic.next.data.scheduleReplacer.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.request.scheduleReplacer.ScheduleReplacerClear
-import ru.n08i40k.polytechnic.next.network.request.scheduleReplacer.ScheduleReplacerGet
-import ru.n08i40k.polytechnic.next.network.request.scheduleReplacer.ScheduleReplacerSet
-import ru.n08i40k.polytechnic.next.network.tryFuture
-
-class RemoteScheduleReplacerRepository(private val context: Context) : ScheduleReplacerRepository {
- override suspend fun getAll(): MyResult> =
- withContext(Dispatchers.IO) {
- tryFuture { ScheduleReplacerGet(context, it, it) }
- }
-
-
- override suspend fun setCurrent(
- fileName: String,
- fileData: ByteArray,
- fileType: String
- ): MyResult =
- withContext(Dispatchers.IO) {
- tryFuture { ScheduleReplacerSet(context, fileName, fileData, fileType, it, it) }
- }
-
- override suspend fun clear(): MyResult {
- val response = withContext(Dispatchers.IO) {
- tryFuture { ScheduleReplacerClear(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/ProfileRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/users/ProfileRepository.kt
deleted file mode 100644
index 4d08f12..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/users/ProfileRepository.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package ru.n08i40k.polytechnic.next.data.users
-
-import ru.n08i40k.polytechnic.next.data.MyResult
-import ru.n08i40k.polytechnic.next.model.Profile
-
-interface ProfileRepository {
- suspend fun getProfile(): MyResult
-
- suspend fun setFcmToken(token: String): MyResult
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/users/impl/FakeProfileRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/data/users/impl/FakeProfileRepository.kt
deleted file mode 100644
index cf03dfe..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/users/impl/FakeProfileRepository.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package ru.n08i40k.polytechnic.next.data.users.impl
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.withContext
-import ru.n08i40k.polytechnic.next.data.MyResult
-import ru.n08i40k.polytechnic.next.data.users.ProfileRepository
-import ru.n08i40k.polytechnic.next.model.Profile
-import ru.n08i40k.polytechnic.next.model.UserRole
-
-class FakeProfileRepository : ProfileRepository {
- private var counter = 0
-
- companion object {
- val exampleProfile =
- Profile(
- "66db32d24030a07e02d974c5",
- "128735612876",
- "n08i40k",
- "ИС-214/23",
- UserRole.STUDENT
- )
- }
-
- override suspend fun getProfile(): MyResult {
- return withContext(Dispatchers.IO) {
- delay(1500)
-
- if (counter++ % 3 == 0)
- MyResult.Failure(Exception())
- else
- MyResult.Success(exampleProfile)
- }
- }
-
- override suspend fun setFcmToken(token: String): MyResult {
- return MyResult.Success(Unit)
- }
-}
\ 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
deleted file mode 100644
index c3606d3..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/users/impl/RemoteProfileRepository.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package ru.n08i40k.polytechnic.next.data.users.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.users.ProfileRepository
-import ru.n08i40k.polytechnic.next.model.Profile
-import ru.n08i40k.polytechnic.next.network.request.fcm.FcmSetToken
-import ru.n08i40k.polytechnic.next.network.request.profile.ProfileMe
-import ru.n08i40k.polytechnic.next.network.tryFuture
-
-class RemoteProfileRepository(private val context: Context) : ProfileRepository {
- override suspend fun getProfile(): MyResult =
- withContext(Dispatchers.IO) {
- tryFuture { ProfileMe(context, it, it) }
- }
-
- override suspend fun setFcmToken(token: String): MyResult =
- withContext(Dispatchers.IO) {
- tryFuture { FcmSetToken(context, token, it, it) }
- }
-}
\ 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
index b3d5ba2..c227666 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/model/Day.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/model/Day.kt
@@ -26,8 +26,8 @@ data class Day(
val street: String? = null
) : Parcelable {
- constructor(name: String, date: Instant, lessons: List) : this(
- name, date.toEpochMilliseconds(), lessons
+ constructor(name: String, date: Instant, lessons: List, street: String?) : this(
+ name, date.toEpochMilliseconds(), lessons, street
)
val date: Instant
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/model/GroupOrTeacher.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/model/GroupOrTeacher.kt
index afcab34..41f154a 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/model/GroupOrTeacher.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/model/GroupOrTeacher.kt
@@ -3,6 +3,7 @@ package ru.n08i40k.polytechnic.next.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
+import ru.n08i40k.polytechnic.next.utils.dateTime
import java.util.Calendar
@Suppress("MemberVisibilityCanBePrivate")
@@ -14,12 +15,16 @@ data class GroupOrTeacher(
) : Parcelable {
val currentIdx: Int?
get() {
- val currentDay = (Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 2)
+ val currentDay = (Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 2) + 1
- if (currentDay < 0 || currentDay > days.size - 1)
+ val day = days.filter {
+ it.date.dateTime.date.dayOfWeek.value == currentDay
+ }
+
+ if (day.isEmpty())
return null
- return currentDay
+ return days.indexOf(day[0])
}
val current: Day?
@@ -27,6 +32,8 @@ data class GroupOrTeacher(
return days.getOrNull(currentIdx ?: return null)
}
+ // TODO: вернуть
+ @Suppress("unused")
val currentKV: Pair?
get() {
val idx = currentIdx ?: return null
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
index 4defb88..323b531 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/model/Lesson.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/model/Lesson.kt
@@ -18,42 +18,32 @@ data class Lesson(
val group: String? = null,
val subGroups: List
) : Parcelable {
- val duration: Int
- get() {
- val startMinutes = time.start.dayMinutes
- val endMinutes = time.end.dayMinutes
-
- return endMinutes - startMinutes
- }
+ // TODO: вернуть
+ @Suppress("unused")
+ val duration get() = time.end.dayMinutes - time.start.dayMinutes
+ @Suppress("unused")
fun getNameAndCabinetsShort(context: Context): String {
val name =
- if (type == LessonType.BREAK) context.getString(R.string.lesson_break)
+ if (type == LessonType.BREAK) context.getString(
+ if (group == null)
+ R.string.student_break
+ else
+ R.string.teacher_break
+ )
else this.name
- val limitedName = name!! limit 15
+ val shortName = name!! limit 15
+ val cabinetList = subGroups.map { it.cabinet }
- val cabinets = subGroups.map { it.cabinet }
+ if (cabinetList.isEmpty())
+ return shortName
- if (cabinets.isEmpty())
- return limitedName
+ if (cabinetList.size == 1 && cabinetList[0] == "с/з")
+ return "$shortName ${context.getString(R.string.in_gym_lc)}"
- if (cabinets.size == 1 && cabinets[0] == "с/з")
- return buildString {
- append(limitedName)
- append(" ")
- append(context.getString(R.string.in_gym_lc))
- }
-
- return buildString {
- append(limitedName)
- append(" ")
- append(
- context.getString(
- R.string.in_cabinets_short_lc,
- cabinets.joinToString(", ")
- )
- )
- }
+ val cabinets =
+ context.getString(R.string.in_cabinets_short_lc, cabinetList.joinToString(", "))
+ return "$shortName $cabinets"
}
}
\ 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 f34eb01..f9dd0e2 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
@@ -5,8 +5,9 @@ import kotlinx.serialization.Serializable
@Serializable
data class Profile(
val id: String,
- val accessToken: String,
val username: String,
val group: String,
- val role: UserRole
+ val role: UserRole,
+ val accessToken: String? = null,
+ val vkId: Int? = null
)
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/NetworkValues.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/NetworkValues.kt
index c35d2f4..106cf81 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/NetworkValues.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/NetworkValues.kt
@@ -1,5 +1,5 @@
package ru.n08i40k.polytechnic.next.network
object NetworkValues {
- const val API_HOST = "https://polytechnic.n08i40k.ru:5050/api/"
+ const val API_HOST = "https://polytechnic.n08i40k.ru/api/"
}
\ 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
index c4622d2..df16e05 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/RequestBase.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/RequestBase.kt
@@ -6,13 +6,12 @@ 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() {
+ open fun send(context: Context) {
Logger.getLogger("RequestBase").info("Sending request to $url")
NetworkConnection.getInstance(context).addToRequestQueue(this)
}
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
index 6e7cdc8..7290ce1 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/RequestUtils.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/RequestUtils.kt
@@ -1,17 +1,19 @@
package ru.n08i40k.polytechnic.next.network
+import android.content.Context
import com.android.volley.VolleyError
import com.android.volley.toolbox.RequestFuture
-import ru.n08i40k.polytechnic.next.data.MyResult
+import ru.n08i40k.polytechnic.next.utils.MyResult
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeoutException
fun tryFuture(
+ context: Context,
buildRequest: (RequestFuture) -> RequestT
): MyResult {
val future = RequestFuture.newFuture()
- buildRequest(future).send()
+ buildRequest(future).send(context)
return tryGet(future)
}
@@ -31,6 +33,6 @@ fun tryGet(future: RequestFuture): MyResult {
fun unwrapException(exception: Exception): Throwable {
if (exception is ExecutionException && exception.cause != null)
return exception.cause!!
-
+
return exception
}
\ 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/request/AuthorizedMultipartRequest.kt
similarity index 94%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/network/AuthorizedMultipartRequest.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/network/request/AuthorizedMultipartRequest.kt
index 3ac758c..1e1716d 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/AuthorizedMultipartRequest.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/AuthorizedMultipartRequest.kt
@@ -1,8 +1,7 @@
-package ru.n08i40k.polytechnic.next.network
+package ru.n08i40k.polytechnic.next.network.request
-import android.content.Context
import com.android.volley.Response
-import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
+import ru.n08i40k.polytechnic.next.app.AppContainer
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.DataOutputStream
@@ -11,13 +10,13 @@ import java.io.UnsupportedEncodingException
import kotlin.math.min
open class AuthorizedMultipartRequest(
- context: Context,
+ appContainer: AppContainer,
method: Int,
url: String,
listener: Response.Listener,
errorListener: Response.ErrorListener?,
canBeUnauthorized: Boolean = false
-) : AuthorizedRequest(context, method, url, listener, errorListener, canBeUnauthorized) {
+) : AuthorizedRequest(appContainer, method, url, listener, errorListener, canBeUnauthorized) {
private val twoHyphens = "--"
private val lineEnd = "\r\n"
private val boundary = "apiclient-" + System.currentTimeMillis()
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/AuthorizedRequest.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/AuthorizedRequest.kt
index 60ae574..9e086d2 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/AuthorizedRequest.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/AuthorizedRequest.kt
@@ -1,52 +1,72 @@
package ru.n08i40k.polytechnic.next.network.request
-import android.content.Context
import com.android.volley.AuthFailureError
import com.android.volley.Response
+import com.android.volley.VolleyError
+import jakarta.inject.Singleton
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.network.RequestBase
-import ru.n08i40k.polytechnic.next.settings.settingsDataStore
-import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
+import ru.n08i40k.polytechnic.next.settings.settings
open class AuthorizedRequest(
- context: Context,
+ val appContainer: AppContainer,
method: Int,
url: String,
listener: Response.Listener,
errorListener: Response.ErrorListener?,
- private val canBeUnauthorized: Boolean = false
+ private val canBeUnauthorized: Boolean = false,
) : RequestBase(
- context,
method,
url,
listener,
- Response.ErrorListener {
- if (!canBeUnauthorized && it is AuthFailureError) {
- runBlocking {
- context.settingsDataStore.updateData { currentSettings ->
- currentSettings.toBuilder().setUserId("")
- .setAccessToken("").build()
- }
- }
- if (context.profileViewModel != null)
- context.profileViewModel!!.onUnauthorized()
- }
+ @Singleton
+ object : Response.ErrorListener {
+ override fun onErrorResponse(error: VolleyError?) {
+ val context = appContainer.context
- errorListener?.onErrorResponse(it)
+ if (!canBeUnauthorized && error is AuthFailureError) {
+ runBlocking {
+ context.settings.updateData { currentSettings ->
+ currentSettings
+ .toBuilder()
+ .clear()
+ .build()
+ }
+ }
+
+ // TODO: если не авторизован
+// if (context.profileViewModel != null)
+// context.profileViewModel!!.onUnauthorized()
+ }
+
+ runBlocking { appContainer.profileRepository.signOut() }
+
+ errorListener?.onErrorResponse(error)
+
+ }
}) {
+
override fun getHeaders(): MutableMap {
val accessToken = runBlocking {
- context.settingsDataStore.data.map { settings -> settings.accessToken }.first()
+ appContainer.context
+ .settings
+ .data
+ .map { settings -> settings.accessToken }
+ .first()
}
- if (accessToken.isEmpty() && context.profileViewModel != null)
- context.profileViewModel!!.onUnauthorized()
+ // TODO: если не авторизован
+// if (accessToken.isEmpty() && context.profileViewModel != null)
+// context.profileViewModel!!.onUnauthorized()
val headers = super.getHeaders()
headers["Authorization"] = "Bearer $accessToken"
return headers
}
+
+ val appContext get() = appContainer.context
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/CachedRequest.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/CachedRequest.kt
index e17d45c..4f234de 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/CachedRequest.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/CachedRequest.kt
@@ -7,31 +7,36 @@ import com.android.volley.toolbox.StringRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
-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.Application
+import ru.n08i40k.polytechnic.next.app.AppContainer
+import ru.n08i40k.polytechnic.next.app.appContainer
import ru.n08i40k.polytechnic.next.network.NetworkConnection
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetCacheStatus
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleUpdate
import ru.n08i40k.polytechnic.next.network.tryFuture
import ru.n08i40k.polytechnic.next.network.tryGet
+import ru.n08i40k.polytechnic.next.utils.MyResult
import java.util.logging.Logger
import java.util.regex.Pattern
open class CachedRequest(
- context: Context,
+ appContainer: AppContainer,
method: Int,
private val url: String,
private val listener: Response.Listener,
errorListener: Response.ErrorListener?,
-) : AuthorizedRequest(context, method, url, {
- runBlocking(Dispatchers.IO) {
- (context as PolytechnicApplication)
- .container.networkCacheRepository.put(url, it)
- }
- listener.onResponse(it)
-}, errorListener) {
- private val appContainer: AppContainer = (context as PolytechnicApplication).container
+) : AuthorizedRequest(
+ appContainer,
+ method,
+ url,
+ {
+ runBlocking(Dispatchers.IO) {
+ appContainer.networkCacheRepository.put(url, it)
+ }
+ listener.onResponse(it)
+ },
+ errorListener
+) {
private suspend fun getXlsUrl(): MyResult = withContext(Dispatchers.IO) {
val mainPageFuture = RequestFuture.newFuture()
@@ -41,7 +46,7 @@ open class CachedRequest(
mainPageFuture,
mainPageFuture
)
- NetworkConnection.getInstance(context).addToRequestQueue(request)
+ NetworkConnection.getInstance(appContext).addToRequestQueue(request)
val response = tryGet(mainPageFuture)
if (response is MyResult.Failure)
@@ -49,7 +54,8 @@ open class CachedRequest(
val pageData = (response as MyResult.Success).data
- val remoteConfig = (context.applicationContext as PolytechnicApplication).container.remoteConfig
+ val remoteConfig =
+ (appContext.applicationContext as Application).container.remoteConfig
val pattern: Pattern =
Pattern.compile(remoteConfig.getString("linkParserRegex"), Pattern.MULTILINE)
@@ -67,10 +73,10 @@ open class CachedRequest(
when (val xlsUrl = getXlsUrl()) {
is MyResult.Failure -> xlsUrl
is MyResult.Success -> {
- tryFuture {
+ tryFuture(appContext) { it ->
ScheduleUpdate(
+ appContext.appContainer,
ScheduleUpdate.RequestDto(xlsUrl.data),
- context,
it,
it
)
@@ -80,23 +86,24 @@ open class CachedRequest(
}
}
- override fun send() {
+ override fun send(context: Context) {
+ // TODO: network cache
val logger = Logger.getLogger("CachedRequest")
- val repository = appContainer.networkCacheRepository
+ val cache = appContainer.networkCacheRepository
- val cacheStatusResult = tryFuture {
- ScheduleGetCacheStatus(context, it, it)
+ val cacheStatusResult = tryFuture(context) {
+ ScheduleGetCacheStatus(appContainer, it, it)
}
if (cacheStatusResult is MyResult.Success) {
val cacheStatus = cacheStatusResult.data
runBlocking {
- repository.setUpdateDates(
+ cache.setUpdateDates(
cacheStatus.lastCacheUpdate,
cacheStatus.lastScheduleUpdate
)
- repository.setHash(cacheStatus.cacheHash)
+ cache.setHash(cacheStatus.cacheHash)
}
if (cacheStatus.cacheUpdateRequired) {
@@ -105,11 +112,11 @@ open class CachedRequest(
when (updateResult) {
is MyResult.Success -> {
runBlocking {
- repository.setUpdateDates(
+ cache.setUpdateDates(
updateResult.data.lastCacheUpdate,
updateResult.data.lastScheduleUpdate
)
- repository.setHash(updateResult.data.cacheHash)
+ cache.setHash(updateResult.data.cacheHash)
}
}
@@ -122,12 +129,12 @@ open class CachedRequest(
logger.warning("Failed to get cache status!")
}
- val cachedResponse = runBlocking { repository.get(url) }
+ val cachedResponse = runBlocking { cache.get(url) }
if (cachedResponse != null) {
listener.onResponse(cachedResponse.data)
return
}
- super.send()
+ super.send(context)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthChangePassword.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthChangePassword.kt
index 73029fd..e7efe97 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthChangePassword.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthChangePassword.kt
@@ -1,19 +1,19 @@
package ru.n08i40k.polytechnic.next.network.request.auth
-import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
class AuthChangePassword(
+ appContainer: AppContainer,
private val data: RequestDto,
- context: Context,
listener: Response.Listener,
errorListener: Response.ErrorListener?
) : AuthorizedRequest(
- context,
+ appContainer,
Method.POST,
"v1/auth/change-password",
{ listener.onResponse(null) },
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthSignIn.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthSignIn.kt
index 3469e30..a298991 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthSignIn.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthSignIn.kt
@@ -1,25 +1,45 @@
package ru.n08i40k.polytechnic.next.network.request.auth
-import android.content.Context
import com.android.volley.Response
+import com.android.volley.VolleyError
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.network.RequestBase
+import ru.n08i40k.polytechnic.next.utils.EnumAsStringSerializer
class AuthSignIn(
private val data: RequestDto,
- context: Context,
listener: Response.Listener,
errorListener: Response.ErrorListener?
) : RequestBase(
- context,
Method.POST,
- "v2/auth/sign-in",
+ "v1/auth/sign-in",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
+ companion object {
+ private class ErrorCodeSerializer : EnumAsStringSerializer(
+ "SignInErrorCode",
+ { it.value },
+ { v -> ErrorCode.entries.first { it.value == v } }
+ )
+
+ @Serializable(with = ErrorCodeSerializer::class)
+ enum class ErrorCode(val value: String) {
+ INCORRECT_CREDENTIALS("INCORRECT_CREDENTIALS"),
+ INVALID_VK_ACCESS_TOKEN("INVALID_VK_ACCESS_TOKEN"),
+ }
+
+ @Serializable
+ data class Error(val code: ErrorCode)
+
+ fun parseError(error: VolleyError): Error {
+ return Json.decodeFromString(error.networkResponse.data.decodeToString())
+ }
+ }
+
@Serializable
data class RequestDto(val username: String, val password: String)
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthSignInVK.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthSignInVK.kt
new file mode 100644
index 0000000..ef0bcf9
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthSignInVK.kt
@@ -0,0 +1,26 @@
+package ru.n08i40k.polytechnic.next.network.request.auth
+
+import com.android.volley.Response
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import ru.n08i40k.polytechnic.next.model.Profile
+import ru.n08i40k.polytechnic.next.network.RequestBase
+
+class AuthSignInVK(
+ private val data: RequestDto,
+ listener: Response.Listener,
+ errorListener: Response.ErrorListener?
+) : RequestBase(
+ Method.POST,
+ "v1/auth/sign-in-vk",
+ { listener.onResponse(Json.decodeFromString(it)) },
+ errorListener
+) {
+ @Serializable
+ data class RequestDto(val accessToken: String)
+
+ override fun getBody(): ByteArray {
+ return Json.encodeToString(data).toByteArray()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthSignUp.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthSignUp.kt
index edc3382..29191c9 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthSignUp.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthSignUp.kt
@@ -1,32 +1,56 @@
package ru.n08i40k.polytechnic.next.network.request.auth
-import android.content.Context
import com.android.volley.Response
+import com.android.volley.VolleyError
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.model.UserRole
import ru.n08i40k.polytechnic.next.network.RequestBase
+import ru.n08i40k.polytechnic.next.utils.EnumAsStringSerializer
class AuthSignUp(
private val data: RequestDto,
- context: Context,
listener: Response.Listener,
errorListener: Response.ErrorListener?
) : RequestBase(
- context,
Method.POST,
- "v2/auth/sign-up",
+ "v1/auth/sign-up",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
+ companion object {
+ private class ErrorCodeSerializer : EnumAsStringSerializer(
+ "SignInErrorCode",
+ { it.value },
+ { v -> ErrorCode.entries.first { it.value == v } }
+ )
+
+ @Serializable(with = ErrorCodeSerializer::class)
+ enum class ErrorCode(val value: String) {
+ USERNAME_ALREADY_EXISTS("USERNAME_ALREADY_EXISTS"),
+ VK_ALREADY_EXISTS("VK_ALREADY_EXISTS"),
+ INVALID_VK_ACCESS_TOKEN("INVALID_VK_ACCESS_TOKEN"),
+ INVALID_GROUP_NAME("INVALID_GROUP_NAME"),
+ DISALLOWED_ROLE("DISALLOWED_ROLE"),
+ }
+
+ @Serializable
+ data class Error(val code: ErrorCode)
+
+ fun parseError(error: VolleyError): Error {
+ return Json.decodeFromString(error.networkResponse.data.decodeToString())
+ }
+ }
+
@Serializable
data class RequestDto(
val username: String,
val password: String,
val group: String,
- val role: UserRole
+ val role: UserRole,
+ val version: String
)
override fun getBody(): ByteArray {
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthSignUpVK.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthSignUpVK.kt
new file mode 100644
index 0000000..e23f6a4
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/auth/AuthSignUpVK.kt
@@ -0,0 +1,33 @@
+package ru.n08i40k.polytechnic.next.network.request.auth
+
+import com.android.volley.Response
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import ru.n08i40k.polytechnic.next.model.Profile
+import ru.n08i40k.polytechnic.next.model.UserRole
+import ru.n08i40k.polytechnic.next.network.RequestBase
+
+class AuthSignUpVK(
+ private val data: RequestDto,
+ listener: Response.Listener,
+ errorListener: Response.ErrorListener?
+) : RequestBase(
+ Method.POST,
+ "v1/auth/sign-up-vk",
+ { listener.onResponse(Json.decodeFromString(it)) },
+ errorListener
+) {
+ @Serializable
+ data class RequestDto(
+ val accessToken: String,
+ val username: String,
+ val group: String,
+ val role: UserRole,
+ val version: String
+ )
+
+ override fun getBody(): ByteArray {
+ return Json.encodeToString(data).toByteArray()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/fcm/FcmSetToken.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/fcm/FcmSetToken.kt
index 5b5bfac..eba6176 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/fcm/FcmSetToken.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/fcm/FcmSetToken.kt
@@ -1,18 +1,26 @@
package ru.n08i40k.polytechnic.next.network.request.fcm
-import android.content.Context
import com.android.volley.Response
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
class FcmSetToken(
- context: Context,
+ appContainer: AppContainer,
token: String,
listener: Response.Listener,
errorListener: Response.ErrorListener?,
) : AuthorizedRequest(
- context, Method.POST,
- "v1/fcm/set-token/$token",
+ appContainer,
+ Method.PATCH,
+ "v1/fcm/set-token?token=$token",
{ listener.onResponse(Unit) },
errorListener,
true
-)
\ No newline at end of file
+) {
+ override fun getHeaders(): MutableMap {
+ val headers = super.getHeaders()
+ headers.remove("Content-Type")
+
+ return headers
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/fcm/FcmUpdateCallback.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/fcm/FcmUpdateCallback.kt
index 9d11699..a82dd02 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/fcm/FcmUpdateCallback.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/fcm/FcmUpdateCallback.kt
@@ -1,16 +1,19 @@
package ru.n08i40k.polytechnic.next.network.request.fcm
-import android.content.Context
import com.android.volley.Response
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
+// TODO: вернуть
+@Suppress("unused")
class FcmUpdateCallback(
- context: Context,
+ appContainer: AppContainer,
version: String,
listener: Response.Listener,
errorListener: Response.ErrorListener?,
) : AuthorizedRequest(
- context, Method.POST,
+ appContainer,
+ Method.POST,
"v1/fcm/update-callback/$version",
{ listener.onResponse(Unit) },
errorListener,
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/profile/ProfileChangeGroup.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/profile/ProfileChangeGroup.kt
index 851a49c..ba53cac 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/profile/ProfileChangeGroup.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/profile/ProfileChangeGroup.kt
@@ -1,19 +1,19 @@
package ru.n08i40k.polytechnic.next.network.request.profile
-import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
class ProfileChangeGroup(
+ appContainer: AppContainer,
private val data: RequestDto,
- context: Context,
listener: Response.Listener,
errorListener: Response.ErrorListener?
) : AuthorizedRequest(
- context,
+ appContainer,
Method.POST,
"v1/users/change-group",
{ listener.onResponse(null) },
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/profile/ProfileChangeUsername.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/profile/ProfileChangeUsername.kt
index 3cd81a0..5a545a4 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/profile/ProfileChangeUsername.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/profile/ProfileChangeUsername.kt
@@ -1,19 +1,19 @@
package ru.n08i40k.polytechnic.next.network.request.profile
-import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
class ProfileChangeUsername(
+ appContainer: AppContainer,
private val data: RequestDto,
- context: Context,
listener: Response.Listener,
errorListener: Response.ErrorListener?
) : AuthorizedRequest(
- context,
+ appContainer,
Method.POST,
"v1/users/change-username",
{ listener.onResponse(null) },
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/profile/ProfileMe.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/profile/ProfileMe.kt
index 03ce8d4..f5955bb 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/profile/ProfileMe.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/profile/ProfileMe.kt
@@ -1,19 +1,19 @@
package ru.n08i40k.polytechnic.next.network.request.profile
-import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.json.Json
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
class ProfileMe(
- context: Context,
+ appContainer: AppContainer,
listener: Response.Listener,
errorListener: Response.ErrorListener?
) : AuthorizedRequest(
- context,
+ appContainer,
Method.GET,
- "v2/users/me",
+ "v1/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/request/schedule/ScheduleGet.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGet.kt
index 44b51ee..fcccfb6 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGet.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGet.kt
@@ -1,20 +1,20 @@
package ru.n08i40k.polytechnic.next.network.request.schedule
-import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
class ScheduleGet(
- context: Context,
+ appContainer: AppContainer,
listener: Response.Listener,
errorListener: Response.ErrorListener? = null
) : CachedRequest(
- context,
+ appContainer,
Method.GET,
- "v4/schedule/group",
+ "v1/schedule/group",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetCacheStatus.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetCacheStatus.kt
index a15db8e..1b6edf8 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetCacheStatus.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetCacheStatus.kt
@@ -1,19 +1,19 @@
package ru.n08i40k.polytechnic.next.network.request.schedule
-import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
class ScheduleGetCacheStatus(
- context: Context,
+ appContainer: AppContainer,
listener: Response.Listener,
errorListener: Response.ErrorListener? = null
) : AuthorizedRequest(
- context,
+ appContainer,
Method.GET,
- "v2/schedule/cache-status",
+ "v1/schedule/cache-status",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetGroupNames.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetGroupNames.kt
index e4b28f7..0875c92 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetGroupNames.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetGroupNames.kt
@@ -1,19 +1,16 @@
package ru.n08i40k.polytechnic.next.network.request.schedule
-import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.RequestBase
class ScheduleGetGroupNames(
- context: Context,
listener: Response.Listener,
errorListener: Response.ErrorListener? = null
) : RequestBase(
- context,
Method.GET,
- "v2/schedule/group-names",
+ "v1/schedule/group-names",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetTeacher.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetTeacher.kt
index d9103d2..ffa84c1 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetTeacher.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetTeacher.kt
@@ -1,21 +1,21 @@
package ru.n08i40k.polytechnic.next.network.request.schedule
-import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
class ScheduleGetTeacher(
- context: Context,
+ appContainer: AppContainer,
teacher: String,
listener: Response.Listener,
errorListener: Response.ErrorListener? = null
) : CachedRequest(
- context,
+ appContainer,
Method.GET,
- "v3/schedule/teacher/$teacher",
+ "v1/schedule/teacher/$teacher",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetTeacherNames.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetTeacherNames.kt
index 5622311..70bc41c 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetTeacherNames.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleGetTeacherNames.kt
@@ -1,19 +1,16 @@
package ru.n08i40k.polytechnic.next.network.request.schedule
-import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import ru.n08i40k.polytechnic.next.network.RequestBase
class ScheduleGetTeacherNames(
- context: Context,
listener: Response.Listener,
errorListener: Response.ErrorListener? = null
) : RequestBase(
- context,
Method.GET,
- "v2/schedule/teacher-names",
+ "v1/schedule/teacher-names",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleUpdate.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleUpdate.kt
index 9fcefa3..b6d61c8 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleUpdate.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/schedule/ScheduleUpdate.kt
@@ -1,21 +1,21 @@
package ru.n08i40k.polytechnic.next.network.request.schedule
-import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
class ScheduleUpdate(
+ appContainer: AppContainer,
private val data: RequestDto,
- context: Context,
listener: Response.Listener,
errorListener: Response.ErrorListener? = null
) : AuthorizedRequest(
- context,
+ appContainer,
Method.PATCH,
- "v4/schedule/update-download-url",
+ "v1/schedule/update-download-url",
{ listener.onResponse(Json.decodeFromString(it)) },
errorListener
) {
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/scheduleReplacer/ScheduleReplacerClear.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/scheduleReplacer/ScheduleReplacerClear.kt
index 26a8d9f..867dbe3 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/scheduleReplacer/ScheduleReplacerClear.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/scheduleReplacer/ScheduleReplacerClear.kt
@@ -1,17 +1,19 @@
package ru.n08i40k.polytechnic.next.network.request.scheduleReplacer
-import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
+// TODO: вернуть
+@Suppress("unused")
class ScheduleReplacerClear(
- context: Context,
+ appContainer: AppContainer,
listener: Response.Listener,
errorListener: Response.ErrorListener?
) : AuthorizedRequest(
- context,
+ appContainer,
Method.POST,
"v1/schedule-replacer/clear",
{ listener.onResponse(Json.decodeFromString(it)) },
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/scheduleReplacer/ScheduleReplacerGet.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/scheduleReplacer/ScheduleReplacerGet.kt
index 1180068..fc22018 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/scheduleReplacer/ScheduleReplacerGet.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/scheduleReplacer/ScheduleReplacerGet.kt
@@ -1,17 +1,19 @@
package ru.n08i40k.polytechnic.next.network.request.scheduleReplacer
-import android.content.Context
import com.android.volley.Response
import kotlinx.serialization.json.Json
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
+// TODO: вернуть
+@Suppress("unused")
class ScheduleReplacerGet(
- context: Context,
+ appContainer: AppContainer,
listener: Response.Listener>,
errorListener: Response.ErrorListener?
) : AuthorizedRequest(
- context,
+ appContainer,
Method.GET,
"v1/schedule-replacer/get",
{ listener.onResponse(Json.decodeFromString(it)) },
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/scheduleReplacer/ScheduleReplacerSet.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/scheduleReplacer/ScheduleReplacerSet.kt
index e3216a3..b5cd140 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/scheduleReplacer/ScheduleReplacerSet.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/scheduleReplacer/ScheduleReplacerSet.kt
@@ -1,18 +1,20 @@
package ru.n08i40k.polytechnic.next.network.request.scheduleReplacer
-import android.content.Context
import com.android.volley.Response
-import ru.n08i40k.polytechnic.next.network.AuthorizedMultipartRequest
+import ru.n08i40k.polytechnic.next.app.AppContainer
+import ru.n08i40k.polytechnic.next.network.request.AuthorizedMultipartRequest
+// TODO: вернуть
+@Suppress("unused")
class ScheduleReplacerSet(
- context: Context,
+ appContainer: AppContainer,
private val fileName: String,
private val fileData: ByteArray,
private val fileType: String,
private val listener: Response.Listener,
errorListener: Response.ErrorListener?
) : AuthorizedMultipartRequest(
- context,
+ appContainer,
Method.POST,
"v1/schedule-replacer/set",
{ listener.onResponse(null) },
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/vkid/VKIDOAuth.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/vkid/VKIDOAuth.kt
new file mode 100644
index 0000000..7568a09
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/network/request/vkid/VKIDOAuth.kt
@@ -0,0 +1,35 @@
+package ru.n08i40k.polytechnic.next.network.request.vkid
+
+import com.android.volley.Request.Method
+import com.android.volley.Response
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+import ru.n08i40k.polytechnic.next.network.RequestBase
+
+class VKIDOAuth(
+ private val data: RequestDto,
+ listener: Response.Listener,
+ errorListener: Response.ErrorListener?,
+) : RequestBase(
+ Method.POST,
+ "v1/vkid/oauth",
+ { listener.onResponse(Json.decodeFromString(it)) },
+ errorListener
+) {
+ @Serializable
+ data class RequestDto(
+ val code: String,
+ val codeVerifier: String,
+ val deviceId: String,
+ )
+
+ @Serializable
+ data class ResponseDto(
+ val accessToken: String,
+ )
+
+ override fun getBody(): ByteArray {
+ return Json.encodeToString(data).toByteArray()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/NetworkCacheRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/cache/NetworkCacheRepository.kt
similarity index 89%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/NetworkCacheRepository.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/repository/cache/NetworkCacheRepository.kt
index 1762a40..66a6ff1 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/NetworkCacheRepository.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/cache/NetworkCacheRepository.kt
@@ -1,4 +1,4 @@
-package ru.n08i40k.polytechnic.next.data.cache
+package ru.n08i40k.polytechnic.next.repository.cache
import ru.n08i40k.polytechnic.next.CachedResponse
import ru.n08i40k.polytechnic.next.UpdateDates
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/impl/LocalNetworkCacheRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/cache/impl/LocalNetworkCacheRepository.kt
similarity index 81%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/impl/LocalNetworkCacheRepository.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/repository/cache/impl/LocalNetworkCacheRepository.kt
index 586282b..05b79ef 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/impl/LocalNetworkCacheRepository.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/cache/impl/LocalNetworkCacheRepository.kt
@@ -1,6 +1,5 @@
-package ru.n08i40k.polytechnic.next.data.cache.impl
+package ru.n08i40k.polytechnic.next.repository.cache.impl
-import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@@ -8,23 +7,26 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import ru.n08i40k.polytechnic.next.CachedResponse
import ru.n08i40k.polytechnic.next.UpdateDates
-import ru.n08i40k.polytechnic.next.data.cache.NetworkCacheRepository
-import ru.n08i40k.polytechnic.next.settings.settingsDataStore
+import ru.n08i40k.polytechnic.next.app.AppContainer
+import ru.n08i40k.polytechnic.next.repository.cache.NetworkCacheRepository
+import ru.n08i40k.polytechnic.next.settings.settings
import javax.inject.Inject
class LocalNetworkCacheRepository
-@Inject constructor(private val applicationContext: Context) : NetworkCacheRepository {
+@Inject constructor(private val appContainer: AppContainer) : NetworkCacheRepository {
private val cacheMap: MutableMap = mutableMapOf()
private var updateDates: UpdateDates = UpdateDates.newBuilder().build()
private var hash: String? = null
+ private val context get() = appContainer.context
+
init {
cacheMap.clear()
runBlocking {
cacheMap.putAll(
- applicationContext
- .settingsDataStore
+ context
+ .settings
.data
.map { settings -> settings.cacheStorageMap }.first()
)
@@ -32,7 +34,7 @@ class LocalNetworkCacheRepository
}
override suspend fun get(url: String): CachedResponse? {
- // Если кешированого ответа нет, то возвращаем null
+ // Если кешированного ответа нет, то возвращаем null
// Если хеши не совпадают и локальный хеш присутствует, то возвращаем null
val response = cacheMap[url] ?: return null
@@ -92,7 +94,7 @@ class LocalNetworkCacheRepository
.setSchedule(schedule).build()
withContext(Dispatchers.IO) {
- applicationContext.settingsDataStore.updateData {
+ context.settings.updateData {
it
.toBuilder()
.setUpdateDates(updateDates)
@@ -104,7 +106,7 @@ class LocalNetworkCacheRepository
private suspend fun save() {
withContext(Dispatchers.IO) {
- applicationContext.settingsDataStore.updateData {
+ context.settings.updateData {
it
.toBuilder()
.putAllCacheStorage(cacheMap)
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/impl/FakeNetworkCacheRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/cache/impl/MockNetworkCacheRepository.kt
similarity index 75%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/impl/FakeNetworkCacheRepository.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/repository/cache/impl/MockNetworkCacheRepository.kt
index ef8bbe7..188dd53 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/cache/impl/FakeNetworkCacheRepository.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/cache/impl/MockNetworkCacheRepository.kt
@@ -1,10 +1,10 @@
-package ru.n08i40k.polytechnic.next.data.cache.impl
+package ru.n08i40k.polytechnic.next.repository.cache.impl
import ru.n08i40k.polytechnic.next.CachedResponse
import ru.n08i40k.polytechnic.next.UpdateDates
-import ru.n08i40k.polytechnic.next.data.cache.NetworkCacheRepository
+import ru.n08i40k.polytechnic.next.repository.cache.NetworkCacheRepository
-class FakeNetworkCacheRepository : NetworkCacheRepository {
+class MockNetworkCacheRepository : NetworkCacheRepository {
override suspend fun get(url: String): CachedResponse? {
return null
}
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/repository/profile/ProfileRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/profile/ProfileRepository.kt
new file mode 100644
index 0000000..63e336c
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/profile/ProfileRepository.kt
@@ -0,0 +1,12 @@
+package ru.n08i40k.polytechnic.next.repository.profile
+
+import ru.n08i40k.polytechnic.next.model.Profile
+import ru.n08i40k.polytechnic.next.utils.MyResult
+
+interface ProfileRepository {
+ suspend fun getProfile(): MyResult
+
+ suspend fun setFCMToken(token: String): MyResult
+
+ suspend fun signOut()
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/repository/profile/impl/MockProfileRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/profile/impl/MockProfileRepository.kt
new file mode 100644
index 0000000..d70734a
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/profile/impl/MockProfileRepository.kt
@@ -0,0 +1,39 @@
+package ru.n08i40k.polytechnic.next.repository.profile.impl
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.withContext
+import ru.n08i40k.polytechnic.next.model.Profile
+import ru.n08i40k.polytechnic.next.model.UserRole
+import ru.n08i40k.polytechnic.next.repository.profile.ProfileRepository
+import ru.n08i40k.polytechnic.next.utils.MyResult
+
+class MockProfileRepository : ProfileRepository {
+ private var getCounter = 0
+
+ companion object {
+ val profile = Profile(
+ "66db32d24030a07e02d974c5",
+ "n08i40k",
+ "ИС-214/23",
+ UserRole.STUDENT
+ )
+ }
+
+ override suspend fun getProfile(): MyResult =
+ withContext(Dispatchers.IO) {
+ delay(1500)
+
+ if (++getCounter % 3 == 0)
+ MyResult.Failure(Exception())
+ else
+ MyResult.Success(profile)
+ }
+
+ override suspend fun setFCMToken(token: String): MyResult =
+ MyResult.Success(Unit)
+
+ override suspend fun signOut() {
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/repository/profile/impl/RemoteProfileRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/profile/impl/RemoteProfileRepository.kt
new file mode 100644
index 0000000..3047465
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/profile/impl/RemoteProfileRepository.kt
@@ -0,0 +1,59 @@
+package ru.n08i40k.polytechnic.next.repository.profile.impl
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import ru.n08i40k.polytechnic.next.app.AppContainer
+import ru.n08i40k.polytechnic.next.model.Profile
+import ru.n08i40k.polytechnic.next.network.request.fcm.FcmSetToken
+import ru.n08i40k.polytechnic.next.network.request.profile.ProfileMe
+import ru.n08i40k.polytechnic.next.network.tryFuture
+import ru.n08i40k.polytechnic.next.repository.profile.ProfileRepository
+import ru.n08i40k.polytechnic.next.settings.settings
+import ru.n08i40k.polytechnic.next.utils.MyResult
+import ru.n08i40k.polytechnic.next.utils.app
+
+class RemoteProfileRepository(private val container: AppContainer) : ProfileRepository {
+ override suspend fun getProfile(): MyResult {
+ return withContext(Dispatchers.IO) {
+ tryFuture(container.context) {
+ ProfileMe(
+ container,
+ it,
+ it
+ )
+ }
+ }
+ }
+
+ override suspend fun setFCMToken(token: String): MyResult =
+ withContext(Dispatchers.IO) {
+ tryFuture(container.context) {
+ FcmSetToken(
+ container,
+ token,
+ it,
+ it
+ )
+ }
+ }
+
+ override suspend fun signOut() {
+ val context = container.context
+
+ container.context.settings.updateData {
+ it
+ .toBuilder()
+ .clear()
+ .build()
+ }
+
+ context.app.events.signOut.next(Unit)
+
+// context.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)
+// val pm = context.packageManager
+// val intent = pm.getLaunchIntentForPackage(context.packageName)
+// val mainIntent = Intent.makeRestartActivityTask(intent?.component)
+// context.startActivity(mainIntent)
+// Runtime.getRuntime().exit(0)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/ScheduleRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/schedule/ScheduleRepository.kt
similarity index 66%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/ScheduleRepository.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/repository/schedule/ScheduleRepository.kt
index 4d75c5b..c5b11eb 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/ScheduleRepository.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/schedule/ScheduleRepository.kt
@@ -1,7 +1,7 @@
-package ru.n08i40k.polytechnic.next.data.schedule
+package ru.n08i40k.polytechnic.next.repository.schedule
-import ru.n08i40k.polytechnic.next.data.MyResult
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
+import ru.n08i40k.polytechnic.next.utils.MyResult
interface ScheduleRepository {
suspend fun getGroup(): MyResult
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/FakeScheduleRepository.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/schedule/impl/MockScheduleRepository.kt
similarity index 93%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/FakeScheduleRepository.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/repository/schedule/impl/MockScheduleRepository.kt
index 73f75fc..f6447e0 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/FakeScheduleRepository.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/schedule/impl/MockScheduleRepository.kt
@@ -1,21 +1,20 @@
-package ru.n08i40k.polytechnic.next.data.schedule.impl
+package ru.n08i40k.polytechnic.next.repository.schedule.impl
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
-import ru.n08i40k.polytechnic.next.data.MyResult
-import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
import ru.n08i40k.polytechnic.next.model.Day
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import ru.n08i40k.polytechnic.next.model.Lesson
import ru.n08i40k.polytechnic.next.model.LessonTime
import ru.n08i40k.polytechnic.next.model.LessonType
import ru.n08i40k.polytechnic.next.model.SubGroup
+import ru.n08i40k.polytechnic.next.repository.schedule.ScheduleRepository
+import ru.n08i40k.polytechnic.next.utils.MyResult
import ru.n08i40k.polytechnic.next.utils.now
private fun genLocalDateTime(hour: Int, minute: Int): Instant {
@@ -36,8 +35,7 @@ private fun genBreak(start: Instant, end: Instant): Lesson {
)
}
-class FakeScheduleRepository : ScheduleRepository {
- @Suppress("SpellCheckingInspection")
+class MockScheduleRepository : ScheduleRepository {
companion object {
val exampleGroup = GroupOrTeacher(
name = "ИС-214/23", days = arrayListOf(
@@ -144,7 +142,8 @@ class FakeScheduleRepository : ScheduleRepository {
),
group = null
),
- )
+ ),
+ street = "Железнодорожная 13",
)
)
)
@@ -254,34 +253,28 @@ class FakeScheduleRepository : ScheduleRepository {
),
group = "ИС-214/23"
),
- )
+ ),
+ null
)
)
)
}
- private val group = MutableStateFlow(exampleGroup)
- private val teacher = MutableStateFlow(exampleTeacher)
-
private var updateCounter: Int = 0
override suspend fun getGroup(): MyResult {
return withContext(Dispatchers.IO) {
delay(1500)
- if (updateCounter++ % 3 == 0) MyResult.Failure(
- IllegalStateException()
- )
- else MyResult.Success(group.value!!)
+ if (updateCounter++ % 3 == 0) MyResult.Failure()
+ else MyResult.Success(exampleGroup)
}
}
override suspend fun getTeacher(name: String): MyResult {
return withContext(Dispatchers.IO) {
delay(1500)
- if (updateCounter++ % 3 == 0) MyResult.Failure(
- IllegalStateException()
- )
- else MyResult.Success(teacher.value!!)
+ if (updateCounter++ % 3 == 0) MyResult.Failure()
+ else MyResult.Success(exampleTeacher)
}
}
}
\ No newline at end of file
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/repository/schedule/impl/RemoteScheduleRepository.kt
similarity index 68%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/RemoteScheduleRepository.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/repository/schedule/impl/RemoteScheduleRepository.kt
index 20adb34..166b2c8 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/data/schedule/impl/RemoteScheduleRepository.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/repository/schedule/impl/RemoteScheduleRepository.kt
@@ -1,37 +1,39 @@
-package ru.n08i40k.polytechnic.next.data.schedule.impl
+package ru.n08i40k.polytechnic.next.repository.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.schedule.ScheduleRepository
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGet
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetTeacher
import ru.n08i40k.polytechnic.next.network.tryFuture
+import ru.n08i40k.polytechnic.next.repository.schedule.ScheduleRepository
+import ru.n08i40k.polytechnic.next.utils.MyResult
+
+class RemoteScheduleRepository(private val container: AppContainer) : ScheduleRepository {
+ private val context get() = container.context
-class RemoteScheduleRepository(private val context: Context) : ScheduleRepository {
override suspend fun getGroup(): MyResult =
withContext(Dispatchers.IO) {
- val response = tryFuture {
+ val response = tryFuture(context) {
ScheduleGet(
- context,
+ container,
it,
it
)
}
when (response) {
- is MyResult.Failure -> response
is MyResult.Success -> MyResult.Success(response.data.group)
+ is MyResult.Failure -> response
}
}
override suspend fun getTeacher(name: String): MyResult =
withContext(Dispatchers.IO) {
- val response = tryFuture {
+ val response = tryFuture(context) {
ScheduleGetTeacher(
- context,
+ container,
name,
it,
it
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/service/CurrentLessonViewService.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/service/CurrentLessonViewService.kt
deleted file mode 100644
index 9333eb3..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/service/CurrentLessonViewService.kt
+++ /dev/null
@@ -1,210 +0,0 @@
-package ru.n08i40k.polytechnic.next.service
-
-import android.app.Notification
-import android.app.NotificationManager
-import android.app.Service
-import android.content.Intent
-import android.os.Build
-import android.os.Handler
-import android.os.IBinder
-import android.os.Looper
-import androidx.core.app.NotificationCompat
-import androidx.core.content.ContextCompat.startForegroundService
-import kotlinx.datetime.LocalDateTime
-import ru.n08i40k.polytechnic.next.NotificationChannels
-import ru.n08i40k.polytechnic.next.PolytechnicApplication
-import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.data.MyResult
-import ru.n08i40k.polytechnic.next.model.Day
-import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
-import ru.n08i40k.polytechnic.next.utils.dayMinutes
-import ru.n08i40k.polytechnic.next.utils.fmtAsClock
-import ru.n08i40k.polytechnic.next.utils.getDayMinutes
-import ru.n08i40k.polytechnic.next.utils.now
-import java.util.Calendar
-import java.util.logging.Logger
-
-class CurrentLessonViewService : Service() {
- companion object {
- private const val NOTIFICATION_STATUS_ID = 1337
- private const val NOTIFICATION_END_ID = NOTIFICATION_STATUS_ID + 1
- private const val UPDATE_INTERVAL = 1_000L
-
- suspend fun startService(application: PolytechnicApplication) {
- if (!application.hasNotificationPermission())
- return
-
- val schedule =
- application
- .container
- .scheduleRepository
- .getGroup()
-
- if (schedule is MyResult.Failure)
- return
-
- val intent = Intent(application, CurrentLessonViewService::class.java)
- .apply {
- putExtra("group", (schedule as MyResult.Success).data)
- }
-
- application.stopService(
- Intent(
- application,
- CurrentLessonViewService::class.java
- )
- )
- startForegroundService(application, intent)
- }
- }
-
- private lateinit var day: Day
-
- private val handler = Handler(Looper.getMainLooper())
-
- private val updateRunnable = object : Runnable {
- override fun run() {
- val (currentIndex, currentLesson) = day.currentKV ?: (null to null)
- val (nextIndex, _) = day.distanceToNext(currentIndex)
- ?: (null to null)
-
- val nextLesson = nextIndex?.let { day.lessons[nextIndex] }
-
- if (currentLesson == null && nextLesson == null) {
- onLessonsEnd()
- return
- }
-
- handler.postDelayed(this, UPDATE_INTERVAL)
-
- val context = this@CurrentLessonViewService
- val currentMinutes = LocalDateTime.now().dayMinutes
-
- val distanceToFirst = day.first!!.time.start.dayMinutes - currentMinutes
-
- val currentLessonName =
- currentLesson?.getNameAndCabinetsShort(context)
- ?: run {
- if (distanceToFirst > 0)
- getString(R.string.lessons_not_started)
- else
- getString(R.string.lesson_break)
- }
-
- val nextLessonName =
- nextLesson?.getNameAndCabinetsShort(context) ?: getString(R.string.lessons_end)
-
- val nextLessonIn =
- (currentLesson?.time?.end ?: nextLesson!!.time.start).dayMinutes
-
- val notification = createNotification(
- getString(
- if (distanceToFirst > 0) R.string.waiting_for_day_start_notification_title
- else R.string.lesson_going_notification_title,
- (nextLessonIn - currentMinutes) / 60,
- (nextLessonIn - currentMinutes) % 60
- ),
- getString(
- R.string.lesson_going_notification_description,
- currentLessonName,
- nextLessonIn.fmtAsClock(),
- nextLessonName,
- )
- )
- getNotificationManager().notify(NOTIFICATION_STATUS_ID, notification)
- }
- }
-
- private fun onLessonsEnd() {
- val notification = NotificationCompat
- .Builder(applicationContext, NotificationChannels.LESSON_VIEW)
- .setSmallIcon(R.drawable.schedule)
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setContentTitle(getString(R.string.lessons_end_notification_title))
- .setContentText(getString(R.string.lessons_end_notification_description))
- .build()
- getNotificationManager().notify(NOTIFICATION_END_ID, notification)
-
- stopSelf()
- }
-
- private fun createNotification(
- title: String? = null,
- description: String? = null
- ): Notification {
- return NotificationCompat
- .Builder(applicationContext, NotificationChannels.LESSON_VIEW)
- .setSmallIcon(R.drawable.schedule)
- .setContentTitle(title ?: getString(R.string.lesson_notification_title))
- .setContentText(description ?: getString(R.string.lesson_notification_description))
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setOngoing(true)
- .setSilent(true)
- .build()
- }
-
- private fun getNotificationManager(): NotificationManager {
- return getSystemService(NOTIFICATION_SERVICE) as NotificationManager
- }
-
- private fun updateSchedule(group: GroupOrTeacher?) {
- val logger = Logger.getLogger("CLV")
-
- if (group == null) {
- logger.warning("Stopping, because group is null")
- stopSelf()
- return
- }
-
- val currentDay = group.current
- if (currentDay == null || currentDay.lessons.isEmpty()) {
- logger.warning("Stopping, because current day is null or empty")
- stopSelf()
- return
- }
-
- val nowMinutes = Calendar.getInstance().getDayMinutes()
- if (nowMinutes < ((5 * 60) + 30)
- || currentDay.last!!.time.end.dayMinutes < nowMinutes
- ) {
- logger.warning("Stopping, because service started outside of acceptable time range!")
- stopSelf()
- return
- }
-
- this.day = currentDay
-
- this.handler.removeCallbacks(updateRunnable)
- updateRunnable.run()
-
- logger.info("Running...")
- }
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- if (!(applicationContext as PolytechnicApplication).hasNotificationPermission()) {
- stopSelf()
- return START_STICKY
- }
-
- if (intent == null)
- throw NullPointerException("Intent shouldn't be null!")
-
- val notification = createNotification()
- startForeground(NOTIFICATION_STATUS_ID, notification)
-
- updateSchedule(
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- intent.getParcelableExtra("group", GroupOrTeacher::class.java)
- } else {
- @Suppress("DEPRECATION")
- intent.getParcelableExtra("group")
- }
- )
-
- return START_STICKY
- }
-
- override fun onBind(p0: Intent?): IBinder? {
- return null
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/service/FCMService.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/service/FCMService.kt
new file mode 100644
index 0000000..63aea01
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/service/FCMService.kt
@@ -0,0 +1,159 @@
+package ru.n08i40k.polytechnic.next.service
+
+import android.Manifest
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import androidx.annotation.DrawableRes
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import com.google.firebase.messaging.FirebaseMessagingService
+import com.google.firebase.messaging.RemoteMessage
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.app.NotificationChannels
+import ru.n08i40k.polytechnic.next.worker.UpdateFCMTokenWorker
+
+
+private data class ScheduleUpdateData(
+ val type: String,
+ val replaced: Boolean,
+ val etag: String
+) {
+ constructor(message: RemoteMessage) : this(
+ type = message.data["type"]
+ ?: throw IllegalArgumentException("Type is missing in RemoteMessage"),
+ replaced = message.data["replaced"]?.toBoolean()
+ ?: throw IllegalArgumentException("Replaced is missing in RemoteMessage"),
+ etag = message.data["etag"]
+ ?: throw IllegalArgumentException("Etag is missing in RemoteMessage")
+ )
+
+ fun handleMessage(service: FCMService) {
+ service.sendNotification(
+ NotificationChannels.SCHEDULE_UPDATE,
+ R.drawable.schedule,
+ service.getString(R.string.schedule_update_title),
+ service.getString(
+ if (replaced)
+ R.string.schedule_update_replaced
+ else
+ R.string.schedule_update_default
+ ),
+ etag
+ )
+ }
+}
+
+private data class LessonsStartData(
+ val type: String
+) {
+ constructor(message: RemoteMessage) : this(
+ type = message.data["type"]
+ ?: throw IllegalArgumentException("Type is missing in RemoteMessage")
+ )
+
+ // TODO: вернуть
+ @Suppress("unused")
+ fun handleMessage(service: FCMService) {
+ // Uncomment and implement if needed
+ // service.scope.launch {
+ // CurrentLessonViewService
+ // .startService(service.applicationContext as PolytechnicApplication)
+ // }
+ }
+}
+
+private data class AppUpdateData(
+ val type: String,
+ val version: String,
+ val downloadLink: String
+) {
+ constructor(message: RemoteMessage) : this(
+ type = message.data["type"]
+ ?: throw IllegalArgumentException("Type is missing in RemoteMessage"),
+ version = message.data["version"]
+ ?: throw IllegalArgumentException("Version is missing in RemoteMessage"),
+ downloadLink = message.data["downloadLink"]
+ ?: throw IllegalArgumentException("DownloadLink is missing in RemoteMessage")
+ )
+
+ fun handleMessage(service: FCMService) {
+ service.sendNotification(
+ NotificationChannels.APP_UPDATE,
+ R.drawable.download,
+ service.getString(R.string.app_update_title, version),
+ service.getString(R.string.app_update_description),
+ version,
+ Intent(Intent.ACTION_VIEW, Uri.parse(downloadLink))
+ )
+ }
+}
+
+class FCMService : FirebaseMessagingService() {
+ // TODO: вернуть
+ @Suppress("unused")
+ private val scope = CoroutineScope(Job() + Dispatchers.Main)
+
+ override fun onNewToken(token: String) {
+ super.onNewToken(token)
+
+ UpdateFCMTokenWorker.schedule(this, token)
+ }
+
+
+ fun sendNotification(
+ channel: String,
+ @DrawableRes iconId: Int,
+ title: String,
+ contentText: String,
+ id: Any?,
+ intent: Intent? = null
+ ) {
+ val pendingIntent: PendingIntent? =
+ if (intent != null)
+ PendingIntent.getActivity(this, 0, intent.apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }, PendingIntent.FLAG_IMMUTABLE)
+ else
+ null
+
+ val notification = NotificationCompat
+ .Builder(applicationContext, channel)
+ .setSmallIcon(iconId)
+ .setContentTitle(title)
+ .setContentText(contentText)
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setAutoCancel(true)
+ .setContentIntent(pendingIntent)
+ .build()
+
+ with(NotificationManagerCompat.from(this)) {
+ if (ActivityCompat.checkSelfPermission(
+ this@FCMService,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ return@with
+ }
+
+ notify(id.hashCode(), notification)
+ }
+ }
+
+ override fun onMessageReceived(message: RemoteMessage) {
+ val type = message.data["type"]
+
+ when (type) {
+ "schedule-update" -> ScheduleUpdateData(message).handleMessage(this)
+ "lessons-start" -> LessonsStartData(message).handleMessage(this)
+ "app-update" -> AppUpdateData(message).handleMessage(this)
+ }
+
+ super.onMessageReceived(message)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/service/MyFirebaseMessagingService.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/service/MyFirebaseMessagingService.kt
deleted file mode 100644
index 36d9f26..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/service/MyFirebaseMessagingService.kt
+++ /dev/null
@@ -1,130 +0,0 @@
-package ru.n08i40k.polytechnic.next.service
-
-import android.Manifest
-import android.app.PendingIntent
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.net.Uri
-import androidx.annotation.DrawableRes
-import androidx.core.app.ActivityCompat
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
-import androidx.work.BackoffPolicy
-import androidx.work.Constraints
-import androidx.work.NetworkType
-import androidx.work.OneTimeWorkRequestBuilder
-import androidx.work.WorkManager
-import androidx.work.workDataOf
-import com.google.firebase.messaging.FirebaseMessagingService
-import com.google.firebase.messaging.RemoteMessage
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-import ru.n08i40k.polytechnic.next.NotificationChannels
-import ru.n08i40k.polytechnic.next.PolytechnicApplication
-import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.work.FcmSetTokenWorker
-import java.time.Duration
-
-class MyFirebaseMessagingService : FirebaseMessagingService() {
- private val scope = CoroutineScope(Job() + Dispatchers.Main)
-
- override fun onNewToken(token: String) {
- super.onNewToken(token)
-
- val constraints = Constraints.Builder()
- .setRequiredNetworkType(NetworkType.CONNECTED)
- .build()
-
- val request = OneTimeWorkRequestBuilder()
- .setConstraints(constraints)
- .setBackoffCriteria(BackoffPolicy.LINEAR, Duration.ofMinutes(1))
- .setInputData(workDataOf("TOKEN" to token))
- .build()
-
- WorkManager
- .getInstance(applicationContext)
- .enqueue(request)
- }
-
- private fun sendNotification(
- channel: String,
- @DrawableRes iconId: Int,
- title: String,
- contentText: String,
- id: Any?,
- intent: Intent? = null
- ) {
- val pendingIntent: PendingIntent? =
- if (intent != null)
- PendingIntent.getActivity(this, 0, intent.apply {
- flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
- }, PendingIntent.FLAG_IMMUTABLE)
- else
- null
-
- val notification = NotificationCompat
- .Builder(applicationContext, channel)
- .setSmallIcon(iconId)
- .setContentTitle(title)
- .setContentText(contentText)
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setAutoCancel(true)
- .setContentIntent(pendingIntent)
- .build()
-
- with(NotificationManagerCompat.from(this)) {
- if (ActivityCompat.checkSelfPermission(
- this@MyFirebaseMessagingService,
- Manifest.permission.POST_NOTIFICATIONS
- ) != PackageManager.PERMISSION_GRANTED
- ) {
- return@with
- }
-
- notify(id.hashCode(), notification)
- }
- }
-
- override fun onMessageReceived(message: RemoteMessage) {
- val type = message.data["type"]
-
- when (type) {
- "schedule-update" -> {
- sendNotification(
- NotificationChannels.SCHEDULE_UPDATE,
- R.drawable.schedule,
- getString(R.string.schedule_update_title),
- getString(
- if (message.data["replaced"] == "true")
- R.string.schedule_update_replaced
- else
- R.string.schedule_update_default
- ),
- message.data["etag"]
- )
- }
-
- "lessons-start" -> {
- scope.launch {
- CurrentLessonViewService
- .startService(applicationContext as PolytechnicApplication)
- }
- }
-
- "app-update" -> {
- sendNotification(
- NotificationChannels.APP_UPDATE,
- R.drawable.download,
- getString(R.string.app_update_title, message.data["version"]),
- getString(R.string.app_update_description),
- message.data["version"],
- Intent(Intent.ACTION_VIEW, Uri.parse(message.data["downloadLink"]))
- )
- }
- }
-
- super.onMessageReceived(message)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/settings/SettingsSerializer.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/settings/SettingsSerializer.kt
index 82a405b..54a9ffa 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/settings/SettingsSerializer.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/settings/SettingsSerializer.kt
@@ -13,18 +13,17 @@ import java.io.OutputStream
object SettingsSerializer : Serializer {
override val defaultValue: Settings = Settings.getDefaultInstance()
- override suspend fun readFrom(input: InputStream): Settings {
+ override suspend fun readFrom(input: InputStream): Settings =
try {
- return Settings.parseFrom(input)
+ Settings.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
- }
override suspend fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
}
-val Context.settingsDataStore: DataStore by dataStore(
+val Context.settings: DataStore by dataStore(
fileName = "settings.pb",
serializer = SettingsSerializer
)
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/MainActivity.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/MainActivity.kt
deleted file mode 100644
index 18f0aeb..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/MainActivity.kt
+++ /dev/null
@@ -1,177 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui
-
-import android.Manifest
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.os.Bundle
-import android.util.Log
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.WindowInsetsSides
-import androidx.compose.foundation.layout.only
-import androidx.compose.foundation.layout.safeContent
-import androidx.compose.foundation.layout.windowInsetsPadding
-import androidx.compose.ui.Modifier
-import androidx.core.view.WindowCompat
-import androidx.lifecycle.lifecycleScope
-import androidx.work.BackoffPolicy
-import androidx.work.Constraints
-import androidx.work.NetworkType
-import androidx.work.OneTimeWorkRequestBuilder
-import androidx.work.PeriodicWorkRequest
-import androidx.work.WorkManager
-import androidx.work.workDataOf
-import com.google.firebase.remoteconfig.remoteConfigSettings
-import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
-import ru.n08i40k.polytechnic.next.NotificationChannels
-import ru.n08i40k.polytechnic.next.PolytechnicApplication
-import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.settings.settingsDataStore
-import ru.n08i40k.polytechnic.next.work.FcmUpdateCallbackWorker
-import ru.n08i40k.polytechnic.next.work.LinkUpdateWorker
-import java.time.Duration
-import java.util.concurrent.TimeUnit
-
-@AndroidEntryPoint
-class MainActivity : ComponentActivity() {
- private val configSettings = remoteConfigSettings {
- minimumFetchIntervalInSeconds = 3600
- }
-
- private fun createNotificationChannel(
- notificationManager: NotificationManager,
- name: String,
- description: String,
- channelId: String
- ) {
- val channel = NotificationChannel(channelId, name, NotificationManager.IMPORTANCE_DEFAULT)
- channel.description = description
-
- notificationManager.createNotificationChannel(channel)
- }
-
- private fun createNotificationChannels() {
- if (!(applicationContext as PolytechnicApplication).hasNotificationPermission())
- return
-
- val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
-
- createNotificationChannel(
- notificationManager,
- getString(R.string.schedule_channel_name),
- getString(R.string.schedule_channel_description),
- NotificationChannels.SCHEDULE_UPDATE
- )
-
- createNotificationChannel(
- notificationManager,
- getString(R.string.app_update_channel_name),
- getString(R.string.app_update_channel_description),
- NotificationChannels.APP_UPDATE
- )
-
- createNotificationChannel(
- notificationManager,
- getString(R.string.lesson_view_channel_name),
- getString(R.string.lesson_view_channel_description),
- NotificationChannels.LESSON_VIEW
- )
- }
-
- private val requestPermissionLauncher =
- registerForActivityResult(ActivityResultContracts.RequestPermission()) {
- if (it) createNotificationChannels()
- }
-
- private fun askNotificationPermission() {
- if (!(applicationContext as PolytechnicApplication).hasNotificationPermission())
- requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
- }
-
-
- fun scheduleLinkUpdate(intervalInMinutes: Long) {
- val tag = "schedule-update"
-
- val workRequest = PeriodicWorkRequest.Builder(
- LinkUpdateWorker::class.java,
- intervalInMinutes.coerceAtLeast(15), TimeUnit.MINUTES
- )
- .addTag(tag)
- .build()
-
- val workManager = WorkManager.getInstance(applicationContext)
-
- workManager.cancelAllWorkByTag(tag)
- workManager.enqueue(workRequest)
- }
-
- private fun setupFirebaseConfig() {
- val remoteConfig = (application as PolytechnicApplication).container.remoteConfig
-
- remoteConfig.setConfigSettingsAsync(configSettings)
- remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
-
- remoteConfig
- .fetchAndActivate()
- .addOnCompleteListener {
- if (!it.isSuccessful)
- Log.w("RemoteConfig", "Failed to fetch and activate!")
-
- scheduleLinkUpdate(remoteConfig.getLong("linkUpdateDelay"))
- }
- }
-
- private fun handleUpdate() {
- lifecycleScope.launch {
- val appVersion = (applicationContext as PolytechnicApplication).getAppVersion()
-
- if (settingsDataStore.data.map { it.version }.first() != appVersion) {
- settingsDataStore.updateData { it.toBuilder().setVersion(appVersion).build() }
-
- val constraints = Constraints.Builder()
- .setRequiredNetworkType(NetworkType.CONNECTED)
- .build()
-
- val request = OneTimeWorkRequestBuilder()
- .setConstraints(constraints)
- .setBackoffCriteria(BackoffPolicy.LINEAR, Duration.ofMinutes(1))
- .setInputData(workDataOf("VERSION" to appVersion))
- .build()
-
- WorkManager
- .getInstance(this@MainActivity)
- .enqueue(request)
- }
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- enableEdgeToEdge()
- WindowCompat.setDecorFitsSystemWindows(window, false)
- super.onCreate(savedInstanceState)
-
- askNotificationPermission()
- createNotificationChannels()
-
- setupFirebaseConfig()
-
- handleUpdate()
-
- setContent {
- Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) {
- PolytechnicApp()
- }
- }
-
- lifecycleScope.launch {
- settingsDataStore.data.first()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/PolytechnicApp.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/PolytechnicApp.kt
index cd48bbc..c5e05bb 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/PolytechnicApp.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/PolytechnicApp.kt
@@ -1,42 +1,276 @@
package ru.n08i40k.polytechnic.next.ui
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Card
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
-import ru.n08i40k.polytechnic.next.settings.settingsDataStore
-import ru.n08i40k.polytechnic.next.ui.auth.AuthScreen
-import ru.n08i40k.polytechnic.next.ui.main.MainScreen
-import ru.n08i40k.polytechnic.next.ui.theme.AppTheme
+import ru.n08i40k.polytechnic.next.Application
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.settings.settings
+import ru.n08i40k.polytechnic.next.ui.screen.MainScreen
+import ru.n08i40k.polytechnic.next.ui.screen.auth.AuthScreen
+import ru.n08i40k.polytechnic.next.utils.app
+import ru.n08i40k.polytechnic.next.utils.openLink
+import kotlin.system.exitProcess
+enum class AppRoute(val route: String) {
+ AUTH("auth"),
+ MAIN("main")
+}
+
+private data class SemVersion(val major: Int, val minor: Int, val patch: Int) :
+ Comparable {
+ companion object {
+ fun fromString(version: String): SemVersion {
+ val numbers = version.split(".").map { it.toInt() }
+ assert(numbers.size == 3)
+
+ return SemVersion(numbers[0], numbers[1], numbers[2])
+ }
+ }
+
+ override fun equals(other: Any?): Boolean =
+ when (other) {
+ is SemVersion -> this.major == other.major && this.minor == other.minor && this.patch == other.patch
+ else -> false
+ }
+
+ override fun toString(): String {
+ return "$major.$minor.$patch"
+ }
+
+ override fun compareTo(b: SemVersion): Int {
+ val majorDiff = this.major - b.major
+ if (majorDiff != 0) return majorDiff
+
+ val minorDiff = this.minor - b.minor
+ if (minorDiff != 0) return minorDiff
+
+ return this.patch - b.patch
+ }
+
+ override fun hashCode(): Int {
+ var result = super.hashCode()
+ result = 31 * result + major
+ result = 31 * result + minor
+ result = 31 * result + patch
+ return result
+ }
+}
+
+@Composable
+private fun checkUpdate(): Boolean {
+ val context = LocalContext.current
+ val app = context.applicationContext as Application
+ val remoteConfig = app.container.remoteConfig
+
+ val currentVersion = SemVersion.fromString(app.version)
+ val minRequiredVersion = SemVersion.fromString(remoteConfig.getString("minVersion"))
+
+ val downloadLink = remoteConfig.getString("downloadLink")
+
+ if (currentVersion < minRequiredVersion) {
+ Dialog({ exitProcess(0) }, DialogProperties(false, false)) {
+ Card(border = BorderStroke(Dp.Hairline, MaterialTheme.colorScheme.inverseSurface)) {
+ var dialogWidth by remember { mutableStateOf(Dp.Unspecified) }
+ val localDensity = LocalDensity.current
+
+ Column(
+ Modifier
+ .padding(10.dp)
+ .onGloballyPositioned {
+ with(localDensity) {
+ dialogWidth = it.size.width.toDp()
+ }
+ },
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ stringResource(R.string.updater_support_end, minRequiredVersion),
+ Modifier.padding(0.dp, 10.dp),
+ style = MaterialTheme.typography.titleLarge
+ )
+
+ Spacer(Modifier.height(5.dp))
+
+ Text(stringResource(R.string.updater_body))
+
+ Spacer(Modifier.height(10.dp))
+
+ if (dialogWidth != Dp.Unspecified) {
+ Row(Modifier.width(dialogWidth), Arrangement.SpaceBetween) {
+ TextButton({ exitProcess(0) }) {
+ Text(
+ stringResource(R.string.updater_exit),
+ color = MaterialTheme.colorScheme.secondaryContainer
+ )
+ }
+ TextButton({ context.openLink(downloadLink) }) {
+ Text(
+ stringResource(R.string.updater_update),
+ color = MaterialTheme.colorScheme.primaryContainer
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return false
+ }
+
+ val latestVersion = SemVersion.fromString(remoteConfig.getString("currVersion"))
+ var suppressedVersion by rememberSaveable {
+ mutableStateOf(
+ runBlocking {
+ val data = context.settings.data.map { it.suppressedVersion }.first()
+
+ if (data.isEmpty())
+ "0.0.0"
+ else
+ data
+ }
+ )
+ }
+ val suppressedSemVer by remember { derivedStateOf { SemVersion.fromString(suppressedVersion) } }
+
+ if (latestVersion > currentVersion && latestVersion != suppressedSemVer) {
+ Dialog({ exitProcess(0) }, DialogProperties(false, false)) {
+ Card(border = BorderStroke(Dp.Hairline, MaterialTheme.colorScheme.inverseSurface)) {
+ var dialogWidth by remember { mutableStateOf(Dp.Unspecified) }
+ val localDensity = LocalDensity.current
+
+ Column(
+ Modifier
+ .padding(10.dp)
+ .onGloballyPositioned {
+ with(localDensity) {
+ dialogWidth = it.size.width.toDp()
+ }
+ },
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ stringResource(R.string.updater_new_version),
+ Modifier.padding(0.dp, 10.dp),
+ style = MaterialTheme.typography.titleLarge
+ )
+
+ Spacer(Modifier.height(5.dp))
+
+ Text(stringResource(R.string.updater_body))
+
+ Spacer(Modifier.height(10.dp))
+
+ if (dialogWidth != Dp.Unspecified) {
+ Row(Modifier.width(dialogWidth), Arrangement.SpaceBetween) {
+ Row {
+ TextButton({ suppressedVersion = latestVersion.toString() }) {
+ Text(
+ stringResource(R.string.updater_no),
+ color = MaterialTheme.colorScheme.secondaryContainer
+ )
+ }
+ TextButton({
+ runBlocking {
+ context.settings.updateData {
+ it.toBuilder()
+ .setSuppressedVersion(latestVersion.toString())
+ .build()
+ }
+ }
+ suppressedVersion = latestVersion.toString()
+ }) {
+ Text(
+ stringResource(R.string.updater_suppress),
+ color = MaterialTheme.colorScheme.secondaryContainer
+ )
+ }
+ }
+
+ TextButton({ context.openLink(downloadLink) }) {
+ Text(
+ stringResource(R.string.updater_update),
+ color = MaterialTheme.colorScheme.primaryContainer
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return false
+ }
+
+ return true
+}
-@Preview(showBackground = true, showSystemUi = true)
@Composable
fun PolytechnicApp() {
- AppTheme(darkTheme = true, content = {
- val navController = rememberNavController()
- val context = LocalContext.current
+ if (!checkUpdate())
+ return
- val accessToken = runBlocking {
- context.settingsDataStore.data.map { it.accessToken }.first()
+ val navController = rememberNavController()
+ val context = LocalContext.current
+
+ remember {
+ context.app.events.signOut.subscribe(
+ context,
+ {
+ navController.navigate(AppRoute.AUTH.route) {
+ popUpTo(AppRoute.AUTH.route) { inclusive = true }
+ }
+ }
+ )
+ }
+
+ val token = runBlocking {
+ context.settings.data.map { it.accessToken }.first()
+ }
+
+ NavHost(
+ navController,
+ startDestination = if (token.isEmpty()) AppRoute.AUTH.route else AppRoute.MAIN.route
+ ) {
+ composable(AppRoute.AUTH.route) {
+ AuthScreen(navController)
}
- NavHost(
- navController = navController,
- startDestination = if (accessToken.isEmpty()) "auth" else "main"
- ) {
- composable(route = "auth") {
- AuthScreen(navController)
- }
-
- composable(route = "main") {
- MainScreen(navController)
- }
+ composable(AppRoute.MAIN.route) {
+ MainScreen(navController)
}
- })
+ }
}
\ 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
deleted file mode 100644
index 727977f..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/AuthScreen.kt
+++ /dev/null
@@ -1,149 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.auth
-
-import androidx.compose.animation.core.FastOutSlowInEasing
-import androidx.compose.animation.core.LinearOutSlowInEasing
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideIn
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Card
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SnackbarDuration
-import androidx.compose.material3.SnackbarHost
-import androidx.compose.material3.SnackbarHostState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.dp
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.composable
-import androidx.navigation.compose.rememberNavController
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import ru.n08i40k.polytechnic.next.settings.settingsDataStore
-
-
-@Preview(showBackground = true)
-@Composable
-fun AuthForm(
- appNavController: NavHostController = rememberNavController(),
- onPendingSnackbar: (String) -> Unit = {},
-) {
- val navController = rememberNavController()
-
- val modifier = Modifier.fillMaxSize()
-
- NavHost(
- modifier = Modifier.fillMaxSize(),
- navController = navController,
- startDestination = "sign-in",
- enterTransition = {
- slideIn(
- animationSpec = tween(
- 400,
- delayMillis = 250,
- easing = LinearOutSlowInEasing
- )
- ) { fullSize -> IntOffset(0, fullSize.height / 16) } + fadeIn(
- animationSpec = tween(
- 400,
- delayMillis = 250,
- easing = LinearOutSlowInEasing
- )
- )
- },
- exitTransition = {
- fadeOut(
- animationSpec = tween(
- 250,
- easing = FastOutSlowInEasing
- )
- )
- },
- ) {
- composable("sign-in") {
- Row(
- modifier,
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Card(border = BorderStroke(Dp.Hairline, MaterialTheme.colorScheme.inverseSurface)) {
- LoginForm(appNavController, navController, onPendingSnackbar)
- }
- }
- }
- composable("sign-up") {
- Row(
- modifier,
- horizontalArrangement = Arrangement.Center,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Card(border = BorderStroke(Dp.Hairline, MaterialTheme.colorScheme.inverseSurface)) {
- RegisterForm(appNavController, navController, onPendingSnackbar)
- }
- }
- }
- }
-
-}
-
-
-@Preview(showBackground = true)
-@Composable
-fun AuthScreen(appNavController: NavHostController = rememberNavController()) {
- val context = LocalContext.current
-
- LaunchedEffect(Unit) {
- val accessToken: String = runBlocking {
- context.settingsDataStore.data.map { settings -> settings.accessToken }.first()
- }
-
- if (accessToken.isNotEmpty()) {
- appNavController.navigate("main") {
- popUpTo("auth") { inclusive = true }
- }
- }
- }
-
- val snackbarHostState = remember { SnackbarHostState() }
- val scope = rememberCoroutineScope()
-
- val onPendingSnackbar: (String) -> Unit = {
- scope.launch { snackbarHostState.showSnackbar(it, duration = SnackbarDuration.Long) }
- }
-
- Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) },
- modifier = Modifier.fillMaxSize(),
- contentWindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp),
- content = { paddingValues ->
- Row(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues),
- horizontalArrangement = Arrangement.SpaceAround,
- verticalAlignment = Alignment.CenterVertically
- ) {
- AuthForm(
- appNavController,
- onPendingSnackbar
- )
- }
- })
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/SignInForm.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/SignInForm.kt
deleted file mode 100644
index d0be085..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/SignInForm.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.auth
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
-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.platform.LocalFocusManager
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.PasswordVisualTransformation
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.rememberNavController
-import ru.n08i40k.polytechnic.next.R
-
-
-@Preview(showBackground = true)
-@Composable
-internal fun LoginForm(
- appNavController: NavHostController = rememberNavController(),
- navController: NavHostController = rememberNavController(),
- onPendingSnackbar: (String) -> Unit = {}
-) {
- val context = LocalContext.current
- val focusManager = LocalFocusManager.current
-
- var loading by remember { mutableStateOf(false) }
-
- var username by remember { mutableStateOf("") }
- var password by remember { mutableStateOf("") }
-
- var usernameError by remember { mutableStateOf(false) }
- var passwordError by remember { mutableStateOf(false) }
-
- val onClick = fun() {
- focusManager.clearFocus()
-
- if (username.length < 4) usernameError = true
- if (password.isEmpty()) passwordError = true
-
- if (usernameError || passwordError) return
-
- loading = true
-
- trySignIn(
- context,
- username,
- password,
- {
- loading = false
-
- val stringRes = when (it) {
- SignInError.INVALID_CREDENTIALS -> {
- usernameError = true
- passwordError = true
-
- R.string.invalid_credentials
- }
-
- SignInError.TIMED_OUT -> R.string.timed_out
- SignInError.NO_CONNECTION -> R.string.no_connection
- SignInError.APPLICATION_TOO_OLD -> R.string.app_too_old
- SignInError.UNKNOWN -> R.string.unknown_error
- }
-
- onPendingSnackbar(context.getString(stringRes))
- },
- {
- loading = false
-
- appNavController.navigate("main") {
- popUpTo("auth") { inclusive = true }
- }
- }
- )
- }
-
- Column(
- modifier = Modifier.padding(20.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
-
- Text(
- text = stringResource(R.string.sign_in_title),
- modifier = Modifier.padding(10.dp),
- style = MaterialTheme.typography.displaySmall,
- fontWeight = FontWeight.ExtraBold
- )
-
- Spacer(modifier = Modifier.size(10.dp))
-
- OutlinedTextField(
- value = username,
- singleLine = true,
- onValueChange = {
- username = it
- usernameError = false
- },
- label = { Text(stringResource(R.string.username)) },
- isError = usernameError
- )
-
- OutlinedTextField(
- value = password,
- singleLine = true,
- visualTransformation = PasswordVisualTransformation(),
- onValueChange = {
- passwordError = false
- password = it
- },
- label = { Text(stringResource(R.string.password)) },
- isError = passwordError
- )
-
- TextButton(onClick = { navController.navigate("sign-up") }) {
- Text(text = stringResource(R.string.not_registered))
- }
-
- Button(
- enabled = !loading && !(usernameError || passwordError),
- onClick = onClick
- ) {
- Text(
- text = stringResource(R.string.proceed),
- style = MaterialTheme.typography.bodyLarge
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/SignUpForm.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/SignUpForm.kt
deleted file mode 100644
index f1c84b1..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/SignUpForm.kt
+++ /dev/null
@@ -1,177 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.auth
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.OutlinedTextField
-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.platform.LocalFocusManager
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.input.PasswordVisualTransformation
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.navigation.NavHostController
-import androidx.navigation.compose.rememberNavController
-import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.model.UserRole
-import ru.n08i40k.polytechnic.next.ui.widgets.GroupSelector
-import ru.n08i40k.polytechnic.next.ui.widgets.RoleSelector
-import ru.n08i40k.polytechnic.next.ui.widgets.TeacherNameSelector
-
-
-@Preview(showBackground = true)
-@Composable
-internal fun RegisterForm(
- appNavController: NavHostController = rememberNavController(),
- navController: NavHostController = rememberNavController(),
- onPendingSnackbar: (String) -> Unit = {}
-) {
- val context = LocalContext.current
- val focusManager = LocalFocusManager.current
-
- var loading by remember { mutableStateOf(false) }
-
- var username by remember { mutableStateOf("") }
- var password by remember { mutableStateOf("") }
- var group by remember { mutableStateOf(null) }
- var role by remember { mutableStateOf(UserRole.STUDENT) }
-
- var usernameError by remember { mutableStateOf(false) }
- var passwordError by remember { mutableStateOf(false) }
- var groupError by remember { mutableStateOf(false) }
-
- val onClick = fun() {
- focusManager.clearFocus()
-
- if (username.length < 4) usernameError = true
- if (password.isEmpty()) passwordError = true
-
- if (usernameError || passwordError || groupError) return
-
- loading = true
-
- trySignUp(
- context,
- username,
- password,
- group!!,
- role,
- {
- loading = false
-
- val stringRes = when (it) {
- SignUpError.UNKNOWN -> R.string.unknown_error
- SignUpError.ALREADY_EXISTS -> R.string.already_exists
- SignUpError.APPLICATION_TOO_OLD -> R.string.app_too_old
- SignUpError.TIMED_OUT -> R.string.timed_out
- SignUpError.NO_CONNECTION -> R.string.no_connection
- SignUpError.GROUP_DOES_NOT_EXISTS -> R.string.group_does_not_exists
- }
-
- onPendingSnackbar(context.getString(stringRes))
- },
- {
- loading = false
-
- appNavController.navigate("main") {
- popUpTo("auth") { inclusive = true }
- }
- }
- )
- }
-
- Column(
- modifier = Modifier.padding(20.dp),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Text(
- text = stringResource(R.string.sign_up_title),
- modifier = Modifier.padding(10.dp),
- style = MaterialTheme.typography.displaySmall,
- fontWeight = FontWeight.ExtraBold
- )
-
- Spacer(modifier = Modifier.size(10.dp))
-
- if (role != UserRole.TEACHER) {
- OutlinedTextField(
- value = username,
- singleLine = true,
- onValueChange = {
- username = it
- usernameError = false
- },
- label = { Text(stringResource(R.string.username)) },
- isError = usernameError,
- readOnly = loading
- )
- } else {
- TeacherNameSelector(
- value = username,
- isError = usernameError,
- readOnly = loading,
- onValueChange = { username = it ?: "" }
- )
- }
-
- OutlinedTextField(
- value = password,
- singleLine = true,
- visualTransformation = PasswordVisualTransformation(),
- onValueChange = {
- passwordError = false
- password = it
- },
- label = { Text(stringResource(R.string.password)) },
- isError = passwordError,
- readOnly = loading
- )
-
- Spacer(modifier = Modifier.size(10.dp))
-
- GroupSelector(
- value = group,
- isError = groupError,
- readOnly = loading,
- teacher = role == UserRole.TEACHER
- ) {
- groupError = false
- group = it
- }
-
- Spacer(modifier = Modifier.size(10.dp))
-
- RoleSelector(
- value = role,
- isError = false,
- readOnly = loading
- ) { role = it }
-
- TextButton(onClick = { navController.navigate("sign-in") }) {
- Text(text = stringResource(R.string.already_registered))
- }
-
- Button(
- enabled = !loading && group != null && !(usernameError || passwordError || groupError),
- onClick = onClick
- ) {
- Text(
- text = stringResource(R.string.proceed),
- style = MaterialTheme.typography.bodyLarge
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/TrySignIn.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/TrySignIn.kt
deleted file mode 100644
index 2c7d3e1..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/TrySignIn.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.auth
-
-import android.content.Context
-import com.android.volley.AuthFailureError
-import com.android.volley.ClientError
-import com.android.volley.NoConnectionError
-import com.android.volley.TimeoutError
-import com.google.firebase.logger.Logger
-import kotlinx.coroutines.runBlocking
-import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignIn
-import ru.n08i40k.polytechnic.next.network.unwrapException
-import ru.n08i40k.polytechnic.next.settings.settingsDataStore
-import java.util.concurrent.TimeoutException
-
-internal enum class SignInError {
- INVALID_CREDENTIALS,
- TIMED_OUT,
- NO_CONNECTION,
- APPLICATION_TOO_OLD,
- UNKNOWN
-}
-
-internal fun trySignIn(
- context: Context,
-
- username: String,
- password: String,
-
- onError: (SignInError) -> Unit,
- onSuccess: () -> Unit,
-) {
- AuthSignIn(AuthSignIn.RequestDto(username, password), context, {
- runBlocking {
- context.settingsDataStore.updateData { currentSettings ->
- currentSettings
- .toBuilder()
- .setUserId(it.id)
- .setAccessToken(it.accessToken)
- .setGroup(it.group)
- .build()
- }
- }
-
- onSuccess()
- }, {
- val error = when (val exception = unwrapException(it)) {
- is TimeoutException -> SignInError.TIMED_OUT
- is TimeoutError -> SignInError.TIMED_OUT
- is NoConnectionError -> SignInError.NO_CONNECTION
- is AuthFailureError -> SignInError.INVALID_CREDENTIALS
- is ClientError -> {
- if (exception.networkResponse.statusCode == 400)
- SignInError.APPLICATION_TOO_OLD
- else
- SignInError.UNKNOWN
- }
-
- else -> SignInError.UNKNOWN
- }
-
- if (error == SignInError.UNKNOWN) {
- Logger.getLogger("tryLogin")
- .error("Unknown exception while trying to login!", it)
- }
-
- onError(error)
- }).send()
-}
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/TrySignUp.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/TrySignUp.kt
deleted file mode 100644
index 8d60614..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/auth/TrySignUp.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.auth
-
-import android.content.Context
-import com.android.volley.ClientError
-import com.android.volley.NoConnectionError
-import com.android.volley.TimeoutError
-import com.google.firebase.logger.Logger
-import kotlinx.coroutines.runBlocking
-import ru.n08i40k.polytechnic.next.model.UserRole
-import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignUp
-import ru.n08i40k.polytechnic.next.network.unwrapException
-import ru.n08i40k.polytechnic.next.settings.settingsDataStore
-import java.util.concurrent.TimeoutException
-
-internal enum class SignUpError {
- ALREADY_EXISTS,
- GROUP_DOES_NOT_EXISTS,
- TIMED_OUT,
- NO_CONNECTION,
- APPLICATION_TOO_OLD,
- UNKNOWN
-}
-
-internal fun trySignUp(
- context: Context,
-
- username: String,
- password: String,
- group: String,
- role: UserRole,
-
- onError: (SignUpError) -> Unit,
- onSuccess: () -> Unit,
-) {
- AuthSignUp(
- AuthSignUp.RequestDto(
- username,
- password,
- group,
- role
- ), context, {
- runBlocking {
- context.settingsDataStore.updateData { currentSettings ->
- currentSettings
- .toBuilder()
- .setUserId(it.id)
- .setAccessToken(it.accessToken)
- .setGroup(group)
- .build()
- }
- }
-
- onSuccess()
- }, {
- val error = when (val exception = unwrapException(it)) {
- is TimeoutException -> SignUpError.TIMED_OUT
- is NoConnectionError -> SignUpError.NO_CONNECTION
- is TimeoutError -> SignUpError.UNKNOWN
- is ClientError -> {
- when (exception.networkResponse.statusCode) {
- 400 -> SignUpError.APPLICATION_TOO_OLD
- 404 -> SignUpError.GROUP_DOES_NOT_EXISTS
- 409 -> SignUpError.ALREADY_EXISTS
- else -> SignUpError.UNKNOWN
- }
- }
-
- else -> SignUpError.UNKNOWN
- }
-
- if (error == SignUpError.UNKNOWN) {
- Logger.getLogger("tryRegister")
- .error("Unknown exception while trying to register!", it)
- }
-
- onError(error)
- }).send()
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/helper/SnackbarBox.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/helper/SnackbarBox.kt
new file mode 100644
index 0000000..19c8696
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/helper/SnackbarBox.kt
@@ -0,0 +1,37 @@
+package ru.n08i40k.polytechnic.next.ui.helper
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import kotlinx.coroutines.launch
+
+typealias PushSnackbar = (String, SnackbarDuration) -> Unit
+
+@Composable
+fun SnackbarBox(modifier: Modifier = Modifier, content: @Composable (PushSnackbar) -> Unit) {
+ val snackbarHostState = remember { SnackbarHostState() }
+ val coroutineScope = rememberCoroutineScope()
+
+ val pushSnackbar: PushSnackbar = { msg, duration ->
+ coroutineScope.launch { snackbarHostState.showSnackbar(msg, duration = duration) }
+ }
+
+ Scaffold(
+ snackbarHost = { SnackbarHost(snackbarHostState) },
+ contentWindowInsets = WindowInsets(0),
+ ) {
+ Box(
+ modifier
+ .fillMaxSize()
+ .padding(it)) { content(pushSnackbar) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/helper/data/InputValue.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/helper/data/InputValue.kt
new file mode 100644
index 0000000..7c299aa
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/helper/data/InputValue.kt
@@ -0,0 +1,31 @@
+package ru.n08i40k.polytechnic.next.ui.helper.data
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+
+data class InputValue(
+ var value: T,
+ val errorCheck: (T) -> Boolean = { false },
+ private var checkNow: Boolean = false,
+ var isError: Boolean = false,
+) {
+
+ init {
+ if (checkNow)
+ isError = isError or errorCheck(value)
+
+ // проверки после it.apply {}
+ checkNow = true
+ }
+}
+
+@Composable
+fun rememberInputValue(
+ defaultValue: T,
+ checkNow: Boolean = false,
+ errorCheck: (T) -> Boolean = { false }
+): MutableState> {
+ return remember { mutableStateOf(InputValue(defaultValue, errorCheck, checkNow)) }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/icons/appicons/filled/VK.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/icons/appicons/filled/VK.kt
new file mode 100644
index 0000000..cb13033
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/icons/appicons/filled/VK.kt
@@ -0,0 +1,87 @@
+package ru.n08i40k.polytechnic.next.ui.icons.appicons.filled
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.group
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import ru.n08i40k.polytechnic.next.ui.icons.appicons.FilledGroup
+
+val FilledGroup.Vk: ImageVector
+ get() {
+ if (_vk != null) {
+ return _vk!!
+ }
+ _vk = Builder(
+ name = "Vk", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp, viewportWidth
+ = 101.0f, viewportHeight = 100.0f
+ ).apply {
+ group {
+ path(
+ fill = SolidColor(Color(0xFF0077FF)), stroke = null, strokeLineWidth = 0.0f,
+ strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
+ pathFillType = NonZero
+ ) {
+ moveTo(0.5f, 48.0f)
+ curveTo(0.5f, 25.37f, 0.5f, 14.06f, 7.53f, 7.03f)
+ curveTo(14.56f, 0.0f, 25.87f, 0.0f, 48.5f, 0.0f)
+ horizontalLineTo(52.5f)
+ curveTo(75.13f, 0.0f, 86.44f, 0.0f, 93.47f, 7.03f)
+ curveTo(100.5f, 14.06f, 100.5f, 25.37f, 100.5f, 48.0f)
+ verticalLineTo(52.0f)
+ curveTo(100.5f, 74.63f, 100.5f, 85.94f, 93.47f, 92.97f)
+ curveTo(86.44f, 100.0f, 75.13f, 100.0f, 52.5f, 100.0f)
+ horizontalLineTo(48.5f)
+ curveTo(25.87f, 100.0f, 14.56f, 100.0f, 7.53f, 92.97f)
+ curveTo(0.5f, 85.94f, 0.5f, 74.63f, 0.5f, 52.0f)
+ verticalLineTo(48.0f)
+ close()
+ }
+ path(
+ fill = SolidColor(Color(0xFFffffff)), stroke = null, strokeLineWidth = 0.0f,
+ strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
+ pathFillType = NonZero
+ ) {
+ moveTo(53.71f, 72.04f)
+ curveTo(30.92f, 72.04f, 17.92f, 56.42f, 17.38f, 30.42f)
+ horizontalLineTo(28.79f)
+ curveTo(29.17f, 49.5f, 37.58f, 57.58f, 44.25f, 59.25f)
+ verticalLineTo(30.42f)
+ horizontalLineTo(55.0f)
+ verticalLineTo(46.88f)
+ curveTo(61.58f, 46.17f, 68.5f, 38.67f, 70.83f, 30.42f)
+ horizontalLineTo(81.58f)
+ curveTo(79.79f, 40.58f, 72.29f, 48.08f, 66.96f, 51.17f)
+ curveTo(72.29f, 53.67f, 80.83f, 60.21f, 84.08f, 72.04f)
+ horizontalLineTo(72.25f)
+ curveTo(69.71f, 64.13f, 63.38f, 58.0f, 55.0f, 57.17f)
+ verticalLineTo(72.04f)
+ horizontalLineTo(53.71f)
+ close()
+ }
+ }
+ }
+ .build()
+ return _vk!!
+ }
+
+private var _vk: ImageVector? = null
+
+@Preview
+@Composable
+private fun Preview() {
+ Box(modifier = Modifier.padding(12.dp)) {
+ Image(imageVector = FilledGroup.Vk, contentDescription = "")
+ }
+}
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
deleted file mode 100644
index 8e06b81..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/Constants.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-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.material.icons.filled.Person
-import androidx.compose.ui.graphics.vector.ImageVector
-import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.model.UserRole
-
-data class BottomNavItem(
- @StringRes val label: Int,
- val icon: ImageVector,
- val route: String,
- val requiredRole: UserRole? = null
-)
-
-object Constants {
- val bottomNavItem = listOf(
- BottomNavItem(R.string.profile, Icons.Filled.AccountCircle, "profile"),
- BottomNavItem(R.string.replacer, Icons.Filled.Create, "replacer", UserRole.ADMIN),
- BottomNavItem(
- R.string.teacher_schedule,
- Icons.Filled.Person,
- "teacher-main-schedule",
- UserRole.TEACHER
- ),
- BottomNavItem(R.string.schedule, Icons.Filled.DateRange, "schedule"),
- BottomNavItem(
- R.string.teachers,
- Icons.Filled.Person,
- "teacher-user-schedule",
- UserRole.STUDENT
- )
- )
-}
\ 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
deleted file mode 100644
index 4db7d5c..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/MainScreen.kt
+++ /dev/null
@@ -1,357 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.main
-
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import androidx.activity.ComponentActivity
-import androidx.compose.animation.core.FastOutSlowInEasing
-import androidx.compose.animation.core.LinearOutSlowInEasing
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideIn
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Menu
-import androidx.compose.material3.Badge
-import androidx.compose.material3.BadgedBox
-import androidx.compose.material3.DropdownMenu
-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.TextButton
-import androidx.compose.material3.TopAppBar
-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.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.dp
-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
-import androidx.navigation.compose.composable
-import androidx.navigation.compose.currentBackStackEntryAsState
-import androidx.navigation.compose.rememberNavController
-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.PolytechnicApplication
-import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.model.Profile
-import ru.n08i40k.polytechnic.next.model.UserRole
-import ru.n08i40k.polytechnic.next.settings.settingsDataStore
-import ru.n08i40k.polytechnic.next.ui.icons.AppIcons
-import ru.n08i40k.polytechnic.next.ui.icons.appicons.Filled
-import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Download
-import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Telegram
-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.group.GroupScheduleScreen
-import ru.n08i40k.polytechnic.next.ui.main.schedule.teacher.main.TeacherMainScheduleScreen
-import ru.n08i40k.polytechnic.next.ui.main.schedule.teacher.user.TeacherUserScheduleScreen
-import ru.n08i40k.polytechnic.next.ui.model.GroupScheduleViewModel
-import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState
-import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel
-import ru.n08i40k.polytechnic.next.ui.model.RemoteConfigViewModel
-import ru.n08i40k.polytechnic.next.ui.model.ScheduleReplacerViewModel
-import ru.n08i40k.polytechnic.next.ui.model.TeacherScheduleViewModel
-import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
-
-
-@Composable
-private fun NavHostContainer(
- navController: NavHostController,
- padding: PaddingValues,
- profileViewModel: ProfileViewModel,
- groupScheduleViewModel: GroupScheduleViewModel,
- teacherScheduleViewModel: TeacherScheduleViewModel,
- scheduleReplacerViewModel: ScheduleReplacerViewModel?
-) {
- val context = LocalContext.current
-
- val profileUiState by profileViewModel.uiState.collectAsStateWithLifecycle()
-
- val profile: Profile? = when (profileUiState) {
- is ProfileUiState.NoProfile -> null
- is ProfileUiState.HasProfile ->
- (profileUiState as ProfileUiState.HasProfile).profile
- }
-
- if (profile == null)
- return
-
- NavHost(
- navController = navController,
- startDestination = if (profile.role == UserRole.TEACHER) "teacher-main-schedule" else "schedule",
- modifier = Modifier.padding(paddingValues = padding),
- enterTransition = {
- slideIn(
- animationSpec = tween(
- 400,
- delayMillis = 250,
- easing = LinearOutSlowInEasing
- )
- ) { fullSize -> IntOffset(0, fullSize.height / 16) } + fadeIn(
- animationSpec = tween(
- 400,
- delayMillis = 250,
- easing = LinearOutSlowInEasing
- )
- )
- },
- exitTransition = {
- fadeOut(
- animationSpec = tween(
- 250,
- easing = FastOutSlowInEasing
- )
- )
- },
- ) {
- composable("profile") {
- ProfileScreen(LocalContext.current.profileViewModel!!) { context.profileViewModel!!.refreshProfile() }
- }
-
- composable("schedule") {
- GroupScheduleScreen(groupScheduleViewModel) { groupScheduleViewModel.refresh() }
- }
-
- composable("teacher-user-schedule") {
- TeacherUserScheduleScreen(teacherScheduleViewModel) {
- if (it.isNotEmpty()) teacherScheduleViewModel.fetch(
- it
- )
- }
- }
-
- composable("teacher-main-schedule") {
- TeacherMainScheduleScreen(teacherScheduleViewModel) {
- if (it.isNotEmpty()) teacherScheduleViewModel.fetch(
- it
- )
- }
- }
-
- if (scheduleReplacerViewModel != null) {
- composable("replacer") {
- ReplacerScreen(scheduleReplacerViewModel) { scheduleReplacerViewModel.refresh() }
- }
- }
- }
-}
-
-private fun openLink(context: Context, link: String) {
- context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link)), null)
-}
-
-@Composable
-private fun LinkButton(
- text: String,
- icon: ImageVector,
- link: String,
- enabled: Boolean = true,
- badged: Boolean = false,
-) {
- val context = LocalContext.current
-
- TextButton(
- modifier = Modifier.fillMaxWidth(),
- onClick = { openLink(context, link) },
- enabled = enabled,
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically
- ) {
- BadgedBox(badge = { if (badged) Badge() }) {
- Icon(
- imageVector = icon,
- contentDescription = text
- )
- }
- Spacer(Modifier.width(5.dp))
- Text(text)
- }
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun TopNavBar(
- remoteConfigViewModel: RemoteConfigViewModel
-) {
- var dropdownExpanded by remember { mutableStateOf(false) }
- val remoteConfigUiState by remoteConfigViewModel.uiState.collectAsStateWithLifecycle()
-
- val packageVersion =
- (LocalContext.current.applicationContext as PolytechnicApplication).getAppVersion()
- val updateAvailable = remoteConfigUiState.currVersion != packageVersion
-
- TopAppBar(
- title = {
- Text(
- modifier = Modifier.fillMaxWidth(),
- text = stringResource(R.string.app_name),
- style = MaterialTheme.typography.titleLarge,
- textAlign = TextAlign.Center
- )
- },
- actions = {
- IconButton(onClick = { dropdownExpanded = true }) {
- BadgedBox(badge = { if (updateAvailable) Badge() }) {
- Icon(
- imageVector = Icons.Filled.Menu,
- contentDescription = "top app bar menu"
- )
- }
- }
- DropdownMenu(
- expanded = dropdownExpanded,
- onDismissRequest = { dropdownExpanded = false }
- ) {
- Column(modifier = Modifier.wrapContentWidth()) {
- LinkButton(
- text = stringResource(R.string.download_update),
- icon = AppIcons.Filled.Download,
- link = remoteConfigUiState.downloadLink,
- enabled = updateAvailable,
- badged = updateAvailable
- )
- LinkButton(
- text = stringResource(R.string.telegram_channel),
- icon = AppIcons.Filled.Telegram,
- link = remoteConfigUiState.telegramLink,
- )
- }
- }
- }
- )
-}
-
-@Composable
-private fun BottomNavBar(navController: NavHostController, userRole: UserRole) {
- NavigationBar {
- val navBackStackEntry by navController.currentBackStackEntryAsState()
-
- val currentRoute = navBackStackEntry?.destination?.route
-
- Constants.bottomNavItem.forEach {
- if (it.requiredRole != null && it.requiredRole != userRole && userRole != UserRole.ADMIN)
- return@forEach
-
- NavigationBarItem(
- selected = it.route == currentRoute,
- onClick = { if (it.route != currentRoute) navController.navigate(it.route) },
- icon = {
- Icon(
- imageVector = it.icon,
- contentDescription = stringResource(it.label)
- )
- },
- label = { Text(stringResource(it.label)) })
- }
- }
-}
-
-@Composable
-fun MainScreen(
- appNavController: NavHostController,
- mainViewModel: MainViewModel = hiltViewModel()
-) {
- val context = LocalContext.current
-
- LaunchedEffect(Unit) {
- val accessToken: String = runBlocking {
- context.settingsDataStore.data.map { settings -> settings.accessToken }.first()
- }
-
- if (accessToken.isEmpty()) appNavController.navigate("auth")
- }
-
- // profile view model
- val profileViewModel: ProfileViewModel =
- viewModel(
- factory = ProfileViewModel.provideFactory(
- profileRepository = mainViewModel.appContainer.profileRepository,
- onUnauthorized = {
- appNavController.navigate("auth") {
- popUpTo("main") { inclusive = true }
- }
- })
- )
- LocalContext.current.profileViewModel = profileViewModel
-
- // remote config view model
- val remoteConfigViewModel: RemoteConfigViewModel =
- viewModel(
- factory = RemoteConfigViewModel.provideFactory(
- appContext = LocalContext.current,
- remoteConfig = (LocalContext.current.applicationContext as PolytechnicApplication).container.remoteConfig
- )
- )
-
- // schedule view model
- val groupScheduleViewModel =
- hiltViewModel(LocalContext.current as ComponentActivity)
-
- // teacher view model
- val teacherScheduleViewModel =
- hiltViewModel(LocalContext.current as ComponentActivity)
-
- // schedule replacer view model
- val profileUiState by profileViewModel.uiState.collectAsStateWithLifecycle()
-
- val profile: Profile? = when (profileUiState) {
- is ProfileUiState.NoProfile -> null
- is ProfileUiState.HasProfile ->
- (profileUiState as ProfileUiState.HasProfile).profile
- }
-
- if (profile == null)
- return
-
- val scheduleReplacerViewModel: ScheduleReplacerViewModel? =
- if (profile.role == UserRole.ADMIN) hiltViewModel(LocalContext.current as ComponentActivity)
- else null
-
- // nav controller
-
- val navController = rememberNavController()
- Scaffold(
- topBar = { TopNavBar(remoteConfigViewModel) },
- bottomBar = { BottomNavBar(navController, profile.role) }
- ) { paddingValues ->
- NavHostContainer(
- navController,
- paddingValues,
- profileViewModel,
- groupScheduleViewModel,
- teacherScheduleViewModel,
- scheduleReplacerViewModel
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ChangePasswordDialog.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ChangePasswordDialog.kt
deleted file mode 100644
index b190a11..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ChangePasswordDialog.kt
+++ /dev/null
@@ -1,135 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.main.profile
-
-import android.content.Context
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.Button
-import androidx.compose.material3.Card
-import androidx.compose.material3.OutlinedTextField
-import androidx.compose.material3.Text
-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.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.input.PasswordVisualTransformation
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.window.Dialog
-import com.android.volley.AuthFailureError
-import com.android.volley.ClientError
-import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
-import ru.n08i40k.polytechnic.next.model.Profile
-import ru.n08i40k.polytechnic.next.network.request.auth.AuthChangePassword
-
-private enum class ChangePasswordError {
- INCORRECT_CURRENT_PASSWORD,
- SAME_PASSWORDS
-}
-
-private fun tryChangePassword(
- context: Context,
- oldPassword: String,
- newPassword: String,
- onError: (ChangePasswordError) -> Unit,
- onSuccess: () -> Unit
-) {
- AuthChangePassword(AuthChangePassword.RequestDto(oldPassword, newPassword), context, {
- onSuccess()
- }, {
- if (it is ClientError && it.networkResponse.statusCode == 409)
- onError(ChangePasswordError.SAME_PASSWORDS)
- else if (it is AuthFailureError)
- onError(ChangePasswordError.INCORRECT_CURRENT_PASSWORD)
- else throw it
- }).send()
-}
-
-@Preview(showBackground = true)
-@Composable
-internal fun ChangePasswordDialog(
- context: Context = LocalContext.current,
- profile: Profile = FakeProfileRepository.exampleProfile,
- onChange: () -> Unit = {},
- onDismiss: () -> Unit = {}
-) {
- Dialog(onDismissRequest = onDismiss) {
- Card {
- var oldPassword by remember { mutableStateOf("") }
- var newPassword by remember { mutableStateOf("") }
-
- var oldPasswordError by remember { mutableStateOf(false) }
- var newPasswordError by remember { mutableStateOf(false) }
-
- var processing by remember { mutableStateOf(false) }
-
- Column(modifier = Modifier.width(IntrinsicSize.Max)) {
- val modifier = Modifier.fillMaxWidth()
-
- OutlinedTextField(
- modifier = modifier,
- value = oldPassword,
- isError = oldPasswordError,
- onValueChange = {
- oldPassword = it
- oldPasswordError = it.isEmpty()
- },
- visualTransformation = PasswordVisualTransformation(),
- label = { Text(text = stringResource(R.string.old_password)) },
- readOnly = processing
- )
- OutlinedTextField(
- modifier = modifier,
- value = newPassword,
- isError = newPasswordError,
- onValueChange = {
- newPassword = it
- newPasswordError = it.isEmpty() || newPassword == oldPassword
- },
- visualTransformation = PasswordVisualTransformation(),
- label = { Text(text = stringResource(R.string.new_password)) },
- readOnly = processing
- )
-
- val focusManager = LocalFocusManager.current
- Button(
- modifier = modifier,
- onClick = {
- processing = true
- focusManager.clearFocus()
-
- tryChangePassword(
- context = context,
- oldPassword = oldPassword,
- newPassword = newPassword,
- onError = {
- when (it) {
- ChangePasswordError.SAME_PASSWORDS -> {
- oldPasswordError = true
- newPasswordError = true
- }
-
- ChangePasswordError.INCORRECT_CURRENT_PASSWORD -> {
- oldPasswordError = true
- }
- }
-
- processing = false
- },
- onSuccess = onChange
- )
- },
- enabled = !(newPasswordError || oldPasswordError || processing)
- ) {
- Text(stringResource(R.string.change_password))
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ProfileCard.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ProfileCard.kt
deleted file mode 100644
index dc4b078..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ProfileCard.kt
+++ /dev/null
@@ -1,206 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.main.profile
-
-import androidx.activity.ComponentActivity
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.AccountCircle
-import androidx.compose.material.icons.filled.Email
-import androidx.compose.material.icons.filled.Lock
-import androidx.compose.material3.Button
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-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.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.input.PasswordVisualTransformation
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
-import kotlinx.coroutines.runBlocking
-import ru.n08i40k.polytechnic.next.PolytechnicApplication
-import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
-import ru.n08i40k.polytechnic.next.model.Profile
-import ru.n08i40k.polytechnic.next.settings.settingsDataStore
-import ru.n08i40k.polytechnic.next.ui.model.GroupScheduleViewModel
-import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
-
-@Preview(showBackground = true)
-@Composable
-internal fun ProfileCard(profile: Profile = FakeProfileRepository.exampleProfile) {
- Column(
- modifier = Modifier.fillMaxSize(),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Box(modifier = Modifier.padding(20.dp)) {
- Card(
- colors = CardDefaults.cardColors(
- contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
- containerColor = MaterialTheme.colorScheme.primaryContainer
- )
- ) {
- Column(
- modifier = Modifier
- .wrapContentWidth()
- .padding(10.dp),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- val focusManager = LocalFocusManager.current
- val context = LocalContext.current
-
- var usernameChanging by remember { mutableStateOf(false) }
- var passwordChanging by remember { mutableStateOf(false) }
- var groupChanging by remember { mutableStateOf(false) }
-
- TextField(
- label = { Text(stringResource(R.string.username)) },
- value = profile.username,
- leadingIcon = {
- Icon(
- imageVector = Icons.Filled.AccountCircle,
- contentDescription = "username"
- )
- },
- readOnly = true,
- onValueChange = {},
- modifier = Modifier.onFocusChanged {
- if (it.isFocused) {
- usernameChanging = true
- focusManager.clearFocus()
- }
- },
- )
-
- TextField(
- label = { Text(stringResource(R.string.password)) },
- value = "12345678",
- visualTransformation = PasswordVisualTransformation(),
- leadingIcon = {
- Icon(
- imageVector = Icons.Filled.Lock,
- contentDescription = "password"
- )
- },
- readOnly = true,
- onValueChange = {},
- modifier = Modifier.onFocusChanged {
- if (it.isFocused) {
- passwordChanging = true
- focusManager.clearFocus()
- }
- },
- )
-
- TextField(
- label = { Text(stringResource(R.string.role)) },
- value = stringResource(profile.role.stringId),
- leadingIcon = {
- Icon(
- imageVector = profile.role.icon,
- contentDescription = "role"
- )
- },
- readOnly = true,
- onValueChange = {},
- )
-
- TextField(
- label = { Text(stringResource(R.string.group)) },
- value = profile.group,
- leadingIcon = {
- Icon(
- imageVector = Icons.Filled.Email,
- contentDescription = "group"
- )
- },
- readOnly = true,
- onValueChange = {},
- modifier = Modifier.onFocusChanged {
- if (it.isFocused) {
- groupChanging = true
- focusManager.clearFocus()
- }
- },
- )
-
- Button(onClick = {
- runBlocking {
- context.settingsDataStore.updateData {
- it
- .toBuilder()
- .setGroup("")
- .setAccessToken("")
- .setUserId("")
- .build()
- }
- }
-
- context.profileViewModel!!.onUnauthorized()
- }) {
- Text(stringResource(R.string.sign_out))
- }
-
- if (passwordChanging) {
- ChangePasswordDialog(
- context,
- profile,
- { passwordChanging = false }
- ) { passwordChanging = false }
- }
-
- if (usernameChanging) {
- ChangeUsernameDialog(
- context,
- profile,
- {
- usernameChanging = false
- context.profileViewModel!!.refreshProfile()
- }
- ) { usernameChanging = false }
- }
-
- if (groupChanging) {
- val groupScheduleViewModel =
- hiltViewModel(LocalContext.current as ComponentActivity)
-
- ChangeGroupDialog(
- context,
- profile,
- { group ->
- groupChanging = false
- runBlocking {
- context.settingsDataStore.updateData {
- it.toBuilder().setGroup(group).build()
- }
- (context.applicationContext as PolytechnicApplication)
- .container
- .networkCacheRepository
- .clear()
- }
- context.profileViewModel!!.refreshProfile {
- groupScheduleViewModel.refresh()
- }
- }
- ) { groupChanging = false }
- }
- }
- }
- }
- }
-}
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
deleted file mode 100644
index ddd021d..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/replacer/ReplacerScreen.kt
+++ /dev/null
@@ -1,245 +0,0 @@
-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.scheduleReplacer.impl.FakeScheduleReplacerRepository
-import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
-import ru.n08i40k.polytechnic.next.ui.widgets.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/LessonView.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/LessonView.kt
deleted file mode 100644
index 203ad9f..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/LessonView.kt
+++ /dev/null
@@ -1,289 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.main.schedule
-
-import androidx.compose.foundation.BorderStroke
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.wrapContentWidth
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardColors
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Dialog
-import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
-import ru.n08i40k.polytechnic.next.model.Lesson
-import ru.n08i40k.polytechnic.next.model.LessonType
-import ru.n08i40k.polytechnic.next.utils.dayMinutes
-import ru.n08i40k.polytechnic.next.utils.fmtAsClock
-
-private enum class LessonTimeFormat {
- FROM_TO, ONLY_MINUTES_DURATION
-}
-
-@Composable
-private fun fmtTime(start: Int, end: Int, format: LessonTimeFormat): ArrayList {
- return when (format) {
- LessonTimeFormat.FROM_TO -> {
- val startClock = start.fmtAsClock()
- val endClock = end.fmtAsClock()
-
- arrayListOf(startClock, endClock)
- }
-
- LessonTimeFormat.ONLY_MINUTES_DURATION -> {
- val duration = end - start
-
- arrayListOf("$duration" + stringResource(R.string.minutes))
- }
- }
-}
-
-@Preview(showBackground = true)
-@Composable
-fun LessonExtraInfo(
- lesson: Lesson = FakeScheduleRepository.exampleGroup.days[0].lessons[0],
- mutableExpanded: MutableState = mutableStateOf(true)
-) {
- Dialog(onDismissRequest = { mutableExpanded.value = false }) {
- if (lesson.type === LessonType.BREAK) {
- mutableExpanded.value = false
- return@Dialog
- }
-
- Card {
- Column(Modifier.padding(10.dp)) {
- Text(lesson.name!!)
-
- for (subGroup in lesson.subGroups) {
- val subGroups = buildString {
- append("[")
- append(subGroup.number)
- append("] ")
- append(subGroup.teacher)
- append(" - ")
- append(subGroup.cabinet)
- }
- Text(subGroups)
- }
-
- val duration = buildString {
- append(stringResource(R.string.lesson_duration))
- append(" - ")
- val duration =
- lesson.time.end.dayMinutes - lesson.time.start.dayMinutes
-
- append(duration / 60)
- append(stringResource(R.string.hours))
- append(" ")
- append(duration % 60)
- append(stringResource(R.string.minutes))
- }
- Text(duration)
- }
- }
- }
-}
-
-@Preview(showBackground = true)
-@Composable
-private fun LessonViewRow(
- lesson: Lesson = FakeScheduleRepository.exampleGroup.days[0].lessons[4],
- timeFormat: LessonTimeFormat = LessonTimeFormat.FROM_TO,
- cardColors: CardColors = CardDefaults.cardColors(),
- verticalPadding: Dp = 10.dp,
- now: Boolean = true,
-) {
- val contentColor =
- if (timeFormat == LessonTimeFormat.FROM_TO) cardColors.contentColor
- else cardColors.disabledContentColor
-
- val rangeSize =
- if (lesson.defaultRange == null) 1
- else (lesson.defaultRange[1] - lesson.defaultRange[0] + 1) * 2
-
- Box(
- if (now) Modifier.border(
- BorderStroke(
- 3.5.dp,
- Color(
- cardColors.containerColor.red * 0.5F,
- cardColors.containerColor.green * 0.5F,
- cardColors.containerColor.blue * 0.5F,
- 1F
- )
- )
- ) else Modifier
- ) {
- Row(
- modifier = Modifier.padding(10.dp, verticalPadding * rangeSize),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- val rangeString = run {
- if (lesson.defaultRange == null)
- " "
- else
- buildString {
- val same = lesson.defaultRange[0] == lesson.defaultRange[1]
-
- append(if (same) " " else lesson.defaultRange[0])
- append(if (same) lesson.defaultRange[0] else "-")
- append(if (same) " " else lesson.defaultRange[1])
- }
- }
- // 1-2
- Text(
- text = rangeString,
- fontFamily = FontFamily.Monospace,
- fontWeight = FontWeight.Bold,
- color = contentColor
- )
-
- Column(
- modifier = Modifier.fillMaxWidth(0.20f),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center
- ) {
- val formattedTime: ArrayList =
- fmtTime(lesson.time.start.dayMinutes, lesson.time.end.dayMinutes, timeFormat)
-
- // 10:20 - 11:40
- Text(
- text = formattedTime[0],
- fontFamily = FontFamily.Monospace,
- color = contentColor
- )
-
- if (formattedTime.count() > 1) {
- Text(
- text = formattedTime[1],
- fontFamily = FontFamily.Monospace,
- color = contentColor
- )
- }
- }
-
- Column(verticalArrangement = Arrangement.Center) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- Column(modifier = Modifier.weight(1f)) {
- if (lesson.type.value > LessonType.BREAK.value) {
- Text(
- text = when (lesson.type) {
- LessonType.CONSULTATION -> stringResource(R.string.lesson_type_consultation)
- LessonType.INDEPENDENT_WORK -> stringResource(R.string.lesson_type_independent_work)
- LessonType.EXAM -> stringResource(R.string.lesson_type_exam)
- LessonType.EXAM_WITH_GRADE -> stringResource(R.string.lesson_type_exam_with_grade)
- LessonType.EXAM_DEFAULT -> stringResource(R.string.lesson_type_exam_default)
- else -> throw Error("Unknown lesson type!")
- },
- fontWeight = FontWeight.Bold,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- color = contentColor
- )
- }
- Text(
- text = lesson.name ?: stringResource(R.string.lesson_break),
- fontWeight = FontWeight.Medium,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- color = contentColor
- )
-
- if (lesson.group != null) {
- Text(
- text = lesson.group,
- fontWeight = FontWeight.Medium,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- color = contentColor
- )
- }
-
- for (subGroup in lesson.subGroups) {
- Text(
- text = subGroup.teacher,
- fontWeight = FontWeight.Thin,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- color = contentColor
- )
- }
- }
-
- Column(modifier = Modifier.wrapContentWidth()) {
- if (lesson.subGroups.size != 1) {
- Text(text = "")
-
- if (lesson.group != null)
- Text(text = "")
- }
- for (subGroup in lesson.subGroups) {
- Text(
- text = subGroup.cabinet,
- maxLines = 1,
- fontWeight = FontWeight.Thin,
- fontFamily = FontFamily.Monospace,
- color = contentColor
- )
- }
- }
- }
-
- }
- }
- }
-}
-
-@Preview(showBackground = true)
-@Composable
-fun FreeLessonRow(
- lesson: Lesson = FakeScheduleRepository.exampleGroup.days[0].lessons[0],
- cardColors: CardColors = CardDefaults.cardColors(),
- now: Boolean = true
-) {
- LessonViewRow(
- lesson,
- LessonTimeFormat.ONLY_MINUTES_DURATION,
- cardColors,
- 2.5.dp,
- now
- )
-}
-
-@Preview(showBackground = true)
-@Composable
-fun LessonRow(
- lesson: Lesson = FakeScheduleRepository.exampleGroup.days[0].lessons[0],
- cardColors: CardColors = CardDefaults.cardColors(),
- now: Boolean = true,
-) {
- LessonViewRow(
- lesson,
- LessonTimeFormat.FROM_TO,
- cardColors,
- 5.dp,
- now
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/GroupScheduleScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/GroupScheduleScreen.kt
deleted file mode 100644
index 52287a4..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/GroupScheduleScreen.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.main.schedule.group
-
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-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.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleEventObserver
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.compose.LocalLifecycleOwner
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import kotlinx.coroutines.delay
-import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.data.MockAppContainer
-import ru.n08i40k.polytechnic.next.ui.main.schedule.DayPager
-import ru.n08i40k.polytechnic.next.ui.model.GroupScheduleUiState
-import ru.n08i40k.polytechnic.next.ui.model.GroupScheduleViewModel
-import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
-
-@Composable
-private fun rememberUpdatedLifecycleOwner(): LifecycleOwner {
- val lifecycleOwner = LocalLifecycleOwner.current
- return remember { lifecycleOwner }
-}
-
-@Preview(showBackground = true, showSystemUi = true)
-@Composable
-fun GroupScheduleScreen(
- groupScheduleViewModel: GroupScheduleViewModel = GroupScheduleViewModel(MockAppContainer(LocalContext.current)),
- onRefresh: () -> Unit = {}
-) {
- val uiState by groupScheduleViewModel.uiState.collectAsStateWithLifecycle()
- LaunchedEffect(uiState) {
- delay(120_000)
- onRefresh()
- }
-
- val lifecycleOwner = rememberUpdatedLifecycleOwner()
-
- DisposableEffect(lifecycleOwner) {
- val observer = LifecycleEventObserver { _, event ->
- when (event) {
- Lifecycle.Event.ON_RESUME -> {
- onRefresh()
- }
- else -> Unit
- }
- }
-
- lifecycleOwner.lifecycle.addObserver(observer)
-
- onDispose {
- lifecycleOwner.lifecycle.removeObserver(observer)
- }
- }
-
- LoadingContent(
- empty = when (uiState) {
- is GroupScheduleUiState.NoData -> uiState.isLoading
- is GroupScheduleUiState.HasData -> false
- },
- loading = uiState.isLoading,
- onRefresh = onRefresh,
- verticalArrangement = Arrangement.Top
- ) {
- when (uiState) {
- is GroupScheduleUiState.HasData -> {
- Column {
- val hasData = uiState as GroupScheduleUiState.HasData
-
- UpdateInfo(hasData.lastUpdateAt, hasData.updateDates)
- Spacer(Modifier.height(10.dp))
- DayPager(hasData.group)
- }
- }
-
- is GroupScheduleUiState.NoData -> {
- if (!uiState.isLoading) {
- TextButton(onClick = onRefresh, modifier = Modifier.fillMaxSize()) {
- Text(stringResource(R.string.reload), textAlign = TextAlign.Center)
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/main/TeacherMainScheduleScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/main/TeacherMainScheduleScreen.kt
deleted file mode 100644
index 1bed1ff..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/main/TeacherMainScheduleScreen.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher.main
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-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.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleEventObserver
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.compose.LocalLifecycleOwner
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import kotlinx.coroutines.delay
-import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.data.MockAppContainer
-import ru.n08i40k.polytechnic.next.ui.main.schedule.DayPager
-import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState
-import ru.n08i40k.polytechnic.next.ui.model.TeacherScheduleUiState
-import ru.n08i40k.polytechnic.next.ui.model.TeacherScheduleViewModel
-import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
-import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
-
-@Composable
-private fun rememberUpdatedLifecycleOwner(): LifecycleOwner {
- val lifecycleOwner = LocalLifecycleOwner.current
- return remember { lifecycleOwner }
-}
-
-@Preview(showBackground = true, showSystemUi = true)
-@Composable
-fun TeacherMainScheduleScreen(
- teacherScheduleViewModel: TeacherScheduleViewModel = TeacherScheduleViewModel(
- MockAppContainer(
- LocalContext.current
- )
- ),
- fetch: (String) -> Unit = {}
-) {
- val profileViewModel = LocalContext.current.profileViewModel!!
- val profileUiState by profileViewModel.uiState.collectAsStateWithLifecycle()
-
- if (profileUiState is ProfileUiState.NoProfile)
- return
-
- val profile = (profileUiState as ProfileUiState.HasProfile).profile
-
- var teacherName = profile.username
-
- val uiState by teacherScheduleViewModel.uiState.collectAsStateWithLifecycle()
- LaunchedEffect(uiState) {
- delay(120_000)
- fetch(teacherName)
- }
-
- val lifecycleOwner = rememberUpdatedLifecycleOwner()
-
- DisposableEffect(lifecycleOwner) {
- val observer = LifecycleEventObserver { _, event ->
- when (event) {
- Lifecycle.Event.ON_RESUME -> {
- fetch(teacherName)
- }
-
- else -> Unit
- }
- }
-
- lifecycleOwner.lifecycle.addObserver(observer)
-
- onDispose {
- lifecycleOwner.lifecycle.removeObserver(observer)
- }
- }
-
- Column(Modifier.fillMaxSize()) {
- LoadingContent(
- empty = when (uiState) {
- is TeacherScheduleUiState.NoData -> uiState.isLoading
- is TeacherScheduleUiState.HasData -> false
- },
- loading = uiState.isLoading,
- ) {
- when (uiState) {
- is TeacherScheduleUiState.HasData -> {
- Column {
- val hasData = uiState as TeacherScheduleUiState.HasData
-
- DayPager(hasData.teacher)
- }
- }
-
- is TeacherScheduleUiState.NoData -> {
- if (!uiState.isLoading) {
- Text(
- modifier = Modifier.fillMaxSize(),
- text = stringResource(R.string.teacher_not_selected),
- textAlign = TextAlign.Center
- )
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/user/SearchBox.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/user/SearchBox.kt
deleted file mode 100644
index 84814d2..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/user/SearchBox.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher.user
-
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Search
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExposedDropdownMenuBox
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MenuAnchorType
-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.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.onFocusChanged
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun SearchBox(
- title: String,
- onSearchAttempt: (String) -> Unit,
- variants: List,
-) {
- var value by remember { mutableStateOf("") }
-
- val searchableVariants =
- remember(variants.size) { variants.map { it.replace(" ", "").replace(".", "").lowercase() } }
- val filteredVariants = remember(searchableVariants, value) {
- searchableVariants.filter { it.contains(value) }
- }
-
- var dropdownExpanded by remember { mutableStateOf(false) }
-
- ExposedDropdownMenuBox(
- expanded = dropdownExpanded,
- onExpandedChange = {}
- ) {
- Row(modifier = Modifier.fillMaxWidth()) {
- TextField(
- modifier = Modifier
- .fillMaxWidth()
- .onFocusChanged {
- if (it.hasFocus)
- dropdownExpanded = true
- }
- .menuAnchor(MenuAnchorType.PrimaryEditable, true),
- label = { Text(title) },
- value = value,
- onValueChange = {
- value = it
- dropdownExpanded = true
- },
- trailingIcon = {
- IconButton(onClick = { onSearchAttempt(value) }) {
- Icon(
- imageVector = Icons.Filled.Search,
- contentDescription = "Search"
- )
- }
- },
- singleLine = true,
- )
- }
-
- ExposedDropdownMenu(
- expanded = dropdownExpanded,
- onDismissRequest = { dropdownExpanded = false }
- ) {
- filteredVariants.forEach {
- val fullVariant = variants[searchableVariants.indexOf(it)]
-
- DropdownMenuItem(
- text = { Text(fullVariant) },
- onClick = {
- value = fullVariant
- onSearchAttempt(value)
-
- dropdownExpanded = false
- }
- )
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/user/TeacherSearchBox.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/user/TeacherSearchBox.kt
deleted file mode 100644
index 5638c9c..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/user/TeacherSearchBox.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher.user
-
-import android.content.Context
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetTeacherNames
-
-@Composable
-private fun getTeacherNames(context: Context): ArrayList {
- val teacherNames = remember { arrayListOf() }
-
- LaunchedEffect(teacherNames) {
- ScheduleGetTeacherNames(context, {
- teacherNames.clear()
- teacherNames.addAll(it.names)
- }, {
- teacherNames.clear()
- }).send()
- }
-
- return teacherNames
-}
-
-@Preview(showBackground = true)
-@Composable
-fun TeacherSearchBox(
- onSearchAttempt: (String) -> Unit = {},
-) {
- val teachers = getTeacherNames(LocalContext.current)
- val focusManager = LocalFocusManager.current
-
- SearchBox(
- stringResource(R.string.teacher_name),
- {
- focusManager.clearFocus(true)
- onSearchAttempt(it)
- },
- teachers,
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/user/TeacherUserScheduleScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/user/TeacherUserScheduleScreen.kt
deleted file mode 100644
index dda37b2..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/teacher/user/TeacherUserScheduleScreen.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher.user
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-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.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleEventObserver
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.compose.LocalLifecycleOwner
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import kotlinx.coroutines.delay
-import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.data.MockAppContainer
-import ru.n08i40k.polytechnic.next.ui.main.schedule.DayPager
-import ru.n08i40k.polytechnic.next.ui.model.TeacherScheduleUiState
-import ru.n08i40k.polytechnic.next.ui.model.TeacherScheduleViewModel
-import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
-
-@Composable
-private fun rememberUpdatedLifecycleOwner(): LifecycleOwner {
- val lifecycleOwner = LocalLifecycleOwner.current
- return remember { lifecycleOwner }
-}
-
-@Preview(showBackground = true, showSystemUi = true)
-@Composable
-fun TeacherUserScheduleScreen(
- teacherScheduleViewModel: TeacherScheduleViewModel = TeacherScheduleViewModel(
- MockAppContainer(
- LocalContext.current
- )
- ),
- fetch: (String) -> Unit = {}
-) {
- var teacherName by remember { mutableStateOf("") }
-
- val uiState by teacherScheduleViewModel.uiState.collectAsStateWithLifecycle()
- LaunchedEffect(uiState) {
- delay(120_000)
- fetch(teacherName)
- }
-
- val lifecycleOwner = rememberUpdatedLifecycleOwner()
-
- DisposableEffect(lifecycleOwner) {
- val observer = LifecycleEventObserver { _, event ->
- when (event) {
- Lifecycle.Event.ON_RESUME -> {
- fetch(teacherName)
- }
-
- else -> Unit
- }
- }
-
- lifecycleOwner.lifecycle.addObserver(observer)
-
- onDispose {
- lifecycleOwner.lifecycle.removeObserver(observer)
- }
- }
-
- Column(Modifier.fillMaxSize()) {
- TeacherSearchBox(onSearchAttempt = {
- teacherName = it
- fetch(it)
- })
-
- Spacer(Modifier.height(10.dp))
-
- LoadingContent(
- empty = when (uiState) {
- is TeacherScheduleUiState.NoData -> uiState.isLoading
- is TeacherScheduleUiState.HasData -> false
- },
- loading = uiState.isLoading,
- ) {
- when (uiState) {
- is TeacherScheduleUiState.HasData -> {
- Column {
- val hasData = uiState as TeacherScheduleUiState.HasData
-
- DayPager(hasData.teacher)
- }
- }
-
- is TeacherScheduleUiState.NoData -> {
- if (!uiState.isLoading) {
- Text(
- modifier = Modifier.fillMaxSize(),
- text = stringResource(R.string.teacher_not_selected),
- textAlign = TextAlign.Center
- )
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/GroupScheduleViewModel.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/GroupViewModel.kt
similarity index 54%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/GroupScheduleViewModel.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/GroupViewModel.kt
index c81575b..e3d9c4e 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/GroupScheduleViewModel.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/GroupViewModel.kt
@@ -10,77 +10,76 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.n08i40k.polytechnic.next.UpdateDates
-import ru.n08i40k.polytechnic.next.data.AppContainer
-import ru.n08i40k.polytechnic.next.data.MyResult
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
+import ru.n08i40k.polytechnic.next.utils.MyResult
import java.util.Date
import javax.inject.Inject
-sealed interface GroupScheduleUiState {
+sealed interface GroupUiState {
val isLoading: Boolean
data class NoData(
override val isLoading: Boolean
- ) : GroupScheduleUiState
+ ) : GroupUiState
data class HasData(
val group: GroupOrTeacher,
val updateDates: UpdateDates,
val lastUpdateAt: Long,
override val isLoading: Boolean
- ) : GroupScheduleUiState
+ ) : GroupUiState
}
-private data class GroupScheduleViewModelState(
+private data class GroupViewModelState(
val group: GroupOrTeacher? = null,
val updateDates: UpdateDates? = null,
val lastUpdateAt: Long = 0,
val isLoading: Boolean = false
) {
- fun toUiState(): GroupScheduleUiState = if (group == null) {
- GroupScheduleUiState.NoData(isLoading)
- } else {
- GroupScheduleUiState.HasData(group, updateDates!!, lastUpdateAt, isLoading)
- }
+ fun toUiState(): GroupUiState =
+ if (group == null)
+ GroupUiState.NoData(isLoading)
+ else
+ GroupUiState.HasData(group, updateDates!!, lastUpdateAt, isLoading)
}
@HiltViewModel
-class GroupScheduleViewModel @Inject constructor(
+class GroupViewModel @Inject constructor(
appContainer: AppContainer
) : ViewModel() {
private val scheduleRepository = appContainer.scheduleRepository
private val networkCacheRepository = appContainer.networkCacheRepository
- private val viewModelState = MutableStateFlow(GroupScheduleViewModelState(isLoading = true))
- val uiState = viewModelState
- .map(GroupScheduleViewModelState::toUiState)
- .stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState())
+ private val state = MutableStateFlow(GroupViewModelState(isLoading = true))
+
+ val uiState = state
+ .map(GroupViewModelState::toUiState)
+ .stateIn(viewModelScope, SharingStarted.Companion.Eagerly, state.value.toUiState())
init {
refresh()
}
fun refresh() {
- viewModelState.update { it.copy(isLoading = true) }
+ state.update { it.copy(isLoading = true) }
viewModelScope.launch {
val result = scheduleRepository.getGroup()
- viewModelState.update {
+ state.update {
when (result) {
- is MyResult.Success -> {
- val updateDates = networkCacheRepository.getUpdateDates()
-
- it.copy(
- group = result.data,
- updateDates = updateDates,
- lastUpdateAt = Date().time,
- isLoading = false
- )
- }
+ is MyResult.Success -> it.copy(
+ group = result.data,
+ updateDates = networkCacheRepository.getUpdateDates(),
+ lastUpdateAt = Date().time,
+ isLoading = false
+ )
is MyResult.Failure -> it.copy(
group = null,
+ updateDates = null,
+ lastUpdateAt = 0,
isLoading = false
)
}
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ProfileViewModel.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ProfileViewModel.kt
index 1533a41..e8c36a7 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ProfileViewModel.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ProfileViewModel.kt
@@ -1,32 +1,30 @@
package ru.n08i40k.polytechnic.next.ui.model
-import android.content.Context
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
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.MyResult
-import ru.n08i40k.polytechnic.next.data.users.ProfileRepository
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.model.Profile
+import ru.n08i40k.polytechnic.next.utils.MyResult
+import ru.n08i40k.polytechnic.next.utils.SingleHook
+import javax.inject.Inject
sealed interface ProfileUiState {
val isLoading: Boolean
- data class NoProfile(
+ data class NoData(
override val isLoading: Boolean
) : ProfileUiState
- data class HasProfile(
- val profile: Profile,
- override val isLoading: Boolean
+ data class HasData(
+ override val isLoading: Boolean,
+ val profile: Profile
) : ProfileUiState
}
@@ -34,59 +32,53 @@ private data class ProfileViewModelState(
val profile: Profile? = null,
val isLoading: Boolean = false
) {
- fun toUiState(): ProfileUiState = if (profile == null) {
- ProfileUiState.NoProfile(isLoading)
- } else {
- ProfileUiState.HasProfile(profile, isLoading)
+ fun toUiState(): ProfileUiState = when (profile) {
+ null -> ProfileUiState.NoData(isLoading)
+ else -> ProfileUiState.HasData(isLoading, profile)
}
}
-
-class ProfileViewModel(
- private val profileRepository: ProfileRepository,
- val onUnauthorized: () -> Unit
+@HiltViewModel
+class ProfileViewModel @Inject constructor(
+ appContainer: AppContainer
) : ViewModel() {
- private val viewModelState = MutableStateFlow(ProfileViewModelState(isLoading = true))
+ private val repository = appContainer.profileRepository
- val uiState = viewModelState
+ private val state = MutableStateFlow(ProfileViewModelState(isLoading = true))
+
+ val uiState = state
.map(ProfileViewModelState::toUiState)
- .stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState())
+ .stateIn(viewModelScope, SharingStarted.Eagerly, state.value.toUiState())
init {
- refreshProfile()
+ refresh()
}
- fun refreshProfile(callback: () -> Unit = {}) {
- viewModelState.update { it.copy(isLoading = true) }
+ // TODO: сделать хук на unauthorized и сделать так что бы waiter удалялся, если сход контекст
+
+ fun refresh(): SingleHook {
+ val singleHook = SingleHook()
+
+ state.update { it.copy(isLoading = true) }
viewModelScope.launch {
- val result = profileRepository.getProfile()
-
- viewModelState.update {
- when (result) {
- is MyResult.Success -> it.copy(profile = result.data, isLoading = false)
- is MyResult.Failure -> it.copy(profile = null, isLoading = false)
+ repository.getProfile().let { result ->
+ state.update {
+ when (result) {
+ is MyResult.Failure -> it.copy(null, false)
+ is MyResult.Success -> it.copy(result.data, false)
+ }
}
- }
- callback()
+ singleHook.resolve(
+ if (result is MyResult.Success)
+ result.data
+ else
+ null
+ )
+ }
}
- }
- companion object {
- fun provideFactory(
- profileRepository: ProfileRepository,
- onUnauthorized: () -> Unit
- ): ViewModelProvider.Factory =
- object : ViewModelProvider.Factory {
- override fun create(modelClass: Class): T {
- @Suppress("UNCHECKED_CAST") return ProfileViewModel(
- profileRepository,
- onUnauthorized
- ) as T
- }
- }
+ return singleHook
}
-}
-
-var Context.profileViewModel: ProfileViewModel? by mutableStateOf(null)
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/RemoteConfigViewModel.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/RemoteConfigViewModel.kt
index 22d3eef..4268440 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/RemoteConfigViewModel.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/RemoteConfigViewModel.kt
@@ -1,19 +1,18 @@
package ru.n08i40k.polytechnic.next.ui.model
-import android.content.Context
import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.google.firebase.remoteconfig.ConfigUpdate
import com.google.firebase.remoteconfig.ConfigUpdateListener
-import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.FirebaseRemoteConfigException
+import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
-import ru.n08i40k.polytechnic.next.ui.MainActivity
+import ru.n08i40k.polytechnic.next.app.AppContainer
import java.util.logging.Logger
+import javax.inject.Inject
data class RemoteConfigUiState(
val minVersion: String,
@@ -24,12 +23,13 @@ data class RemoteConfigUiState(
val linkUpdateDelay: Long,
)
-
-class RemoteConfigViewModel(
- private val appContext: Context,
- private val remoteConfig: FirebaseRemoteConfig,
+@HiltViewModel
+class RemoteConfigViewModel @Inject constructor(
+ appContainer: AppContainer
) : ViewModel() {
- private val viewModelState = MutableStateFlow(
+ private val remoteConfig = appContainer.remoteConfig
+
+ private val state = MutableStateFlow(
RemoteConfigUiState(
minVersion = remoteConfig.getString("minVersion"),
currVersion = remoteConfig.getString("currVersion"),
@@ -40,17 +40,14 @@ class RemoteConfigViewModel(
)
)
- val uiState = viewModelState
- .stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value)
+ val uiState = state
+ .stateIn(viewModelScope, SharingStarted.Eagerly, state.value)
init {
- (appContext as MainActivity)
- .scheduleLinkUpdate(viewModelState.value.linkUpdateDelay)
-
remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener {
override fun onUpdate(configUpdate: ConfigUpdate) {
remoteConfig.activate().addOnCompleteListener {
- viewModelState.update {
+ state.update {
it.copy(
minVersion = remoteConfig.getString("minVersion"),
currVersion = remoteConfig.getString("currVersion"),
@@ -60,8 +57,6 @@ class RemoteConfigViewModel(
linkUpdateDelay = remoteConfig.getLong("linkUpdateDelay"),
)
}
-
- appContext.scheduleLinkUpdate(viewModelState.value.linkUpdateDelay)
}
}
@@ -71,19 +66,4 @@ class RemoteConfigViewModel(
}
})
}
-
- companion object {
- fun provideFactory(
- appContext: Context,
- remoteConfig: FirebaseRemoteConfig,
- ): ViewModelProvider.Factory =
- object : ViewModelProvider.Factory {
- override fun create(modelClass: Class): T {
- @Suppress("UNCHECKED_CAST") return RemoteConfigViewModel(
- appContext,
- remoteConfig,
- ) as T
- }
- }
- }
-}
\ No newline at end of file
+}
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
deleted file mode 100644
index 6ad8245..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/ScheduleReplacerViewModel.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-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/TeacherScheduleViewModel.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/SearchViewModel.kt
similarity index 51%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/TeacherScheduleViewModel.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/SearchViewModel.kt
index 1bddf3a..0aab3ac 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/TeacherScheduleViewModel.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/SearchViewModel.kt
@@ -10,89 +10,95 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.n08i40k.polytechnic.next.UpdateDates
-import ru.n08i40k.polytechnic.next.data.AppContainer
-import ru.n08i40k.polytechnic.next.data.MyResult
+import ru.n08i40k.polytechnic.next.app.AppContainer
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
+import ru.n08i40k.polytechnic.next.utils.MyResult
import java.util.Date
import javax.inject.Inject
-sealed interface TeacherScheduleUiState {
+sealed interface SearchUiState {
val isLoading: Boolean
data class NoData(
override val isLoading: Boolean
- ) : TeacherScheduleUiState
+ ) : SearchUiState
data class HasData(
val teacher: GroupOrTeacher,
val updateDates: UpdateDates,
val lastUpdateAt: Long,
override val isLoading: Boolean
- ) : TeacherScheduleUiState
+ ) : SearchUiState
}
-private data class TeacherScheduleViewModelState(
+private data class SearchViewModelState(
val teacher: GroupOrTeacher? = null,
val updateDates: UpdateDates? = null,
val lastUpdateAt: Long = 0,
val isLoading: Boolean = false
) {
- fun toUiState(): TeacherScheduleUiState = if (teacher == null) {
- TeacherScheduleUiState.NoData(isLoading)
- } else {
- TeacherScheduleUiState.HasData(teacher, updateDates!!, lastUpdateAt, isLoading)
- }
+ fun toUiState(): SearchUiState =
+ if (teacher == null) SearchUiState.NoData(isLoading)
+ else SearchUiState.HasData(teacher, updateDates!!, lastUpdateAt, isLoading)
}
@HiltViewModel
-class TeacherScheduleViewModel @Inject constructor(
+class SearchViewModel @Inject constructor(
appContainer: AppContainer
) : ViewModel() {
private val scheduleRepository = appContainer.scheduleRepository
private val networkCacheRepository = appContainer.networkCacheRepository
- private val viewModelState = MutableStateFlow(TeacherScheduleViewModelState(isLoading = true))
- val uiState = viewModelState
- .map(TeacherScheduleViewModelState::toUiState)
- .stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState())
+ private val state = MutableStateFlow(SearchViewModelState(isLoading = true))
+
+ val uiState = state
+ .map(SearchViewModelState::toUiState)
+ .stateIn(viewModelScope, SharingStarted.Eagerly, state.value.toUiState())
+
+ private var teacherName: String? = null
init {
- fetch(null)
+ refresh()
}
- fun fetch(name: String?) {
- if (name == null) {
- viewModelState.update {
+ fun set(name: String?) {
+ teacherName = name
+ refresh()
+ }
+
+ fun refresh() {
+ state.update { it.copy(isLoading = true) }
+
+ if (teacherName == null) {
+ state.update {
it.copy(
teacher = null,
+ updateDates = null,
+ lastUpdateAt = 0,
isLoading = false
)
}
return
}
- viewModelState.update { it.copy(isLoading = true) }
-
viewModelScope.launch {
- val result = scheduleRepository.getTeacher(name)
-
- viewModelState.update {
- when (result) {
- is MyResult.Success -> {
- val updateDates = networkCacheRepository.getUpdateDates()
-
- it.copy(
+ scheduleRepository.getTeacher(teacherName!!).let { result ->
+ state.update {
+ when (result) {
+ is MyResult.Success -> it.copy(
teacher = result.data,
- updateDates = updateDates,
+ updateDates = networkCacheRepository.getUpdateDates(),
lastUpdateAt = Date().time,
isLoading = false
)
- }
- is MyResult.Failure -> it.copy(
- teacher = null,
- isLoading = false
- )
+ is MyResult.Failure -> it.copy(
+ teacher = null,
+ updateDates = null,
+ lastUpdateAt = 0,
+ isLoading = false
+ )
+ }
}
}
}
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/TeacherViewModel.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/TeacherViewModel.kt
new file mode 100644
index 0000000..664cfe6
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/model/TeacherViewModel.kt
@@ -0,0 +1,87 @@
+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.UpdateDates
+import ru.n08i40k.polytechnic.next.app.AppContainer
+import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
+import ru.n08i40k.polytechnic.next.utils.MyResult
+import javax.inject.Inject
+
+sealed interface TeacherUiState {
+ val isLoading: Boolean
+
+ data class NoData(
+ override val isLoading: Boolean
+ ) : TeacherUiState
+
+ data class HasData(
+ val teacher: GroupOrTeacher,
+ val updateDates: UpdateDates,
+ val lastUpdateAt: Long,
+ override val isLoading: Boolean
+ ) : TeacherUiState
+}
+
+private data class TeacherViewModelState(
+ val teacher: GroupOrTeacher? = null,
+ val updateDates: UpdateDates? = null,
+ val lastUpdateAt: Long = 0,
+ val isLoading: Boolean = false
+) {
+ fun toUiState(): TeacherUiState = when (teacher) {
+ null -> TeacherUiState.NoData(isLoading)
+ else -> TeacherUiState.HasData(teacher, updateDates!!, lastUpdateAt, isLoading)
+ }
+}
+
+@HiltViewModel
+class TeacherViewModel @Inject constructor(
+ appContainer: AppContainer
+) : ViewModel() {
+ private val scheduleRepository = appContainer.scheduleRepository
+ private val networkCacheRepository = appContainer.networkCacheRepository
+
+ private val state = MutableStateFlow(TeacherViewModelState(isLoading = true))
+
+ val uiState = state
+ .map { it.toUiState() }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), state.value.toUiState())
+
+ init {
+ refresh()
+ }
+
+ fun refresh() {
+ state.update { it.copy(isLoading = true) }
+
+ viewModelScope.launch {
+ scheduleRepository.getTeacher("self").let { result ->
+ state.update {
+ when (result) {
+ is MyResult.Success -> it.copy(
+ teacher = result.data,
+ updateDates = networkCacheRepository.getUpdateDates(),
+ lastUpdateAt = System.currentTimeMillis(),
+ isLoading = false
+ )
+
+ is MyResult.Failure -> it.copy(
+ teacher = null,
+ updateDates = null,
+ lastUpdateAt = 0,
+ isLoading = false
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/navigation/BottomNavBar.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/navigation/BottomNavBar.kt
new file mode 100644
index 0000000..fca6b31
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/navigation/BottomNavBar.kt
@@ -0,0 +1,37 @@
+package ru.n08i40k.polytechnic.next.ui.navigation
+
+import androidx.annotation.StringRes
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.currentBackStackEntryAsState
+
+data class BottomNavItem(
+ @StringRes val label: Int,
+ val icon: ImageVector,
+ val route: String,
+)
+
+@Composable
+fun BottomNavBar(navHostController: NavHostController, items: List) {
+ NavigationBar {
+ val navBackStackEntry by navHostController.currentBackStackEntryAsState()
+
+ val currentRoute = navBackStackEntry?.destination?.route
+
+ items.forEach {
+ NavigationBarItem(
+ selected = it.route == currentRoute,
+ onClick = { if (it.route != currentRoute) navHostController.navigate(it.route) },
+ icon = { Icon(it.icon, stringResource(it.label)) },
+ label = { Text(stringResource(it.label)) }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/navigation/NavHostContainer.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/navigation/NavHostContainer.kt
new file mode 100644
index 0000000..52a7d8f
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/navigation/NavHostContainer.kt
@@ -0,0 +1,64 @@
+package ru.n08i40k.polytechnic.next.ui.navigation
+
+import androidx.compose.animation.AnimatedContentTransitionScope
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.SizeTransform
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideIn
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.IntOffset
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+
+@Composable
+fun NavHostContainer(
+ navHostController: NavHostController,
+ padding: PaddingValues,
+ startDestination: String,
+ routes: Map Unit>,
+ enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition)? = null,
+ exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition)? = null,
+ sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = null,
+) {
+ NavHost(
+ navController = navHostController,
+ modifier = Modifier.padding(padding),
+ startDestination = startDestination,
+ enterTransition = enterTransition ?: {
+ slideIn(
+ animationSpec = tween(
+ 400,
+ delayMillis = 250,
+ easing = LinearOutSlowInEasing
+ )
+ ) { fullSize -> IntOffset(0, fullSize.height / 16) } + fadeIn(
+ animationSpec = tween(
+ 400,
+ delayMillis = 250,
+ easing = LinearOutSlowInEasing
+ )
+ )
+ },
+ exitTransition = exitTransition ?: {
+ fadeOut(
+ animationSpec = tween(
+ 250,
+ easing = FastOutSlowInEasing
+ )
+ )
+ },
+ sizeTransform = sizeTransform
+ ) {
+ routes.forEach { route -> composable(route.key) { route.value() } }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/navigation/TopAppBar.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/navigation/TopAppBar.kt
new file mode 100644
index 0000000..978c854
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/navigation/TopAppBar.kt
@@ -0,0 +1,57 @@
+package ru.n08i40k.polytechnic.next.ui.navigation
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material3.Badge
+import androidx.compose.material3.BadgedBox
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+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.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import ru.n08i40k.polytechnic.next.R
+
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TopAppBar(badge: Boolean, items: List<@Composable ColumnScope.() -> Unit>) {
+ var dropdownExpanded by remember { mutableStateOf(false) }
+
+ TopAppBar(
+ title = {
+ Text(
+ stringResource(R.string.app_name),
+ Modifier.fillMaxWidth(),
+ style = MaterialTheme.typography.titleLarge,
+ textAlign = TextAlign.Center
+ )
+ },
+ actions = {
+ IconButton({ dropdownExpanded = true }) {
+ BadgedBox({ if (badge) Badge() }) {
+ Icon(Icons.Filled.Menu, stringResource(R.string.cd_top_app_bar))
+ }
+ }
+
+ DropdownMenu(dropdownExpanded, { dropdownExpanded = false }) {
+ Column(Modifier.wrapContentWidth()) {
+ items.forEach { it() }
+ }
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/MainScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/MainScreen.kt
new file mode 100644
index 0000000..108f8a4
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/MainScreen.kt
@@ -0,0 +1,267 @@
+package ru.n08i40k.polytechnic.next.ui.screen
+
+import android.content.Context
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.LocalActivity
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.width
+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.material.icons.filled.Person
+import androidx.compose.material3.Badge
+import androidx.compose.material3.BadgedBox
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.navigation.NavController
+import androidx.navigation.compose.rememberNavController
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
+import ru.n08i40k.polytechnic.next.Application
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.model.UserRole
+import ru.n08i40k.polytechnic.next.settings.settings
+import ru.n08i40k.polytechnic.next.ui.AppRoute
+import ru.n08i40k.polytechnic.next.ui.icons.AppIcons
+import ru.n08i40k.polytechnic.next.ui.icons.appicons.Filled
+import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Download
+import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Telegram
+import ru.n08i40k.polytechnic.next.ui.model.GroupViewModel
+import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState
+import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel
+import ru.n08i40k.polytechnic.next.ui.model.RemoteConfigUiState
+import ru.n08i40k.polytechnic.next.ui.model.RemoteConfigViewModel
+import ru.n08i40k.polytechnic.next.ui.model.SearchViewModel
+import ru.n08i40k.polytechnic.next.ui.model.TeacherViewModel
+import ru.n08i40k.polytechnic.next.ui.navigation.BottomNavBar
+import ru.n08i40k.polytechnic.next.ui.navigation.BottomNavItem
+import ru.n08i40k.polytechnic.next.ui.navigation.NavHostContainer
+import ru.n08i40k.polytechnic.next.ui.navigation.TopAppBar
+import ru.n08i40k.polytechnic.next.ui.screen.profile.ProfileScreen
+import ru.n08i40k.polytechnic.next.ui.screen.replacer.ReplacerScreen
+import ru.n08i40k.polytechnic.next.ui.screen.schedule.GroupScheduleScreen
+import ru.n08i40k.polytechnic.next.ui.screen.schedule.TeacherScheduleScreen
+import ru.n08i40k.polytechnic.next.ui.screen.schedule.TeacherSearchScreen
+import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
+import ru.n08i40k.polytechnic.next.utils.openLink
+
+private data class MainBottomNavItem(
+ val bottomNavItem: BottomNavItem,
+ val requiredRole: UserRole?
+)
+
+private enum class MainScreenRoute(val route: String) {
+ PROFILE("profile"),
+ REPLACER("replacer"),
+ TEACHER_SCHEDULE("teacher-schedule"),
+ GROUP_SCHEDULE("group-schedule"),
+ TEACHER_SEARCH("teacher-search")
+}
+
+private val mainNavBarItems = listOf(
+ MainBottomNavItem(
+ BottomNavItem(
+ R.string.profile,
+ Icons.Filled.AccountCircle,
+ MainScreenRoute.PROFILE.route
+ ),
+ null
+ ),
+ MainBottomNavItem(
+ BottomNavItem(
+ R.string.replacer,
+ Icons.Filled.Create,
+ MainScreenRoute.REPLACER.route
+ ),
+ UserRole.ADMIN
+ ),
+ MainBottomNavItem(
+ BottomNavItem(
+ R.string.teacher_schedule,
+ Icons.Filled.Person,
+ MainScreenRoute.TEACHER_SCHEDULE.route
+ ),
+ UserRole.TEACHER
+ ),
+ MainBottomNavItem(
+ BottomNavItem(
+ R.string.group_schedule,
+ Icons.Filled.DateRange,
+ MainScreenRoute.GROUP_SCHEDULE.route
+ ),
+ null
+ ),
+ MainBottomNavItem(
+ BottomNavItem(
+ R.string.teachers_schedule,
+ Icons.Filled.Person,
+ MainScreenRoute.TEACHER_SEARCH.route
+ ),
+ UserRole.STUDENT
+ ),
+)
+
+@Composable
+private fun LinkButton(
+ text: String,
+ icon: ImageVector,
+ link: String,
+ enabled: Boolean = true,
+ badged: Boolean = false,
+) {
+ val context = LocalContext.current
+
+ TextButton(
+ modifier = Modifier.fillMaxWidth(),
+ onClick = { context.openLink(link) },
+ enabled = enabled,
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ BadgedBox(badge = { if (badged) Badge() }) {
+ Icon(
+ imageVector = icon,
+ contentDescription = text
+ )
+ }
+ Spacer(Modifier.width(5.dp))
+ Text(text)
+ }
+ }
+}
+
+private fun topBarItems(
+ context: Context,
+ remoteConfigUiState: RemoteConfigUiState
+): Pair Unit>> {
+ val packageVersion = (context.applicationContext as Application).version
+ val updateAvailable = remoteConfigUiState.currVersion != packageVersion
+
+ return Pair Unit>>(
+ updateAvailable,
+ listOf(
+ {
+ LinkButton(
+ text = stringResource(R.string.download_update),
+ icon = AppIcons.Filled.Download,
+ link = remoteConfigUiState.downloadLink,
+ enabled = updateAvailable,
+ badged = updateAvailable
+ )
+ },
+ {
+ LinkButton(
+ text = stringResource(R.string.telegram_channel),
+ icon = AppIcons.Filled.Telegram,
+ link = remoteConfigUiState.telegramLink,
+ )
+ }
+ )
+ )
+}
+
+@Composable
+fun MainScreen(navController: NavController) {
+ val context = LocalContext.current
+
+ LaunchedEffect(context) {
+ runBlocking {
+ val accessToken = context.settings.data.map { it.accessToken }.first()
+
+ if (accessToken.isEmpty()) {
+ navController.navigate(AppRoute.AUTH.route) {
+ popUpTo(AppRoute.AUTH.route) { inclusive = true }
+ }
+ }
+ }
+ }
+
+ val viewModelStoreOwner = LocalActivity.current as ComponentActivity
+
+ val profileViewModel = hiltViewModel(viewModelStoreOwner)
+ val profileUiState by profileViewModel.uiState.collectAsStateWithLifecycle()
+
+ LoadingContent(
+ empty = false,
+ emptyContent = {},
+ loading = profileUiState.isLoading,
+ ) {
+ val profile =
+ if (profileUiState is ProfileUiState.HasData)
+ (profileUiState as ProfileUiState.HasData).profile
+ else
+ null
+
+ val role = profile?.role ?: UserRole.STUDENT
+
+ val items =
+ mainNavBarItems.filter {
+ it.requiredRole == null
+ || (role == UserRole.ADMIN
+ || it.requiredRole == role)
+ }
+
+ val groupViewModel = hiltViewModel(viewModelStoreOwner)
+
+ val remoteConfigViewModel = hiltViewModel(viewModelStoreOwner)
+ val remoteConfigUiState by remoteConfigViewModel.uiState.collectAsStateWithLifecycle()
+
+ val teacherViewModel =
+ if (role === UserRole.STUDENT)
+ null
+ else
+ hiltViewModel(viewModelStoreOwner)
+
+ val searchViewModel =
+ if (role === UserRole.TEACHER)
+ null
+ else
+ hiltViewModel(viewModelStoreOwner)
+
+ val routes = mapOf Unit>(
+ MainScreenRoute.PROFILE.route to { ProfileScreen(profileViewModel) },
+ MainScreenRoute.REPLACER.route to { ReplacerScreen() },
+ MainScreenRoute.TEACHER_SCHEDULE.route to { TeacherScheduleScreen(teacherViewModel!!) },
+ MainScreenRoute.GROUP_SCHEDULE.route to { GroupScheduleScreen(groupViewModel) },
+ MainScreenRoute.TEACHER_SEARCH.route to { TeacherSearchScreen(searchViewModel!!) },
+ )
+
+ val topAppBar = topBarItems(context, remoteConfigUiState)
+
+ val navHostController = rememberNavController()
+ Scaffold(
+ topBar = { TopAppBar(topAppBar.first, topAppBar.second) },
+ bottomBar = { BottomNavBar(navHostController, items.map { it.bottomNavItem }) }
+ ) { paddingValues ->
+ NavHostContainer(
+ navHostController,
+ paddingValues,
+ if (role == UserRole.TEACHER)
+ MainScreenRoute.TEACHER_SCHEDULE.route
+ else
+ MainScreenRoute.GROUP_SCHEDULE.route,
+ routes
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/AuthScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/AuthScreen.kt
new file mode 100644
index 0000000..9440c8a
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/AuthScreen.kt
@@ -0,0 +1,161 @@
+package ru.n08i40k.polytechnic.next.ui.screen.auth
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.slideOut
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Card
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+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.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import com.google.android.gms.tasks.OnCompleteListener
+import com.google.android.gms.tasks.Task
+import com.google.firebase.messaging.FirebaseMessaging
+import ru.n08i40k.polytechnic.next.ui.AppRoute
+import ru.n08i40k.polytechnic.next.ui.helper.PushSnackbar
+import ru.n08i40k.polytechnic.next.ui.helper.SnackbarBox
+import ru.n08i40k.polytechnic.next.worker.UpdateFCMTokenWorker
+
+
+enum class AuthRoute(val route: String) {
+ SignUp("sign-up"),
+ SignIn("sign-in"),
+}
+
+@Composable
+private fun FormWrapper(
+ onWidthChange: (Dp) -> Unit,
+ content: @Composable BoxScope.() -> Unit
+) {
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ Card(border = BorderStroke(Dp.Hairline, MaterialTheme.colorScheme.inverseSurface)) {
+ val localDensity = LocalDensity.current
+ Box(
+ Modifier
+ .padding(10.dp)
+ .onGloballyPositioned {
+ with(localDensity) {
+ onWidthChange(it.size.width.toDp())
+ }
+ }
+ /*.animateContentSize()*/,
+ content = content
+ )
+ }
+ }
+}
+
+@Composable
+private fun AuthForm(parentNavController: NavController, pushSnackbar: PushSnackbar) {
+ val navController = rememberNavController()
+ val context = LocalContext.current
+
+ val switch: () -> Unit = {
+ navController.navigate(
+ if (navController.currentDestination?.route == AuthRoute.SignUp.route)
+ AuthRoute.SignIn.route
+ else
+ AuthRoute.SignUp.route
+ )
+ }
+
+ val finish: () -> Unit = {
+ parentNavController.navigate(AppRoute.MAIN.route) {
+ popUpTo(AppRoute.AUTH.route) { inclusive = true }
+ }
+
+ FirebaseMessaging.getInstance().token.addOnCompleteListener(object :
+ OnCompleteListener {
+ override fun onComplete(token: Task) {
+ if (!token.isSuccessful)
+ return
+
+ UpdateFCMTokenWorker.schedule(context, token.result!!)
+ }
+ })
+ }
+
+ NavHost(
+ modifier = Modifier.fillMaxSize(),
+ navController = navController,
+ startDestination = "sign-up",
+ enterTransition = {
+ fadeIn(
+ animationSpec = tween(
+ durationMillis = 700,
+ delayMillis = 800,
+ easing = LinearOutSlowInEasing
+ )
+ ) + scaleIn(
+ animationSpec = tween(
+ durationMillis = 400,
+ delayMillis = 700,
+ easing = LinearOutSlowInEasing
+ )
+ )
+ },
+ exitTransition = {
+ slideOut(
+ animationSpec = tween(
+ durationMillis = 250,
+ easing = LinearEasing
+ )
+ ) { fullSize -> IntOffset(0, fullSize.height / 16) } + fadeOut(
+ animationSpec = tween(
+ durationMillis = 250,
+ easing = LinearEasing
+ )
+ )
+ },
+ ) {
+ composable(AuthRoute.SignUp.route) {
+ var width by remember { mutableStateOf(Dp.Unspecified) }
+
+ FormWrapper({ width = it }) {
+ SignUpForm(pushSnackbar, switch, finish, width)
+ }
+ }
+
+ composable(AuthRoute.SignIn.route) {
+ var width by remember { mutableStateOf(Dp.Unspecified) }
+
+ FormWrapper({ width = it }) {
+ SignInCard(pushSnackbar, switch, finish, width)
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun AuthScreen(navController: NavController) {
+ SnackbarBox {
+ Box(contentAlignment = Alignment.Center) {
+ AuthForm(navController, it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/SignInCard.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/SignInCard.kt
new file mode 100644
index 0000000..a23adb8
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/SignInCard.kt
@@ -0,0 +1,164 @@
+package ru.n08i40k.polytechnic.next.ui.screen.auth
+
+import androidx.compose.animation.SizeTransform
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.slideIn
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Create
+import androidx.compose.material3.Button
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+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.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.navigation.compose.rememberNavController
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.ui.helper.PushSnackbar
+import ru.n08i40k.polytechnic.next.ui.navigation.NavHostContainer
+import ru.n08i40k.polytechnic.next.ui.screen.auth.signin.ManualPage
+import ru.n08i40k.polytechnic.next.ui.screen.auth.signin.VKOneTap
+
+
+private enum class SignInPage(val route: String) {
+ SELECT("select"),
+ MANUAL("manual"),
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SelectSignInMethod(
+ onSelected: (SignInPage) -> Unit = {},
+ switch: () -> Unit = {},
+ toApp: () -> Unit = {},
+ pushSnackbar: PushSnackbar = { _, _ -> },
+) {
+ val modifier = Modifier.width(240.dp)
+ var vkId by remember { mutableStateOf(false) }
+
+ Column(
+ modifier,
+ verticalArrangement = Arrangement.spacedBy(5.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ stringResource(R.string.sign_in_title),
+ Modifier.padding(10.dp),
+ style = MaterialTheme.typography.displaySmall,
+ fontWeight = FontWeight.ExtraBold
+ )
+
+ Box(modifier, contentAlignment = Alignment.Center) {
+ Button({ onSelected(SignInPage.MANUAL) }, modifier, !vkId) {
+ Text(stringResource(R.string.sign_in_manual), fontWeight = FontWeight.Bold)
+ }
+ Row(modifier.padding(10.dp, 0.dp)) {
+ Icon(
+ Icons.Filled.Create,
+ stringResource(R.string.cd_manual_icon),
+ tint = MaterialTheme.colorScheme.onPrimary
+ )
+ }
+ }
+
+ VKOneTap(toApp, pushSnackbar) { vkId = it }
+
+ Box(modifier, contentAlignment = Alignment.Center) {
+ HorizontalDivider()
+ Text(
+ stringResource(R.string.or_divider),
+ Modifier.background(CardDefaults.cardColors().containerColor)
+ )
+ }
+
+ Button(switch, modifier, !vkId) {
+ Text(stringResource(R.string.sign_in_not_registered))
+ }
+ }
+}
+
+@Composable
+fun SignInCard(
+ pushSnackbar: PushSnackbar,
+ toSignUp: () -> Unit,
+ toApp: () -> Unit,
+ parentWidth: Dp,
+) {
+ val navHostController = rememberNavController()
+
+ val toSelect: () -> Unit = {
+ navHostController.navigate(SignInPage.SELECT.route) {
+ popUpTo(SignInPage.SELECT.route) { inclusive = true }
+ }
+ }
+
+ NavHostContainer(
+ navHostController,
+ PaddingValues(0.dp),
+ SignInPage.SELECT.route,
+ mapOf Unit>(
+ SignInPage.SELECT.route to {
+ SelectSignInMethod(
+ { page -> navHostController.navigate(page.route) },
+ toSignUp,
+ toApp,
+ pushSnackbar
+ )
+ },
+ SignInPage.MANUAL.route to {
+ ManualPage(
+ pushSnackbar,
+ toApp,
+ toSelect,
+ parentWidth
+ )
+ }
+ ),
+ enterTransition = {
+ slideIn(
+ animationSpec = tween(
+ 400,
+ delayMillis = 500,
+ easing = LinearOutSlowInEasing
+ )
+ ) { fullSize -> IntOffset(0, -fullSize.height / 16) } + fadeIn(
+ animationSpec = tween(
+ 400,
+ delayMillis = 500,
+ easing = LinearOutSlowInEasing
+ )
+ )
+ },
+ sizeTransform = {
+ SizeTransform { initialSize, targetSize ->
+ keyframes {
+ durationMillis = 250
+ delayMillis = 250
+ }
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/SignUpCard.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/SignUpCard.kt
new file mode 100644
index 0000000..28b5aaa
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/SignUpCard.kt
@@ -0,0 +1,164 @@
+package ru.n08i40k.polytechnic.next.ui.screen.auth
+
+import androidx.compose.animation.SizeTransform
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.keyframes
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.slideIn
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Create
+import androidx.compose.material3.Button
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+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.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.navigation.compose.rememberNavController
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.ui.helper.PushSnackbar
+import ru.n08i40k.polytechnic.next.ui.navigation.NavHostContainer
+import ru.n08i40k.polytechnic.next.ui.screen.auth.signup.ManualPage
+import ru.n08i40k.polytechnic.next.ui.screen.auth.signup.VKPage
+import ru.n08i40k.polytechnic.next.ui.widgets.OneTapComplete
+
+private enum class SignUpPage(val route: String) {
+ SELECT("select"),
+ MANUAL("manual"),
+ VK("vk")
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun SelectSignUpMethod(
+ onSelected: (SignUpPage, String?) -> Unit = { _, _ -> },
+ switch: () -> Unit = {}
+) {
+ val modifier = Modifier.width(240.dp)
+
+ Column(
+ modifier,
+ verticalArrangement = Arrangement.spacedBy(5.dp),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ stringResource(R.string.sign_up_title),
+ Modifier.padding(10.dp),
+ style = MaterialTheme.typography.displaySmall,
+ fontWeight = FontWeight.ExtraBold
+ )
+
+ Box(modifier, contentAlignment = Alignment.Center) {
+ Button({ onSelected(SignUpPage.MANUAL, null) }, modifier) {
+ Text(stringResource(R.string.sign_up_manual), fontWeight = FontWeight.Bold)
+ }
+ Row(modifier.padding(10.dp, 0.dp)) {
+ Icon(
+ Icons.Filled.Create,
+ stringResource(R.string.cd_manual_icon),
+ tint = MaterialTheme.colorScheme.onPrimary
+ )
+ }
+ }
+
+ OneTapComplete(onAuth = { onSelected(SignUpPage.VK, it) }, onFail = {})
+
+ Box(modifier, contentAlignment = Alignment.Center) {
+ HorizontalDivider()
+ Text(
+ stringResource(R.string.or_divider),
+ Modifier.background(CardDefaults.cardColors().containerColor)
+ )
+ }
+
+ Button(switch, modifier) {
+ Text(stringResource(R.string.sign_up_already_registered))
+ }
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+fun SignUpForm(
+ pushSnackbar: PushSnackbar = { msg, dur -> },
+ toSignIn: () -> Unit = {},
+ toApp: () -> Unit = {},
+ parentWidth: Dp = Dp.Unspecified,
+) {
+ val navHostController = rememberNavController()
+
+ val toSelect: () -> Unit = {
+ navHostController.navigate(SignUpPage.SELECT.route) {
+ popUpTo(SignUpPage.SELECT.route) { inclusive = true }
+ }
+ }
+
+ var accessToken by remember { mutableStateOf(null) }
+
+ NavHostContainer(
+ navHostController,
+ PaddingValues(0.dp),
+ SignUpPage.SELECT.route,
+ mapOf Unit>(
+ SignUpPage.SELECT.route to {
+ SelectSignUpMethod(
+ { page, token ->
+ navHostController.navigate(page.route)
+ accessToken = token
+ },
+ toSignIn
+ )
+ },
+ SignUpPage.MANUAL.route to {
+ ManualPage(pushSnackbar, toApp, toSelect, parentWidth)
+ },
+ SignUpPage.VK.route to {
+ VKPage(accessToken!!, pushSnackbar, toApp, toSelect, parentWidth)
+ }
+ ),
+ enterTransition = {
+ slideIn(
+ animationSpec = tween(
+ 400,
+ delayMillis = 500,
+ easing = LinearOutSlowInEasing
+ )
+ ) { fullSize -> IntOffset(0, -fullSize.height / 16) } + fadeIn(
+ animationSpec = tween(
+ 400,
+ delayMillis = 500,
+ easing = LinearOutSlowInEasing
+ )
+ )
+ },
+ sizeTransform = {
+ SizeTransform { initialSize, targetSize ->
+ keyframes {
+ durationMillis = 250
+ delayMillis = 250
+ }
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signin/ManualPage.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signin/ManualPage.kt
new file mode 100644
index 0000000..88278b4
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signin/ManualPage.kt
@@ -0,0 +1,229 @@
+package ru.n08i40k.polytechnic.next.ui.screen.auth.signin
+
+import android.content.Context
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.Text
+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.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.runBlocking
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignIn
+import ru.n08i40k.polytechnic.next.network.unwrapException
+import ru.n08i40k.polytechnic.next.settings.settings
+import ru.n08i40k.polytechnic.next.ui.helper.PushSnackbar
+import ru.n08i40k.polytechnic.next.ui.helper.data.rememberInputValue
+import java.util.logging.Logger
+
+private fun trySignIn(
+ context: Context,
+
+ username: String,
+ password: String,
+
+ onSuccess: () -> Unit,
+ onError: (SignInError) -> Unit,
+) {
+ AuthSignIn(
+ AuthSignIn.RequestDto(username, password),
+ {
+ runBlocking {
+ context.settings.updateData { settings ->
+ settings
+ .toBuilder()
+ .setUserId(it.id)
+ .setAccessToken(it.accessToken)
+ .setGroup(it.group)
+ .build()
+ }
+ }
+
+ onSuccess()
+ },
+ {
+ val error = mapError(unwrapException(it))
+
+ if (error == SignInError.UNKNOWN) {
+ val logger = Logger.getLogger("tryRegister")
+
+ logger.severe("Unknown exception while trying to register!")
+ logger.severe(it.toString())
+ }
+
+ onError(error)
+ }
+ ).send(context)
+}
+
+
+@Composable
+internal fun ManualPage(
+ pushSnackbar: PushSnackbar,
+ toApp: () -> Unit,
+ toSelect: () -> Unit,
+ parentWidth: Dp,
+) {
+ val context = LocalContext.current
+
+ var username by rememberInputValue("") { it.length < 4 }
+ var password by rememberInputValue("") { it.isEmpty() }
+
+ var invalidCredentials by remember { mutableStateOf(false) }
+
+ var loading by remember { mutableStateOf(false) }
+
+ val focusManager = LocalFocusManager.current
+
+ val onClick: () -> Unit = fun() {
+ focusManager.clearFocus(true)
+
+ loading = true
+
+ trySignIn(
+ context,
+ username.value,
+ password.value,
+ {
+ loading = false
+ toApp()
+ },
+ {
+ loading = false
+
+ when (it) {
+ SignInError.INCORRECT_CREDENTIALS -> username = username.copy(isError = true)
+ else -> Unit
+ }
+
+ pushSnackbar(getErrorMessage(context, it, false), SnackbarDuration.Long)
+ }
+ )
+ }
+
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Box(
+ Modifier.defaultMinSize(parentWidth, Dp.Unspecified),
+ contentAlignment = Alignment.Center
+ ) {
+ if (parentWidth != Dp.Unspecified) {
+ Row(Modifier.width(parentWidth)) {
+ IconButton(toSelect) {
+ Icon(
+ Icons.AutoMirrored.Default.ArrowBack,
+ stringResource(R.string.cd_back_icon)
+ )
+ }
+ }
+ }
+
+ Text(
+ stringResource(R.string.sign_in_title),
+ Modifier.padding(10.dp),
+ style = MaterialTheme.typography.displaySmall,
+ fontWeight = FontWeight.ExtraBold
+ )
+ }
+
+ val onValueChange: () -> Unit = {
+ if (invalidCredentials) {
+ invalidCredentials = false
+
+ username = username.copy(isError = false, checkNow = true)
+ password = password.copy(isError = false, checkNow = true)
+ }
+ }
+
+ OutlinedTextField(
+ username.value,
+ {
+ onValueChange()
+
+ username = username.copy(
+ it.filter { it != ' ' }.lowercase(),
+ isError = false
+ )
+ },
+ readOnly = loading,
+ label = { Text(stringResource(R.string.username)) },
+ isError = username.isError,
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(
+ KeyboardCapitalization.None,
+ autoCorrectEnabled = false,
+ KeyboardType.Ascii,
+ ImeAction.Next
+ )
+ )
+
+ OutlinedTextField(
+ password.value,
+ {
+ onValueChange()
+
+ password = password.copy(
+ it,
+ isError = false
+ )
+ },
+ readOnly = loading,
+ label = { Text(stringResource(R.string.password)) },
+ isError = password.isError,
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(
+ KeyboardCapitalization.None,
+ autoCorrectEnabled = false,
+ KeyboardType.Password,
+ ImeAction.Next
+ ),
+ visualTransformation = PasswordVisualTransformation()
+ )
+
+ if (parentWidth != Dp.Unspecified) {
+ Spacer(Modifier.height(10.dp))
+
+ val canProceed = !loading
+ && (!username.isError && username.value.isNotEmpty())
+ && (!password.isError && password.value.isNotEmpty())
+ Button(
+ onClick,
+ Modifier.width(parentWidth),
+ enabled = canProceed
+ ) {
+ Text(
+ stringResource(R.string.proceed),
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signin/SignInError.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signin/SignInError.kt
new file mode 100644
index 0000000..e7802ab
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signin/SignInError.kt
@@ -0,0 +1,62 @@
+package ru.n08i40k.polytechnic.next.ui.screen.auth.signin
+
+import android.content.Context
+import com.android.volley.ClientError
+import com.android.volley.NoConnectionError
+import com.android.volley.TimeoutError
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignIn
+import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignIn.Companion.ErrorCode
+import java.util.concurrent.TimeoutException
+
+enum class SignInError {
+ // server errors
+ INCORRECT_CREDENTIALS,
+ INVALID_VK_ACCESS_TOKEN,
+
+ // client errors
+ TIMED_OUT,
+ NO_CONNECTION,
+
+ UNKNOWN
+}
+
+fun mapError(exception: Throwable): SignInError {
+ return when (exception) {
+ is TimeoutException -> SignInError.TIMED_OUT
+ is TimeoutError -> SignInError.TIMED_OUT
+
+ is NoConnectionError -> SignInError.NO_CONNECTION
+
+ is ClientError -> {
+ if (exception.networkResponse.statusCode != 406)
+ return SignInError.UNKNOWN
+
+ val error = AuthSignIn.Companion.parseError(exception)
+
+ when (error.code) {
+ ErrorCode.INVALID_VK_ACCESS_TOKEN -> SignInError.INVALID_VK_ACCESS_TOKEN
+ ErrorCode.INCORRECT_CREDENTIALS -> SignInError.INCORRECT_CREDENTIALS
+ }
+ }
+
+ else -> SignInError.UNKNOWN
+ }
+}
+
+fun getErrorMessage(context: Context, error: SignInError, isVK: Boolean): String {
+ return context.getString(
+ when (error) {
+ SignInError.UNKNOWN -> R.string.unknown_error
+ SignInError.INVALID_VK_ACCESS_TOKEN -> R.string.auth_error_invalid_vk_access_token
+ SignInError.INCORRECT_CREDENTIALS ->
+ if (isVK)
+ R.string.auth_error_vk_not_linked
+ else
+ R.string.auth_error_incorrect_credentials
+
+ SignInError.TIMED_OUT -> R.string.timed_out
+ SignInError.NO_CONNECTION -> R.string.no_connection
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signin/VKOneTap.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signin/VKOneTap.kt
new file mode 100644
index 0000000..ca47f65
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signin/VKOneTap.kt
@@ -0,0 +1,80 @@
+package ru.n08i40k.polytechnic.next.ui.screen.auth.signin
+
+import android.content.Context
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.tooling.preview.Preview
+import kotlinx.coroutines.runBlocking
+import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignInVK
+import ru.n08i40k.polytechnic.next.network.unwrapException
+import ru.n08i40k.polytechnic.next.settings.settings
+import ru.n08i40k.polytechnic.next.ui.widgets.OneTapComplete
+import java.util.logging.Logger
+
+private fun trySignIn(
+ context: Context,
+
+ accessToken: String,
+
+ onSuccess: () -> Unit,
+ onError: (SignInError) -> Unit,
+) {
+ AuthSignInVK(
+ AuthSignInVK.RequestDto(accessToken),
+ {
+ runBlocking {
+ context.settings.updateData { settings ->
+ settings
+ .toBuilder()
+ .setUserId(it.id)
+ .setAccessToken(it.accessToken)
+ .setGroup(it.group)
+ .build()
+ }
+ }
+
+ onSuccess()
+ },
+ {
+ val error = mapError(unwrapException(it))
+
+ if (error == SignInError.UNKNOWN) {
+ val logger = Logger.getLogger("trySignIn")
+
+ logger.severe("Unknown exception while trying to sign-in!")
+ logger.severe(it.toString())
+ }
+
+ onError(error)
+ }
+ ).send(context)
+}
+
+
+@Preview(showBackground = true)
+@Composable
+fun VKOneTap(
+ toApp: () -> Unit = {},
+ pushSnackbar: (String, SnackbarDuration) -> Unit = { _, _ -> },
+
+ onProcess: (Boolean) -> Unit = {}
+) {
+ val context = LocalContext.current
+
+ OneTapComplete(
+ onAuth = {
+ onProcess(true)
+
+ trySignIn(
+ context,
+ it,
+ toApp
+ ) {
+ pushSnackbar(getErrorMessage(context, it, true), SnackbarDuration.Long)
+ onProcess(false)
+ }
+ },
+ onFail = {}
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signup/ManualPage.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signup/ManualPage.kt
new file mode 100644
index 0000000..d074776
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signup/ManualPage.kt
@@ -0,0 +1,276 @@
+package ru.n08i40k.polytechnic.next.ui.screen.auth.signup
+
+import android.content.Context
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.Text
+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.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.runBlocking
+import ru.n08i40k.polytechnic.next.Application
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.model.UserRole
+import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignUp
+import ru.n08i40k.polytechnic.next.network.unwrapException
+import ru.n08i40k.polytechnic.next.settings.settings
+import ru.n08i40k.polytechnic.next.ui.helper.PushSnackbar
+import ru.n08i40k.polytechnic.next.ui.helper.data.rememberInputValue
+import ru.n08i40k.polytechnic.next.ui.widgets.selector.GroupSelector
+import ru.n08i40k.polytechnic.next.ui.widgets.selector.RoleSelector
+import ru.n08i40k.polytechnic.next.ui.widgets.selector.TeacherNameSelector
+import java.util.logging.Logger
+
+private fun trySignUp(
+ context: Context,
+
+ username: String,
+ password: String,
+ group: String,
+ role: UserRole,
+
+ onSuccess: () -> Unit,
+ onError: (SignUpError) -> Unit,
+) {
+ AuthSignUp(
+ AuthSignUp.RequestDto(
+ username,
+ password,
+ group,
+ role,
+ (context.applicationContext as Application).version
+ ),
+ {
+ runBlocking {
+ context.settings.updateData { settings ->
+ settings
+ .toBuilder()
+ .setUserId(it.id)
+ .setAccessToken(it.accessToken)
+ .setGroup(group)
+ .build()
+ }
+ }
+
+ onSuccess()
+ },
+ {
+ val error = mapError(unwrapException(it))
+
+ if (error == SignUpError.UNKNOWN) {
+ val logger = Logger.getLogger("tryRegister")
+
+ logger.severe("Unknown exception while trying to register!")
+ logger.severe(it.toString())
+ }
+
+ onError(error)
+ }
+ ).send(context)
+}
+
+
+@Composable
+internal fun ManualPage(
+ pushSnackbar: PushSnackbar,
+ toApp: () -> Unit,
+ toSelect: () -> Unit,
+ parentWidth: Dp,
+) {
+ val context = LocalContext.current
+
+ var username by rememberInputValue("") { it.length < 4 }
+ var password by rememberInputValue("") { it.isEmpty() }
+ var group by rememberInputValue(null) { it == null }
+ var role by remember { mutableStateOf(UserRole.STUDENT) }
+
+ var loading by remember { mutableStateOf(false) }
+
+ val focusManager = LocalFocusManager.current
+
+ val onClick: () -> Unit = fun() {
+ focusManager.clearFocus(true)
+
+ loading = true
+
+ trySignUp(
+ context,
+ username.value,
+ password.value,
+ group.value!!,
+ role,
+ {
+ loading = false
+ toApp()
+ },
+ {
+ loading = false
+
+ when (it) {
+ SignUpError.USERNAME_ALREADY_EXISTS -> username = username.copy(isError = true)
+ SignUpError.INVALID_GROUP_NAME -> group = group.copy(isError = true)
+ else -> Unit
+ }
+
+ pushSnackbar(getErrorMessage(context, it), SnackbarDuration.Long)
+ }
+ )
+ }
+
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Box(
+ Modifier.defaultMinSize(parentWidth, Dp.Unspecified),
+ contentAlignment = Alignment.Center
+ ) {
+ if (parentWidth != Dp.Unspecified) {
+ Row(Modifier.width(parentWidth)) {
+ IconButton(toSelect) {
+ Icon(
+ Icons.AutoMirrored.Default.ArrowBack,
+ stringResource(R.string.cd_back_icon)
+ )
+ }
+ }
+ }
+
+ Text(
+ stringResource(R.string.sign_up_title),
+ Modifier.padding(10.dp),
+ style = MaterialTheme.typography.displaySmall,
+ fontWeight = FontWeight.ExtraBold
+ )
+ }
+
+ when (role) {
+ UserRole.TEACHER -> {
+ TeacherNameSelector(
+ username.value, {
+ username = username.copy(
+ value = it,
+ isError = false
+ )
+ },
+ isError = username.isError,
+ readOnly = loading
+ )
+ }
+
+ UserRole.STUDENT -> {
+ OutlinedTextField(
+ username.value,
+ {
+ username = username.copy(
+ value = it.filter { it != ' ' }.lowercase(),
+ isError = false
+ )
+ },
+ readOnly = loading,
+ label = { Text(stringResource(R.string.username)) },
+ isError = username.isError,
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(
+ KeyboardCapitalization.None,
+ autoCorrectEnabled = false,
+ KeyboardType.Ascii,
+ ImeAction.Next
+ )
+ )
+ }
+
+ else -> Unit
+ }
+
+ OutlinedTextField(
+ password.value,
+ {
+ password = password.copy(
+ value = it,
+ isError = false
+ )
+ },
+ readOnly = loading,
+ label = { Text(stringResource(R.string.password)) },
+ isError = password.isError,
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(
+ KeyboardCapitalization.None,
+ autoCorrectEnabled = false,
+ KeyboardType.Password,
+ ImeAction.Next
+ ),
+ visualTransformation = PasswordVisualTransformation(),
+ )
+
+ Spacer(Modifier.height(10.dp))
+
+ GroupSelector(
+ group.value,
+ {
+ group = group.copy(
+ value = it,
+ isError = false
+ )
+ },
+ isError = group.isError,
+ readOnly = loading,
+ supervised = role == UserRole.TEACHER
+ )
+
+ Spacer(Modifier.height(10.dp))
+
+ RoleSelector(
+ role,
+ false,
+ loading
+ ) { role = it }
+
+ if (parentWidth != Dp.Unspecified) {
+ Spacer(Modifier.height(10.dp))
+
+ val canProceed = !loading
+ && (!username.isError && username.value.isNotEmpty())
+ && (!password.isError && password.value.isNotEmpty())
+ && (!group.isError && group.value != null)
+
+ Button(
+ onClick,
+ Modifier.width(parentWidth),
+ enabled = canProceed
+ ) {
+ Text(
+ stringResource(R.string.proceed),
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signup/SignUpError.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signup/SignUpError.kt
new file mode 100644
index 0000000..8ba40e5
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signup/SignUpError.kt
@@ -0,0 +1,66 @@
+package ru.n08i40k.polytechnic.next.ui.screen.auth.signup
+
+import android.content.Context
+import com.android.volley.ClientError
+import com.android.volley.NoConnectionError
+import com.android.volley.TimeoutError
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignUp
+import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignUp.Companion.ErrorCode
+import java.util.concurrent.TimeoutException
+
+enum class SignUpError {
+ // server errors
+ USERNAME_ALREADY_EXISTS,
+ VK_ALREADY_EXISTS,
+ INVALID_VK_ACCESS_TOKEN,
+ INVALID_GROUP_NAME,
+ DISALLOWED_ROLE,
+
+ // client errors
+ TIMED_OUT,
+ NO_CONNECTION,
+
+ UNKNOWN
+}
+
+fun mapError(exception: Throwable): SignUpError {
+ return when (exception) {
+ is TimeoutException -> SignUpError.TIMED_OUT
+ is TimeoutError -> SignUpError.TIMED_OUT
+
+ is NoConnectionError -> SignUpError.NO_CONNECTION
+
+ is ClientError -> {
+ if (exception.networkResponse.statusCode != 406)
+ return SignUpError.UNKNOWN
+
+ val error = AuthSignUp.Companion.parseError(exception)
+
+ when (error.code) {
+ ErrorCode.USERNAME_ALREADY_EXISTS -> SignUpError.USERNAME_ALREADY_EXISTS
+ ErrorCode.VK_ALREADY_EXISTS -> SignUpError.VK_ALREADY_EXISTS
+ ErrorCode.INVALID_VK_ACCESS_TOKEN -> SignUpError.INVALID_VK_ACCESS_TOKEN
+ ErrorCode.INVALID_GROUP_NAME -> SignUpError.INVALID_GROUP_NAME
+ ErrorCode.DISALLOWED_ROLE -> SignUpError.DISALLOWED_ROLE
+ }
+ }
+
+ else -> SignUpError.UNKNOWN
+ }
+}
+
+fun getErrorMessage(context: Context, error: SignUpError): String {
+ return context.getString(
+ when (error) {
+ SignUpError.UNKNOWN -> R.string.unknown_error
+ SignUpError.USERNAME_ALREADY_EXISTS -> R.string.auth_error_username_already_exists
+ SignUpError.VK_ALREADY_EXISTS -> R.string.auth_error_vk_already_exists
+ SignUpError.INVALID_VK_ACCESS_TOKEN -> R.string.auth_error_invalid_vk_access_token
+ SignUpError.INVALID_GROUP_NAME -> R.string.auth_error_invalid_group_name
+ SignUpError.DISALLOWED_ROLE -> R.string.auth_error_disallowed_role
+ SignUpError.TIMED_OUT -> R.string.timed_out
+ SignUpError.NO_CONNECTION -> R.string.no_connection
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signup/VKPage.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signup/VKPage.kt
new file mode 100644
index 0000000..d9b7f76
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/auth/signup/VKPage.kt
@@ -0,0 +1,253 @@
+package ru.n08i40k.polytechnic.next.ui.screen.auth.signup
+
+import android.content.Context
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.Text
+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.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.runBlocking
+import ru.n08i40k.polytechnic.next.Application
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.model.UserRole
+import ru.n08i40k.polytechnic.next.network.request.auth.AuthSignUpVK
+import ru.n08i40k.polytechnic.next.network.unwrapException
+import ru.n08i40k.polytechnic.next.settings.settings
+import ru.n08i40k.polytechnic.next.ui.helper.PushSnackbar
+import ru.n08i40k.polytechnic.next.ui.helper.data.rememberInputValue
+import ru.n08i40k.polytechnic.next.ui.widgets.selector.GroupSelector
+import ru.n08i40k.polytechnic.next.ui.widgets.selector.RoleSelector
+import ru.n08i40k.polytechnic.next.ui.widgets.selector.TeacherNameSelector
+import java.util.logging.Logger
+
+private fun trySignUp(
+ context: Context,
+
+ accessToken: String,
+
+ username: String,
+ group: String,
+ role: UserRole,
+
+ onSuccess: () -> Unit,
+ onError: (SignUpError) -> Unit,
+) {
+ AuthSignUpVK(
+ AuthSignUpVK.RequestDto(
+ accessToken,
+ username,
+ group,
+ role,
+ (context.applicationContext as Application).version
+ ),
+ {
+ runBlocking {
+ context.settings.updateData { settings ->
+ settings
+ .toBuilder()
+ .setUserId(it.id)
+ .setAccessToken(it.accessToken)
+ .setGroup(group)
+ .build()
+ }
+ }
+
+ onSuccess()
+ },
+ {
+ val error = mapError(unwrapException(it))
+
+ if (error == SignUpError.UNKNOWN) {
+ val logger = Logger.getLogger("trySignUp")
+
+ logger.severe("Unknown exception while trying to sign up!")
+ logger.severe(it.toString())
+ }
+
+ onError(error)
+ }
+ ).send(context)
+}
+
+@Composable
+internal fun VKPage(
+ accessToken: String,
+ pushSnackbar: PushSnackbar,
+ toApp: () -> Unit,
+ toSelect: () -> Unit,
+ parentWidth: Dp,
+) {
+ val context = LocalContext.current
+
+ var username by rememberInputValue("") { it.length < 4 }
+ var group by rememberInputValue(null) { it == null }
+ var role by remember { mutableStateOf(UserRole.STUDENT) }
+
+ var loading by remember { mutableStateOf(false) }
+
+ val focusManager = LocalFocusManager.current
+
+ val onClick: () -> Unit = fun() {
+ focusManager.clearFocus(true)
+
+ loading = true
+
+ trySignUp(
+ context,
+ accessToken,
+ username.value,
+ group.value!!,
+ role,
+ {
+ loading = false
+ toApp()
+ },
+ {
+ loading = false
+
+ when (it) {
+ SignUpError.USERNAME_ALREADY_EXISTS -> username = username.copy(isError = true)
+ SignUpError.INVALID_GROUP_NAME -> group = group.copy(isError = true)
+ else -> Unit
+ }
+
+ pushSnackbar(getErrorMessage(context, it), SnackbarDuration.Long)
+ }
+ )
+ }
+
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Box(
+ Modifier.defaultMinSize(parentWidth, Dp.Unspecified),
+ contentAlignment = Alignment.Center
+ ) {
+ if (parentWidth != Dp.Unspecified) {
+ Row(Modifier.width(parentWidth)) {
+ IconButton(toSelect) {
+ Icon(
+ Icons.AutoMirrored.Default.ArrowBack,
+ stringResource(R.string.cd_back_icon)
+ )
+ }
+ }
+ }
+
+ Text(
+ stringResource(R.string.sign_up_title),
+ Modifier.padding(10.dp),
+ style = MaterialTheme.typography.displaySmall,
+ fontWeight = FontWeight.ExtraBold
+ )
+ }
+
+ when (role) {
+ UserRole.TEACHER -> {
+ TeacherNameSelector(
+ username.value,
+ {
+ username = username.copy(
+ value = it,
+ isError = false
+ )
+ },
+ isError = username.isError,
+ readOnly = loading
+ )
+ }
+
+ UserRole.STUDENT -> {
+ OutlinedTextField(
+ username.value,
+ {
+ username = username.copy(
+ value = it.filter { it != ' ' }.lowercase(),
+ isError = false
+ )
+ },
+ readOnly = loading,
+ label = { Text(stringResource(R.string.username)) },
+ isError = username.isError,
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(
+ KeyboardCapitalization.None,
+ autoCorrectEnabled = false,
+ KeyboardType.Ascii,
+ ImeAction.Next
+ )
+ )
+ }
+
+ else -> Unit
+ }
+
+ Spacer(Modifier.height(10.dp))
+
+ GroupSelector(
+ group.value, {
+ group = group.copy(
+ value = it,
+ isError = false
+ )
+ },
+ isError = group.isError,
+ readOnly = loading,
+ supervised = role == UserRole.TEACHER
+ )
+
+ Spacer(Modifier.height(10.dp))
+
+ RoleSelector(
+ role,
+ false,
+ loading
+ ) { role = it }
+
+ if (parentWidth != Dp.Unspecified) {
+ Spacer(Modifier.height(10.dp))
+
+ val canProceed = !loading
+ && (!username.isError && username.value.isNotEmpty())
+ && (!group.isError && group.value != null)
+
+ Button(
+ onClick,
+ Modifier.width(parentWidth),
+ enabled = canProceed
+ ) {
+ Text(
+ stringResource(R.string.proceed),
+ style = MaterialTheme.typography.bodyLarge
+ )
+ }
+ }
+ }
+}
\ 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/screen/profile/ChangeGroupDialog.kt
similarity index 64%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ChangeGroupDialog.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/profile/ChangeGroupDialog.kt
index ada7752..ef461b4 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/screen/profile/ChangeGroupDialog.kt
@@ -1,4 +1,4 @@
-package ru.n08i40k.polytechnic.next.ui.main.profile
+package ru.n08i40k.polytechnic.next.ui.screen.profile
import android.content.Context
import androidx.compose.foundation.layout.Column
@@ -21,11 +21,13 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.window.Dialog
import com.android.volley.ClientError
import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
+import ru.n08i40k.polytechnic.next.app.appContainer
import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.model.UserRole
import ru.n08i40k.polytechnic.next.network.request.profile.ProfileChangeGroup
-import ru.n08i40k.polytechnic.next.ui.widgets.GroupSelector
+import ru.n08i40k.polytechnic.next.repository.profile.impl.MockProfileRepository
+import ru.n08i40k.polytechnic.next.ui.helper.data.rememberInputValue
+import ru.n08i40k.polytechnic.next.ui.widgets.selector.GroupSelector
private enum class ChangeGroupError {
NOT_EXISTS
@@ -35,29 +37,31 @@ private fun tryChangeGroup(
context: Context,
group: String,
onError: (ChangeGroupError) -> Unit,
- onSuccess: (String) -> Unit
+ onSuccess: () -> Unit
) {
- ProfileChangeGroup(ProfileChangeGroup.RequestDto(group), context, {
- onSuccess(group)
- }, {
- if (it is ClientError && it.networkResponse.statusCode == 404)
- onError(ChangeGroupError.NOT_EXISTS)
- else throw it
- }).send()
+ ProfileChangeGroup(
+ context.appContainer,
+ ProfileChangeGroup.RequestDto(group),
+ { onSuccess() },
+ {
+ if (it is ClientError && it.networkResponse.statusCode == 404)
+ onError(ChangeGroupError.NOT_EXISTS)
+ else throw it
+ }
+ ).send(context)
}
@Preview(showBackground = true)
@Composable
internal fun ChangeGroupDialog(
context: Context = LocalContext.current,
- profile: Profile = FakeProfileRepository.exampleProfile,
- onChange: (String) -> Unit = {},
+ profile: Profile = MockProfileRepository.profile,
+ onChange: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
Dialog(onDismissRequest = onDismiss) {
Card {
- var group by remember { mutableStateOf(profile.group) }
- var groupError by remember { mutableStateOf(false) }
+ var group by rememberInputValue(profile.group) { it == null }
var processing by remember { mutableStateOf(false) }
@@ -65,35 +69,32 @@ internal fun ChangeGroupDialog(
val modifier = Modifier.fillMaxWidth()
GroupSelector(
- value = group,
- isError = groupError,
+ group.value,
+ { group = group.copy(value = it, isError = false) },
+ isError = group.isError,
readOnly = processing,
- teacher = profile.role == UserRole.TEACHER
- ) { group = it }
+ supervised = profile.role == UserRole.TEACHER
+ )
val focusManager = LocalFocusManager.current
Button(
- modifier = modifier,
- onClick = {
+ {
processing = true
focusManager.clearFocus()
tryChangeGroup(
context = context,
- group = group!!,
+ group = group.value!!,
onError = {
- when (it) {
- ChangeGroupError.NOT_EXISTS -> {
- groupError = true
- }
- }
+ group = group.copy(isError = true)
processing = false
},
onSuccess = onChange
)
},
- enabled = !(groupError || processing) && group != null
+ modifier,
+ !(group.isError || processing) && group.value != null
) {
Text(stringResource(R.string.change_group))
}
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/profile/ChangePasswordDialog.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/profile/ChangePasswordDialog.kt
new file mode 100644
index 0000000..9bcd54e
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/profile/ChangePasswordDialog.kt
@@ -0,0 +1,149 @@
+package ru.n08i40k.polytechnic.next.ui.screen.profile
+
+import android.content.Context
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+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.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.window.Dialog
+import com.android.volley.AuthFailureError
+import com.android.volley.ClientError
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.app.appContainer
+import ru.n08i40k.polytechnic.next.network.request.auth.AuthChangePassword
+import ru.n08i40k.polytechnic.next.ui.helper.data.rememberInputValue
+
+private enum class ChangePasswordError {
+ INCORRECT_CURRENT_PASSWORD,
+ SAME_PASSWORDS
+}
+
+private fun tryChangePassword(
+ context: Context,
+ oldPassword: String,
+ newPassword: String,
+ onSuccess: () -> Unit,
+ onError: (ChangePasswordError) -> Unit
+) {
+ AuthChangePassword(
+ context.appContainer,
+ AuthChangePassword.RequestDto(oldPassword, newPassword),
+ { onSuccess() },
+ {
+ if (it is ClientError && it.networkResponse.statusCode == 409)
+ onError(ChangePasswordError.SAME_PASSWORDS)
+ else if (it is AuthFailureError)
+ onError(ChangePasswordError.INCORRECT_CURRENT_PASSWORD)
+ else throw it
+ }
+ ).send(context)
+}
+
+@Preview(showBackground = true)
+@Composable
+internal fun ChangePasswordDialog(
+ context: Context = LocalContext.current,
+ onChange: () -> Unit = {},
+ onDismiss: () -> Unit = {}
+) {
+ Dialog(onDismissRequest = onDismiss) {
+ Card {
+ var oldPassword by rememberInputValue("") { it.isEmpty() }
+ var newPassword by rememberInputValue("") { it.isEmpty() || it == oldPassword.value }
+
+ var processing by remember { mutableStateOf(false) }
+
+ Column(modifier = Modifier.width(IntrinsicSize.Max)) {
+ val modifier = Modifier.fillMaxWidth()
+
+ OutlinedTextField(
+ oldPassword.value,
+ {
+ oldPassword = oldPassword.copy(value = it, isError = false)
+ newPassword = newPassword.copy(isError = false, checkNow = true)
+ },
+ modifier,
+ readOnly = processing,
+ label = { Text(text = stringResource(R.string.old_password)) },
+ isError = oldPassword.isError,
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(
+ KeyboardCapitalization.None,
+ autoCorrectEnabled = false,
+ KeyboardType.Password,
+ ImeAction.Next
+ ),
+ visualTransformation = PasswordVisualTransformation()
+ )
+
+ OutlinedTextField(
+ newPassword.value,
+ { newPassword = newPassword.copy(value = it, isError = false) },
+ modifier,
+ readOnly = processing,
+ label = { Text(text = stringResource(R.string.new_password)) },
+ isError = newPassword.isError,
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(
+ KeyboardCapitalization.None,
+ autoCorrectEnabled = false,
+ KeyboardType.Password,
+ ImeAction.Next
+ ),
+ visualTransformation = PasswordVisualTransformation()
+ )
+
+ val focusManager = LocalFocusManager.current
+ Button(
+ {
+ processing = true
+ focusManager.clearFocus()
+
+ tryChangePassword(
+ context,
+ oldPassword.value,
+ newPassword.value,
+ onChange
+ ) {
+ when (it) {
+ ChangePasswordError.SAME_PASSWORDS -> {
+ oldPassword.copy(isError = true)
+ newPassword.copy(isError = true)
+ }
+
+ ChangePasswordError.INCORRECT_CURRENT_PASSWORD -> {
+ oldPassword.isError = true
+ }
+ }
+
+ processing = false
+ }
+ },
+ modifier,
+ !(newPassword.isError || oldPassword.isError || processing)
+ ) {
+ Text(stringResource(R.string.change_password))
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ChangeUsernameDialog.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/profile/ChangeUsernameDialog.kt
similarity index 60%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ChangeUsernameDialog.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/profile/ChangeUsernameDialog.kt
index 70e718d..abe9a23 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ChangeUsernameDialog.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/profile/ChangeUsernameDialog.kt
@@ -1,10 +1,11 @@
-package ru.n08i40k.polytechnic.next.ui.main.profile
+package ru.n08i40k.polytechnic.next.ui.screen.profile
import android.content.Context
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.OutlinedTextField
@@ -18,13 +19,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.window.Dialog
import com.android.volley.ClientError
import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
-import ru.n08i40k.polytechnic.next.model.Profile
+import ru.n08i40k.polytechnic.next.app.appContainer
import ru.n08i40k.polytechnic.next.network.request.profile.ProfileChangeUsername
+import ru.n08i40k.polytechnic.next.ui.helper.data.rememberInputValue
private enum class ChangeUsernameError {
INCORRECT_LENGTH,
@@ -37,29 +41,32 @@ private fun tryChangeUsername(
onError: (ChangeUsernameError) -> Unit,
onSuccess: () -> Unit
) {
- ProfileChangeUsername(ProfileChangeUsername.RequestDto(username), context, {
- onSuccess()
- }, {
- if (it is ClientError && it.networkResponse.statusCode == 409)
- onError(ChangeUsernameError.ALREADY_EXISTS)
- if (it is ClientError && it.networkResponse.statusCode == 400)
- onError(ChangeUsernameError.INCORRECT_LENGTH)
- else throw it
- }).send()
+ ProfileChangeUsername(
+ context.appContainer,
+ ProfileChangeUsername.RequestDto(username),
+ {
+ onSuccess()
+ },
+ {
+ if (it is ClientError && it.networkResponse.statusCode == 409)
+ onError(ChangeUsernameError.ALREADY_EXISTS)
+ if (it is ClientError && it.networkResponse.statusCode == 400)
+ onError(ChangeUsernameError.INCORRECT_LENGTH)
+ else throw it
+ }
+ ).send(context)
}
@Preview(showBackground = true)
@Composable
internal fun ChangeUsernameDialog(
context: Context = LocalContext.current,
- profile: Profile = FakeProfileRepository.exampleProfile,
onChange: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
Dialog(onDismissRequest = onDismiss) {
Card {
- var username by remember { mutableStateOf("") }
- var usernameError by remember { mutableStateOf(false) }
+ var username by rememberInputValue("") { it.length < 4 }
var processing by remember { mutableStateOf(false) }
@@ -67,18 +74,19 @@ internal fun ChangeUsernameDialog(
val modifier = Modifier.fillMaxWidth()
OutlinedTextField(
- modifier = modifier,
- value = username,
- isError = usernameError,
- onValueChange = {
- username = it
- usernameError = it.isEmpty()
- || username == profile.username
- || username.length < 4
- || username.length > 10
- },
+ username.value,
+ { username = username.copy(value = it.filter { it != ' ' }.lowercase()) },
+ modifier,
+ readOnly = processing,
label = { Text(text = stringResource(R.string.username)) },
- readOnly = processing
+ isError = username.isError,
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(
+ KeyboardCapitalization.None,
+ autoCorrectEnabled = false,
+ KeyboardType.Ascii,
+ ImeAction.Next
+ )
)
val focusManager = LocalFocusManager.current
@@ -90,19 +98,15 @@ internal fun ChangeUsernameDialog(
tryChangeUsername(
context = context,
- username = username,
+ username = username.value,
onError = {
- usernameError = when (it) {
- ChangeUsernameError.ALREADY_EXISTS -> true
- ChangeUsernameError.INCORRECT_LENGTH -> true
- }
-
+ username = username.copy(isError = true)
processing = false
},
onSuccess = onChange
)
},
- enabled = !(usernameError || processing)
+ enabled = !(username.isError || processing)
) {
Text(stringResource(R.string.change_username))
}
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/profile/ProfileCard.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/profile/ProfileCard.kt
new file mode 100644
index 0000000..46204e3
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/profile/ProfileCard.kt
@@ -0,0 +1,162 @@
+package ru.n08i40k.polytechnic.next.ui.screen.profile
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.AccountCircle
+import androidx.compose.material.icons.filled.Email
+import androidx.compose.material.icons.filled.Lock
+import androidx.compose.material3.Button
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+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.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.runBlocking
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.app.appContainer
+import ru.n08i40k.polytechnic.next.model.Profile
+import ru.n08i40k.polytechnic.next.model.UserRole
+import ru.n08i40k.polytechnic.next.repository.profile.impl.MockProfileRepository
+
+private enum class ChangeValue {
+ NONE,
+ USERNAME,
+ PASSWORD,
+ GROUP
+}
+
+@Preview(showSystemUi = true)
+@Composable
+fun ProfileCard(profile: Profile = MockProfileRepository.profile, refresh: () -> Unit = {}) {
+ Box(Modifier.padding(20.dp)) {
+ Card(
+ colors = CardDefaults.cardColors(
+ contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
+ containerColor = MaterialTheme.colorScheme.primaryContainer
+ )
+ ) {
+ var columnSize by remember { mutableStateOf(10.dp) }
+ val localDensity = LocalDensity.current
+
+ Column(
+ modifier = Modifier
+ .wrapContentWidth()
+ .padding(10.dp)
+ .onGloballyPositioned {
+ with(localDensity) {
+ columnSize = it.size.width.toDp()
+ }
+ },
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ val context = LocalContext.current
+ val focusManager = LocalFocusManager.current
+
+ var change by remember { mutableStateOf(ChangeValue.NONE) }
+
+ TextField(
+ profile.username,
+ {},
+ Modifier.onFocusChanged {
+ if (it.isFocused && profile.role !== UserRole.TEACHER) {
+ change = ChangeValue.USERNAME
+ focusManager.clearFocus(true)
+ }
+ },
+ readOnly = true,
+ label = { Text(stringResource(R.string.username)) },
+ leadingIcon = {
+ Icon(Icons.Filled.AccountCircle, stringResource(R.string.cd_profile_icon))
+ }
+ )
+
+ TextField(
+ "12345678",
+ {},
+ Modifier.onFocusChanged {
+ if (it.isFocused) {
+ change = ChangeValue.PASSWORD
+ focusManager.clearFocus(true)
+ }
+ },
+ readOnly = true,
+ label = { Text(stringResource(R.string.password)) },
+ leadingIcon = {
+ Icon(Icons.Filled.Lock, stringResource(R.string.cd_password_icon))
+ },
+ visualTransformation = PasswordVisualTransformation(),
+ )
+
+ TextField(
+ stringResource(profile.role.stringId),
+ {},
+ readOnly = true,
+ label = { Text(stringResource(R.string.role)) },
+ leadingIcon = {
+ Icon(profile.role.icon, stringResource(R.string.cd_role_icon))
+ },
+ )
+
+ TextField(
+ profile.group,
+ {},
+ Modifier.onFocusChanged {
+ if (it.isFocused) {
+ change = ChangeValue.GROUP
+ focusManager.clearFocus()
+ }
+ },
+ true,
+ label = { Text(stringResource(R.string.group)) },
+ leadingIcon = {
+ Icon(Icons.Filled.Email, stringResource(R.string.cd_group_icon))
+ },
+ )
+
+ Button({
+ val repo = context.applicationContext.appContainer.profileRepository
+ runBlocking { repo.signOut() }
+ }, Modifier.width(columnSize)) {
+ Text(stringResource(R.string.sign_out))
+ }
+
+ val onDismiss: () -> Unit = {
+ change = ChangeValue.NONE
+ }
+
+ val onChange: () -> Unit = {
+ change = ChangeValue.NONE
+ refresh()
+ }
+
+ when (change) {
+ ChangeValue.NONE -> Unit
+ ChangeValue.USERNAME -> ChangeUsernameDialog(context, onChange, onDismiss)
+ ChangeValue.PASSWORD -> ChangePasswordDialog(context, onChange, onDismiss)
+ ChangeValue.GROUP -> ChangeGroupDialog(context, profile, onChange, onDismiss)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ProfileScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/profile/ProfileScreen.kt
similarity index 53%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ProfileScreen.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/profile/ProfileScreen.kt
index 9aaedae..e145038 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/profile/ProfileScreen.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/profile/ProfileScreen.kt
@@ -1,53 +1,59 @@
-package ru.n08i40k.polytechnic.next.ui.main.profile
+package ru.n08i40k.polytechnic.next.ui.screen.profile
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+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.lifecycle.compose.collectAsStateWithLifecycle
+import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.data.MockAppContainer
-import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
+import ru.n08i40k.polytechnic.next.app.appContainer
import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState
import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel
+import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
-
-@Preview(showBackground = true)
@Composable
-fun ProfileScreen(
- profileViewModel: ProfileViewModel = ProfileViewModel(MockAppContainer(LocalContext.current).profileRepository) {},
- onRefreshProfile: () -> Unit = {}
-) {
+fun ProfileScreen(profileViewModel: ProfileViewModel) {
val uiState by profileViewModel.uiState.collectAsStateWithLifecycle()
+ val onRefresh: () -> Unit = { profileViewModel.refresh() }
LoadingContent(
empty = when (uiState) {
- is ProfileUiState.NoProfile -> uiState.isLoading
- is ProfileUiState.HasProfile -> false
+ is ProfileUiState.NoData -> uiState.isLoading
+ is ProfileUiState.HasData -> false
},
loading = uiState.isLoading,
- onRefresh = onRefreshProfile,
+ onRefresh = { profileViewModel.refresh() },
verticalArrangement = Arrangement.Top
) {
when (uiState) {
- is ProfileUiState.HasProfile -> {
- ProfileCard((uiState as ProfileUiState.HasProfile).profile)
+ is ProfileUiState.HasData -> {
+ Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+ val context = LocalContext.current
+
+ ProfileCard((uiState as ProfileUiState.HasData).profile) {
+ runBlocking { context.appContainer.networkCacheRepository.clear() }
+
+ profileViewModel.refresh()
+ }
+ }
}
- is ProfileUiState.NoProfile -> {
+ is ProfileUiState.NoData -> {
if (!uiState.isLoading) {
- TextButton(onClick = onRefreshProfile, modifier = Modifier.fillMaxSize()) {
+ TextButton(onClick = onRefresh, modifier = Modifier.fillMaxSize()) {
Text(stringResource(R.string.reload), textAlign = TextAlign.Center)
}
}
}
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/replacer/ReplacerScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/replacer/ReplacerScreen.kt
new file mode 100644
index 0000000..bff3113
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/replacer/ReplacerScreen.kt
@@ -0,0 +1,8 @@
+package ru.n08i40k.polytechnic.next.ui.screen.replacer
+
+import androidx.compose.runtime.Composable
+
+@Composable
+fun ReplacerScreen() {
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/schedule/GroupScheduleScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/schedule/GroupScheduleScreen.kt
new file mode 100644
index 0000000..8cb10f8
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/schedule/GroupScheduleScreen.kt
@@ -0,0 +1,83 @@
+package ru.n08i40k.polytechnic.next.ui.screen.schedule
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import kotlinx.coroutines.delay
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.ui.model.GroupUiState
+import ru.n08i40k.polytechnic.next.ui.model.GroupViewModel
+import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
+import ru.n08i40k.polytechnic.next.ui.widgets.schedule.SchedulePager
+import ru.n08i40k.polytechnic.next.utils.rememberUpdatedLifecycleOwner
+
+@Composable
+fun GroupScheduleScreen(viewModel: GroupViewModel) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val refresh: () -> Unit = { viewModel.refresh() }
+
+ // auto-refresh every 2 minutes
+ LaunchedEffect(uiState) {
+ delay(120_000)
+ refresh()
+ }
+
+ val lifecycleOwner = rememberUpdatedLifecycleOwner()
+
+ // обновление при развороте приложения
+ DisposableEffect(lifecycleOwner) {
+ val lifecycle = lifecycleOwner.lifecycle
+
+ val observer = LifecycleEventObserver { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_RESUME -> refresh()
+ else -> Unit
+ }
+ }
+
+ lifecycle.addObserver(observer)
+ onDispose { lifecycle.removeObserver(observer) }
+ }
+
+ LoadingContent(
+ empty = uiState is GroupUiState.NoData && uiState.isLoading,
+ loading = uiState.isLoading,
+ onRefresh = refresh,
+ verticalArrangement = Arrangement.Top
+ ) {
+ when (uiState) {
+ is GroupUiState.HasData -> {
+ Column {
+ val data = uiState as GroupUiState.HasData
+
+ UpdateInfo(data.lastUpdateAt, data.updateDates)
+ Spacer(Modifier.height(10.dp))
+ SchedulePager(data.group)
+ }
+ }
+
+ else -> {
+ if (!uiState.isLoading) {
+ TextButton(refresh, Modifier.fillMaxSize()) {
+ Text(stringResource(R.string.reload), textAlign = TextAlign.Center)
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/Paskhalko.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/schedule/Paskhalko.kt
similarity index 90%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/Paskhalko.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/schedule/Paskhalko.kt
index 2b86bf1..4ea6d08 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/Paskhalko.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/schedule/Paskhalko.kt
@@ -1,4 +1,4 @@
-package ru.n08i40k.polytechnic.next.ui.main.schedule.group
+package ru.n08i40k.polytechnic.next.ui.screen.schedule
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/schedule/TeacherScheduleScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/schedule/TeacherScheduleScreen.kt
new file mode 100644
index 0000000..448ef92
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/schedule/TeacherScheduleScreen.kt
@@ -0,0 +1,83 @@
+package ru.n08i40k.polytechnic.next.ui.screen.schedule
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import kotlinx.coroutines.delay
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.ui.model.TeacherUiState
+import ru.n08i40k.polytechnic.next.ui.model.TeacherViewModel
+import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
+import ru.n08i40k.polytechnic.next.ui.widgets.schedule.SchedulePager
+import ru.n08i40k.polytechnic.next.utils.rememberUpdatedLifecycleOwner
+
+@Composable
+fun TeacherScheduleScreen(viewModel: TeacherViewModel) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val refresh: () -> Unit = { viewModel.refresh() }
+
+ // auto-refresh every 2 minutes
+ LaunchedEffect(uiState) {
+ delay(120_000)
+ refresh()
+ }
+
+ val lifecycleOwner = rememberUpdatedLifecycleOwner()
+
+ // обновление при развороте приложения
+ DisposableEffect(lifecycleOwner) {
+ val lifecycle = lifecycleOwner.lifecycle
+
+ val observer = LifecycleEventObserver { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_RESUME -> refresh()
+ else -> Unit
+ }
+ }
+
+ lifecycle.addObserver(observer)
+ onDispose { lifecycle.removeObserver(observer) }
+ }
+
+ LoadingContent(
+ empty = uiState is TeacherUiState.NoData && uiState.isLoading,
+ loading = uiState.isLoading,
+ onRefresh = refresh,
+ verticalArrangement = Arrangement.Top
+ ) {
+ when (uiState) {
+ is TeacherUiState.HasData -> {
+ Column {
+ val data = uiState as TeacherUiState.HasData
+
+ UpdateInfo(data.lastUpdateAt, data.updateDates)
+ Spacer(Modifier.height(10.dp))
+ SchedulePager(data.teacher)
+ }
+ }
+
+ else -> {
+ if (!uiState.isLoading) {
+ TextButton(refresh, Modifier.fillMaxSize()) {
+ Text(stringResource(R.string.reload), textAlign = TextAlign.Center)
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/schedule/TeacherSearchScreen.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/schedule/TeacherSearchScreen.kt
new file mode 100644
index 0000000..6271186
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/schedule/TeacherSearchScreen.kt
@@ -0,0 +1,103 @@
+package ru.n08i40k.polytechnic.next.ui.screen.schedule
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import kotlinx.coroutines.delay
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.ui.model.SearchUiState
+import ru.n08i40k.polytechnic.next.ui.model.SearchViewModel
+import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
+import ru.n08i40k.polytechnic.next.ui.widgets.schedule.SchedulePager
+import ru.n08i40k.polytechnic.next.ui.widgets.selector.TeacherNameSelector
+import ru.n08i40k.polytechnic.next.utils.rememberUpdatedLifecycleOwner
+
+@Composable
+fun TeacherSearchScreen(viewModel: SearchViewModel) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val refresh: () -> Unit = { viewModel.refresh() }
+
+ // auto-refresh every 2 minutes
+ LaunchedEffect(uiState) {
+ delay(120_000)
+ refresh()
+ }
+
+ val lifecycleOwner = rememberUpdatedLifecycleOwner()
+
+ // обновление при развороте приложения
+ DisposableEffect(lifecycleOwner) {
+ val lifecycle = lifecycleOwner.lifecycle
+
+ val observer = LifecycleEventObserver { _, event ->
+ when (event) {
+ Lifecycle.Event.ON_RESUME -> refresh()
+ else -> Unit
+ }
+ }
+
+ lifecycle.addObserver(observer)
+ onDispose { lifecycle.removeObserver(observer) }
+ }
+
+ var teacherName by remember { mutableStateOf(null) }
+
+ LoadingContent(
+ empty = uiState is SearchUiState.NoData && uiState.isLoading,
+ loading = uiState.isLoading,
+ onRefresh = refresh,
+ verticalArrangement = Arrangement.Top
+ ) {
+ TeacherNameSelector(
+ teacherName,
+ {
+ teacherName = it
+ viewModel.set(teacherName)
+ },
+ Modifier.fillMaxWidth(),
+ false,
+ uiState.isLoading
+ )
+
+ Spacer(Modifier.height(10.dp))
+
+ when (uiState) {
+ is SearchUiState.HasData -> {
+ Column {
+ val data = uiState as SearchUiState.HasData
+
+ UpdateInfo(data.lastUpdateAt, data.updateDates)
+ Spacer(Modifier.height(10.dp))
+ SchedulePager(data.teacher)
+ }
+ }
+
+ else -> {
+ if (!uiState.isLoading) {
+ TextButton(refresh, Modifier.fillMaxSize()) {
+ Text(stringResource(R.string.reload), textAlign = TextAlign.Center)
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/UpdateInfo.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/schedule/UpdateInfo.kt
similarity index 83%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/UpdateInfo.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/schedule/UpdateInfo.kt
index 99f6d21..435bcde 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/group/UpdateInfo.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/screen/schedule/UpdateInfo.kt
@@ -1,4 +1,4 @@
-package ru.n08i40k.polytechnic.next.ui.main.schedule.group
+package ru.n08i40k.polytechnic.next.ui.screen.schedule
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -23,15 +23,8 @@ import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.UpdateDates
import ru.n08i40k.polytechnic.next.ui.widgets.ExpandableCard
import ru.n08i40k.polytechnic.next.ui.widgets.ExpandableCardTitle
-import java.text.SimpleDateFormat
+import ru.n08i40k.polytechnic.next.utils.*
import java.util.Date
-import java.util.Locale
-
-
-fun Date.toString(format: String, locale: Locale = Locale.getDefault()): String {
- val formatter = SimpleDateFormat(format, locale)
- return formatter.format(this)
-}
val expanded = mutableStateOf(false)
@@ -60,14 +53,14 @@ fun UpdateInfo(
PaskhalkoDialog()
Column(
- modifier = Modifier
+ Modifier
.fillMaxWidth()
.padding(10.dp)
.clickable { ++paskhalkoCounter }
) {
Row(
- horizontalArrangement = Arrangement.SpaceBetween,
- modifier = Modifier.fillMaxWidth()
+ Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = stringResource(R.string.last_local_update))
Text(
@@ -78,12 +71,12 @@ fun UpdateInfo(
}
Row(
- horizontalArrangement = Arrangement.SpaceBetween,
- modifier = Modifier.fillMaxWidth()
+ Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween
) {
- Text(text = stringResource(R.string.last_server_cache_update))
+ Text(stringResource(R.string.last_server_cache_update))
Text(
- text = cacheUpdateDate,
+ cacheUpdateDate,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
@@ -95,7 +88,7 @@ fun UpdateInfo(
) {
Text(text = stringResource(R.string.last_server_schedule_update))
Text(
- text = scheduleUpdateDate,
+ scheduleUpdateDate,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Monospace
)
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/GroupSelector.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/GroupSelector.kt
deleted file mode 100644
index abd409e..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/GroupSelector.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.widgets
-
-import android.content.Context
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Email
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExposedDropdownMenuBox
-import androidx.compose.material3.ExposedDropdownMenuDefaults
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MenuAnchorType
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextField
-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.setValue
-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 ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetGroupNames
-
-@Composable
-private fun getTeacherNames(context: Context, onUpdated: (String?) -> Unit): ArrayList {
- val groupPlaceholder = stringResource(R.string.loading)
-
- val groups = remember { arrayListOf(null, groupPlaceholder) }
-
- LaunchedEffect(groups) {
- ScheduleGetGroupNames(context, {
- groups.clear()
- groups.addAll(it.names)
- onUpdated(groups.getOrElse(0) { "TODO" }!!)
- }, {
- groups.clear()
- groups.add(null)
- groups.add(context.getString(R.string.failed_to_fetch_group_names))
- onUpdated(groups[1]!!)
- }).send()
- }
-
- return groups
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Preview(showBackground = true)
-@Composable
-fun GroupSelector(
- value: String? = "ИС-214/24",
- isError: Boolean = false,
- readOnly: Boolean = false,
- teacher: Boolean = false,
- onValueChange: (String?) -> Unit = {},
-) {
- var expanded by remember { mutableStateOf(false) }
-
- Box(
- modifier = Modifier.wrapContentSize()
- ) {
- ExposedDropdownMenuBox(
- expanded = expanded,
- onExpandedChange = {
- expanded = !readOnly && !expanded
- }
- ) {
- val groups = getTeacherNames(LocalContext.current, onValueChange)
-
- TextField(
- label = { Text(stringResource(if (teacher) R.string.supervised_group else R.string.group)) },
- modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable),
- value = value ?: groups.getOrElse(1) { "TODO" }!!,
- leadingIcon = {
- Icon(
- Icons.Filled.Email,
- contentDescription = "group"
- )
- },
- onValueChange = {},
- isError = isError,
- readOnly = true,
- trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }
- )
-
- ExposedDropdownMenu(
- expanded = expanded,
- onDismissRequest = { expanded = false }
- ) {
- groups.forEach {
- if (it == null)
- return@forEach
-
- DropdownMenuItem(
- text = { Text(it) },
- onClick = {
- if (groups.isNotEmpty() && groups[0] != null)
- onValueChange(it)
- expanded = false
- }
- )
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/NotificationCard.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/NotificationCard.kt
index 08bca4f..2e69b51 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/NotificationCard.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/NotificationCard.kt
@@ -73,6 +73,7 @@ fun NotificationCard(
val colors = when (level) {
Level.WARNING -> {
val colorFamily = extendedColorScheme().warning
+
CardDefaults.cardColors(
containerColor = colorFamily.colorContainer,
contentColor = colorFamily.onColorContainer
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/OneTapComplete.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/OneTapComplete.kt
new file mode 100644
index 0000000..ec6d57a
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/OneTapComplete.kt
@@ -0,0 +1,81 @@
+package ru.n08i40k.polytechnic.next.ui.widgets
+
+import android.util.Base64
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import com.vk.id.auth.VKIDAuthUiParams
+import com.vk.id.onetap.common.OneTapStyle
+import com.vk.id.onetap.common.button.style.OneTapButtonCornersStyle
+import com.vk.id.onetap.compose.onetap.OneTap
+import com.vk.id.onetap.compose.onetap.OneTapTitleScenario
+import ru.n08i40k.polytechnic.next.network.request.vkid.VKIDOAuth
+import java.nio.charset.Charset
+import java.security.MessageDigest
+import java.util.UUID
+
+private data class PKCE(
+ val codeVerifier: String,
+ val state: String,
+ val codeChallenge: String
+) {
+ companion object {
+ private val ALLOWED_CHARS = ('A'..'Z') + ('a'..'z') + ('0'..'9') + '_' + '-'
+
+ fun create(): PKCE {
+ val codeVerifier = List(64) { ALLOWED_CHARS.random() }.joinToString("")
+
+ val sha256Digester = MessageDigest.getInstance("SHA-256")
+ sha256Digester.update(codeVerifier.toByteArray(Charset.forName("ISO_8859_1")))
+
+ val codeChallenge = Base64.encodeToString(
+ sha256Digester.digest(),
+ Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP
+ )
+
+ return PKCE(codeVerifier, UUID.randomUUID().toString(), codeChallenge)
+ }
+ }
+}
+
+private val PkceSaver = listSaver, String>(
+ save = {
+ val value by it
+
+ listOf(value.codeVerifier, value.state, value.codeChallenge)
+ },
+ restore = { mutableStateOf(PKCE(it[0], it[1], it[2])) }
+)
+
+@Composable
+fun OneTapComplete(modifier: Modifier = Modifier, onAuth: (String) -> Unit, onFail: () -> Unit) {
+ val context = LocalContext.current
+
+ var pkce by rememberSaveable(saver = PkceSaver) { mutableStateOf(PKCE.create()) }
+ val uiParams = VKIDAuthUiParams.Builder().apply {
+ state = pkce.state
+ codeChallenge = pkce.codeChallenge
+ }.build()
+
+ OneTap(
+ modifier = modifier,
+ onAuth = { _, _ -> },
+ onAuthCode = { authCode, isComplete ->
+ VKIDOAuth(
+ VKIDOAuth.RequestDto(authCode.code, pkce.codeVerifier, authCode.deviceId),
+ { pkce = PKCE.create(); onAuth(it.accessToken) },
+ { pkce = PKCE.create() }
+ ).send(context)
+ },
+ onFail = { _, _ -> pkce = PKCE.create(); onFail() },
+ style = OneTapStyle.Dark(cornersStyle = OneTapButtonCornersStyle.Round),
+ scenario = OneTapTitleScenario.SignIn,
+ authParams = uiParams
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/TeacherNameSelector.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/TeacherNameSelector.kt
deleted file mode 100644
index 62a7c35..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/TeacherNameSelector.kt
+++ /dev/null
@@ -1,109 +0,0 @@
-package ru.n08i40k.polytechnic.next.ui.widgets
-
-import android.content.Context
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.wrapContentSize
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Person
-import androidx.compose.material3.DropdownMenuItem
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExposedDropdownMenuBox
-import androidx.compose.material3.ExposedDropdownMenuDefaults
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MenuAnchorType
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextField
-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.setValue
-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 ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetTeacherNames
-
-@Composable
-private fun getTeacherNames(context: Context, onUpdated: (String?) -> Unit): ArrayList {
- val groupPlaceholder = stringResource(R.string.loading)
-
- val names = remember { arrayListOf(null, groupPlaceholder) }
-
- LaunchedEffect(names) {
- ScheduleGetTeacherNames(context, {
- names.clear()
- names.addAll(it.names)
- onUpdated(names.getOrElse(0) { "TODO" }!!)
- }, {
- names.clear()
- names.add(null)
- names.add(context.getString(R.string.failed_to_fetch_teacher_names))
- onUpdated(names[1]!!)
- }).send()
- }
-
- return names
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Preview(showBackground = true)
-@Composable
-fun TeacherNameSelector(
- value: String? = "Фамилия И.О.",
- isError: Boolean = false,
- readOnly: Boolean = false,
- onValueChange: (String?) -> Unit = {},
-) {
- var expanded by remember { mutableStateOf(false) }
-
- Box(
- modifier = Modifier.wrapContentSize()
- ) {
- ExposedDropdownMenuBox(
- expanded = expanded,
- onExpandedChange = {
- expanded = !readOnly && !expanded
- }
- ) {
- val names = getTeacherNames(LocalContext.current, onValueChange)
-
- TextField(
- label = { Text(stringResource(R.string.username)) },
- modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable),
- value = value ?: names.getOrElse(1) { "TODO" }!!,
- leadingIcon = {
- Icon(
- Icons.Filled.Person,
- contentDescription = "username"
- )
- },
- onValueChange = {},
- isError = isError,
- readOnly = true,
- trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }
- )
-
- ExposedDropdownMenu(
- expanded = expanded,
- onDismissRequest = { expanded = false }
- ) {
- names.forEach {
- if (it == null)
- return@forEach
-
- DropdownMenuItem(
- text = { Text(it) },
- onClick = {
- if (names.isNotEmpty() && names[0] != null)
- onValueChange(it)
- expanded = false
- }
- )
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayCard.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/schedule/DayCard.kt
similarity index 53%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayCard.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/schedule/DayCard.kt
index e8ee421..608c59a 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayCard.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/schedule/DayCard.kt
@@ -1,7 +1,8 @@
-package ru.n08i40k.polytechnic.next.ui.main.schedule
+package ru.n08i40k.polytechnic.next.ui.widgets.schedule
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -11,6 +12,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -18,6 +20,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -28,10 +31,34 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
+import kotlinx.datetime.LocalDateTime
import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
import ru.n08i40k.polytechnic.next.model.Day
import ru.n08i40k.polytechnic.next.model.LessonType
+import ru.n08i40k.polytechnic.next.repository.schedule.impl.MockScheduleRepository
+import ru.n08i40k.polytechnic.next.utils.dateTime
+import ru.n08i40k.polytechnic.next.utils.now
+
+private enum class DayOffset {
+ YESTERDAY,
+ TODAY,
+ TOMORROW,
+ OTHER
+}
+
+private fun getDayOffset(day: Day): DayOffset {
+ val now = LocalDateTime.now()
+ val currentDay = now.date.dayOfWeek
+
+ val dayOfWeek = day.date.dateTime.dayOfWeek
+
+ return when (currentDay.value - dayOfWeek.value) {
+ -1 -> DayOffset.TOMORROW
+ 0 -> DayOffset.TODAY
+ 1 -> DayOffset.YESTERDAY
+ else -> DayOffset.OTHER
+ }
+}
@Composable
private fun getCurrentLessonIdx(day: Day?): Flow {
@@ -53,9 +80,10 @@ private fun getCurrentLessonIdx(day: Day?): Flow {
@Composable
fun DayCard(
modifier: Modifier = Modifier,
- day: Day? = FakeScheduleRepository.exampleGroup.days[0],
- distance: Int = 0
+ day: Day = MockScheduleRepository.exampleTeacher.days[0]
) {
+ val offset = remember { getDayOffset(day) }
+
val defaultCardColors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
@@ -74,104 +102,98 @@ fun DayCard(
)
Card(
- modifier = modifier,
+ modifier,
colors = CardDefaults.cardColors(
- containerColor =
- if (distance == 0) MaterialTheme.colorScheme.primaryContainer
- else MaterialTheme.colorScheme.secondaryContainer
+ containerColor = when (offset) {
+ DayOffset.TODAY -> MaterialTheme.colorScheme.primaryContainer
+ else -> MaterialTheme.colorScheme.secondaryContainer
+ }
),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.inverseSurface)
) {
- if (day == null) {
- Text(
- modifier = Modifier.fillMaxWidth(),
- fontWeight = FontWeight.Bold,
- textAlign = TextAlign.Center,
- style = MaterialTheme.typography.titleLarge,
- text = stringResource(R.string.day_null)
- )
- return@Card
- }
Text(
- modifier = Modifier.fillMaxWidth(),
+ day.name,
+ Modifier.fillMaxWidth(),
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
- text = day.name,
+ style = MaterialTheme.typography.titleLarge
)
if (day.street != null) {
Text(
- modifier = Modifier.fillMaxWidth(),
- fontWeight = FontWeight.ExtraLight,
+ day.street,
+ Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
- text = day.street,
)
}
- if (distance >= -1 && distance <= 1) {
+ if (offset != DayOffset.OTHER) {
Text(
- modifier = Modifier.fillMaxWidth(),
+ stringResource(
+ when (offset) {
+ DayOffset.YESTERDAY -> R.string.yesterday
+ DayOffset.TODAY -> R.string.today
+ DayOffset.TOMORROW -> R.string.tomorrow
+ DayOffset.OTHER -> throw RuntimeException()
+ }
+ ),
+ Modifier.fillMaxWidth(),
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium,
- text = stringResource(
- when (distance) {
- -1 -> R.string.yesterday
- 0 -> R.string.today
- 1 -> R.string.tomorrow
- else -> throw RuntimeException()
- }
- ),
)
}
- val currentLessonIdx by getCurrentLessonIdx(if (distance == 0) day else null)
+ val currentLessonIndex by getCurrentLessonIdx(if (offset == DayOffset.TODAY) day else null)
.collectAsStateWithLifecycle(0)
Column(
- modifier = Modifier
+ Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
- verticalArrangement = Arrangement.spacedBy(0.5.dp)
+ Arrangement.spacedBy(0.5.dp)
) {
if (day.lessons.isEmpty()) {
- Text("Can't get schedule!")
+ Text(stringResource(R.string.empty_day))
return@Column
}
- for (lessonIdx in day.lessons.indices) {
- val lesson = day.lessons[lessonIdx]
+ for (lessonIndex in day.lessons.indices) {
+ HorizontalDivider(
+ thickness = 1.dp,
+ color = MaterialTheme.colorScheme.inversePrimary
+ )
+
+ val lesson = day.lessons[lessonIndex]
val cardColors = when (lesson.type) {
- LessonType.DEFAULT -> defaultCardColors
- LessonType.ADDITIONAL -> noneCardColors
- LessonType.BREAK -> noneCardColors
- LessonType.CONSULTATION -> customCardColors
+ LessonType.DEFAULT -> defaultCardColors
+ LessonType.ADDITIONAL -> noneCardColors
+ LessonType.BREAK -> noneCardColors
+ LessonType.CONSULTATION -> customCardColors
LessonType.INDEPENDENT_WORK -> customCardColors
- LessonType.EXAM -> examCardColors
- LessonType.EXAM_WITH_GRADE -> examCardColors
- LessonType.EXAM_DEFAULT -> examCardColors
+ LessonType.EXAM -> examCardColors
+ LessonType.EXAM_WITH_GRADE -> examCardColors
+ LessonType.EXAM_DEFAULT -> examCardColors
}
- val mutableExpanded = remember { mutableStateOf(false) }
+ // TODO: Вернуть ExtraInfo
+ var extraInfo by remember { mutableStateOf(false) }
Box(
Modifier
- .clickable { mutableExpanded.value = true }
+ .clickable { extraInfo = true }
.background(cardColors.containerColor)
) {
- val now = lessonIdx == currentLessonIdx
+ val modifier =
+ if (lessonIndex == currentLessonIndex)
+ Modifier.border(BorderStroke(1.dp, MaterialTheme.colorScheme.error))
+ else
+ Modifier
- if (lesson.type === LessonType.BREAK)
- FreeLessonRow(lesson, cardColors, now)
- else
- LessonRow(lesson, cardColors, now)
+ LessonRow(modifier, lesson, cardColors)
}
-
- if (mutableExpanded.value)
- LessonExtraInfo(lesson, mutableExpanded)
}
-
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/schedule/LessonRow.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/schedule/LessonRow.kt
new file mode 100644
index 0000000..eb7e0de
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/schedule/LessonRow.kt
@@ -0,0 +1,197 @@
+package ru.n08i40k.polytechnic.next.ui.widgets.schedule
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.material3.CardColors
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.LocalTextStyle
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.rememberTextMeasurer
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.model.Lesson
+import ru.n08i40k.polytechnic.next.model.LessonType
+import ru.n08i40k.polytechnic.next.repository.schedule.impl.MockScheduleRepository
+import ru.n08i40k.polytechnic.next.utils.dayMinutes
+import ru.n08i40k.polytechnic.next.utils.fmtAsClock
+
+private enum class TimeFormat {
+ CLOCK,
+ DURATION
+}
+
+@Composable
+private fun fmtTime(start: Int, end: Int, format: TimeFormat): ArrayList {
+ return when (format) {
+ TimeFormat.CLOCK -> arrayListOf(start.fmtAsClock(), end.fmtAsClock())
+ TimeFormat.DURATION -> arrayListOf(
+ "${end - start} ${stringResource(R.string.minutes)}"
+ )
+ }
+}
+
+@Preview(showBackground = true, showSystemUi = true)
+@Composable
+fun LessonRow(
+ modifier: Modifier = Modifier,
+ lesson: Lesson = MockScheduleRepository.exampleGroup.days[0].lessons[0],
+ colors: CardColors = CardDefaults.cardColors()
+) {
+ val verticalPadding = when (lesson.type) {
+ LessonType.BREAK -> 2.5.dp
+ else -> 5.dp
+ }
+
+ val timeFormat = when (lesson.type) {
+ LessonType.BREAK -> TimeFormat.DURATION
+ else -> TimeFormat.CLOCK
+ }
+
+ val contentColor = when (lesson.type) {
+ LessonType.BREAK -> colors.disabledContentColor
+ else -> colors.contentColor
+ }
+
+ // магические вычисления))
+ val range = lesson.defaultRange
+
+ val rangeSize =
+ if (range == null) 1
+ else (range[1] - range[0] + 1) * 2
+
+ Box(modifier) {
+ Row(
+ Modifier.padding(10.dp, verticalPadding * rangeSize),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ when (range) {
+ null -> " "
+ else -> {
+ if (range[0] == range[1])
+ " ${range[0]} "
+ else
+ "${range[0]}-${range[1]}"
+ }
+ },
+ fontFamily = FontFamily.Monospace,
+ fontWeight = FontWeight.Bold,
+ color = contentColor
+ )
+
+ Spacer(Modifier.width(5.dp))
+
+ val textMeasurer = rememberTextMeasurer()
+ val timeWidth = textMeasurer.measure(
+ text = "00:00 ",
+ style = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace)
+ )
+
+ Column(
+ Modifier.width(with(LocalDensity.current) { timeWidth.size.width.toDp() }),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ var time = fmtTime(
+ lesson.time.start.dayMinutes,
+ lesson.time.end.dayMinutes,
+ timeFormat
+ )
+
+ Text(time[0], color = contentColor, fontFamily = FontFamily.Monospace, maxLines = 1)
+ if (lesson.type != LessonType.BREAK)
+ Text(
+ time[1],
+ color = contentColor,
+ fontFamily = FontFamily.Monospace,
+ maxLines = 1
+ )
+ }
+
+ Spacer(Modifier.width(5.dp))
+
+ Row(Modifier.fillMaxWidth(), Arrangement.SpaceBetween, Alignment.CenterVertically) {
+ Column(Modifier.weight(1f)) {
+ // FIXME: Очень странный метод отсеивания, может что-нибудь на замену сделать?
+ if (lesson.type.value > LessonType.BREAK.value) {
+ Text(
+ when (lesson.type) {
+ LessonType.CONSULTATION -> stringResource(R.string.lesson_type_consultation)
+ LessonType.INDEPENDENT_WORK -> stringResource(R.string.lesson_type_independent_work)
+ LessonType.EXAM -> stringResource(R.string.lesson_type_exam)
+ LessonType.EXAM_WITH_GRADE -> stringResource(R.string.lesson_type_exam_with_grade)
+ LessonType.EXAM_DEFAULT -> stringResource(R.string.lesson_type_exam_default)
+ else -> throw RuntimeException("Unknown lesson type!")
+ },
+ fontWeight = FontWeight.Bold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = contentColor
+ )
+ }
+
+ Text(
+ lesson.name ?: stringResource(R.string.lesson_type_break),
+ fontWeight = FontWeight.Medium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ color = contentColor
+ )
+
+ if (lesson.group != null) {
+ Text(
+ lesson.group,
+ color = contentColor,
+ fontWeight = FontWeight.Medium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+
+ for (subGroup in lesson.subGroups) {
+ Text(
+ subGroup.teacher,
+ color = contentColor,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ }
+
+ Column(Modifier.wrapContentWidth()) {
+ if (lesson.subGroups.size != 1) {
+ BasicText("")
+
+ if (lesson.group != null)
+ BasicText("")
+ }
+
+ for (subGroup in lesson.subGroups) {
+ Text(
+ subGroup.cabinet,
+ color = contentColor,
+ maxLines = 1,
+ fontFamily = FontFamily.Monospace
+ )
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
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/widgets/schedule/SchedulePager.kt
similarity index 61%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/ui/main/schedule/DayPager.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/schedule/SchedulePager.kt
index 8a3ea80..13996aa 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/widgets/schedule/SchedulePager.kt
@@ -1,4 +1,4 @@
-package ru.n08i40k.polytechnic.next.ui.main.schedule
+package ru.n08i40k.polytechnic.next.ui.widgets.schedule
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -16,51 +16,44 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import kotlinx.datetime.LocalDateTime
import ru.n08i40k.polytechnic.next.R
-import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
+import ru.n08i40k.polytechnic.next.repository.schedule.impl.MockScheduleRepository
import ru.n08i40k.polytechnic.next.ui.widgets.NotificationCard
import ru.n08i40k.polytechnic.next.utils.dateTime
import ru.n08i40k.polytechnic.next.utils.now
-import java.util.Calendar
import java.util.logging.Level
import kotlin.math.absoluteValue
-private fun isScheduleOutdated(groupOrTeacher: GroupOrTeacher): Boolean {
+private fun isScheduleOutdated(schedule: GroupOrTeacher): Boolean {
val nowDateTime = LocalDateTime.now()
- val lastDay = groupOrTeacher.days.lastOrNull() ?: return true
+ val lastDay = schedule.days.lastOrNull() ?: return true
val lastLesson = lastDay.last ?: return true
return nowDateTime > lastLesson.time.end.dateTime
}
-@Preview
+@Preview(showSystemUi = true)
@Composable
-fun DayPager(groupOrTeacher: GroupOrTeacher = FakeScheduleRepository.exampleGroup) {
- val currentDay = (Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 2)
- val calendarDay = if (currentDay == -1) 6 else currentDay
-
+fun SchedulePager(schedule: GroupOrTeacher = MockScheduleRepository.exampleTeacher) {
val pagerState = rememberPagerState(
- initialPage = calendarDay
- .coerceAtMost(groupOrTeacher.days.size - 1),
- pageCount = { groupOrTeacher.days.size })
+ initialPage = (schedule.currentIdx ?: (schedule.days.size - 1)).coerceAtLeast(0),
+ pageCount = { schedule.days.size }
+ )
Column {
- if (isScheduleOutdated(groupOrTeacher)) {
- NotificationCard(
- level = Level.WARNING,
- title = stringResource(R.string.outdated_schedule)
- )
- }
+ if (isScheduleOutdated(schedule))
+ NotificationCard(Level.WARNING, stringResource(R.string.outdated_schedule))
+
HorizontalPager(
- state = pagerState,
- contentPadding = PaddingValues(horizontal = 7.dp),
- verticalAlignment = Alignment.Top,
- modifier = Modifier
+ pagerState,
+ Modifier
.height(600.dp)
- .padding(top = 5.dp)
+ .padding(top = 5.dp),
+ PaddingValues(horizontal = 7.dp),
+ verticalAlignment = Alignment.Top
) { page ->
DayCard(
- modifier = Modifier.graphicsLayer {
+ Modifier.graphicsLayer {
val offset = pagerState.getOffsetDistanceInPages(
page.coerceIn(0, pagerState.pageCount - 1)
).absoluteValue
@@ -75,8 +68,7 @@ fun DayPager(groupOrTeacher: GroupOrTeacher = FakeScheduleRepository.exampleGrou
start = 0.5f, stop = 1f, fraction = 1f - offset.coerceIn(0f, 1f)
)
},
- day = groupOrTeacher.days[page],
- distance = page - currentDay
+ schedule.days[page]
)
}
}
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/selector/GroupSelector.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/selector/GroupSelector.kt
new file mode 100644
index 0000000..e5a0954
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/selector/GroupSelector.kt
@@ -0,0 +1,60 @@
+package ru.n08i40k.polytechnic.next.ui.widgets.selector
+
+import android.annotation.SuppressLint
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+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.setValue
+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 ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetGroupNames
+
+@SuppressLint("MutableCollectionMutableState")
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview(showBackground = true)
+@Composable
+fun GroupSelector(
+ value: String? = "ИС-214/24",
+ onValueChange: (String?) -> Unit = {},
+ modifier: Modifier = Modifier,
+ isError: Boolean = false,
+ readOnly: Boolean = false,
+ supervised: Boolean = false,
+) {
+ ListSelector(
+ value,
+ onValueChange,
+ modifier,
+ readOnly,
+ stringResource(if (supervised) R.string.supervised_group else R.string.group),
+ { Icon(Icons.Filled.Person, stringResource(R.string.cd_user_icon)) },
+ isError
+ ) {
+ val context = LocalContext.current
+ var entries by remember { mutableStateOf?>(null) }
+
+ LaunchedEffect(context) {
+ ScheduleGetGroupNames(
+ listener = {
+ entries = it.names
+ it(entries!!.getOrNull(0))
+ },
+ errorListener = {
+ entries = arrayListOf()
+ it(null)
+ }
+ ).send(context)
+ }
+
+ entries
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/selector/ListSelector.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/selector/ListSelector.kt
new file mode 100644
index 0000000..01ec5d1
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/selector/ListSelector.kt
@@ -0,0 +1,81 @@
+package ru.n08i40k.polytechnic.next.ui.widgets.selector
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExposedDropdownMenuBox
+import androidx.compose.material3.ExposedDropdownMenuDefaults
+import androidx.compose.material3.MenuAnchorType
+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.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import ru.n08i40k.polytechnic.next.R
+
+@Composable
+private fun DropdownMenuItem(text: String, onClick: () -> Unit) {
+ DropdownMenuItem({ Text(text) }, onClick)
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ListSelector(
+ value: String? = null,
+ onValueChange: (String) -> Unit = {},
+ modifier: Modifier = Modifier,
+ readOnly: Boolean = false,
+ title: String,
+ leadingIcon: @Composable () -> Unit,
+ isError: Boolean = false,
+ loader: @Composable ((String?) -> Unit) -> ArrayList?,
+) {
+ var expanded by remember { mutableStateOf(false) }
+
+ Box(
+ modifier = Modifier.wrapContentSize()
+ ) {
+ val variants = loader { it?.apply { onValueChange(it) } }
+
+ ExposedDropdownMenuBox(
+ expanded = expanded,
+ onExpandedChange = {
+ expanded = !readOnly && !expanded && variants?.isNotEmpty() == true
+ }
+ ) {
+ val context = LocalContext.current
+
+ val viewValue =
+ if (variants == null) stringResource(R.string.loading)
+ else value ?: variants.getOrElse(0) { context.getString(R.string.failed_to_fetch) }
+
+ TextField(
+ viewValue,
+ {},
+ modifier.menuAnchor(MenuAnchorType.PrimaryEditable),
+ readOnly = true,
+ label = { Text(title) },
+ leadingIcon = leadingIcon,
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
+ isError = isError
+ )
+
+ ExposedDropdownMenu(expanded, { expanded = false }) {
+ variants?.forEach {
+ DropdownMenuItem(it) {
+ if (variants.isNotEmpty())
+ onValueChange(it)
+
+ expanded = false
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/RoleSelector.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/selector/RoleSelector.kt
similarity index 98%
rename from app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/RoleSelector.kt
rename to app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/selector/RoleSelector.kt
index d767069..e16474d 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/RoleSelector.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/selector/RoleSelector.kt
@@ -1,4 +1,4 @@
-package ru.n08i40k.polytechnic.next.ui.widgets
+package ru.n08i40k.polytechnic.next.ui.widgets.selector
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.wrapContentSize
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/selector/TeacherNameSelector.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/selector/TeacherNameSelector.kt
new file mode 100644
index 0000000..58e42cb
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/ui/widgets/selector/TeacherNameSelector.kt
@@ -0,0 +1,59 @@
+package ru.n08i40k.polytechnic.next.ui.widgets.selector
+
+import android.annotation.SuppressLint
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+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.setValue
+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 ru.n08i40k.polytechnic.next.R
+import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetTeacherNames
+
+@SuppressLint("MutableCollectionMutableState")
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview(showBackground = true)
+@Composable
+fun TeacherNameSelector(
+ value: String? = "Фамилия И.О.",
+ onValueChange: (String) -> Unit = {},
+ modifier: Modifier = Modifier,
+ isError: Boolean = false,
+ readOnly: Boolean = false,
+) {
+ ListSelector(
+ value,
+ onValueChange,
+ modifier,
+ readOnly,
+ stringResource(R.string.teacher_name),
+ { Icon(Icons.Filled.Person, stringResource(R.string.cd_user_icon)) },
+ isError
+ ) {
+ val context = LocalContext.current
+ var entries by remember { mutableStateOf?>(null) }
+
+ LaunchedEffect(context) {
+ ScheduleGetTeacherNames(
+ listener = {
+ entries = it.names
+ it(entries!!.getOrNull(0))
+ },
+ errorListener = {
+ entries = arrayListOf()
+ it(null)
+ }
+ ).send(context)
+ }
+
+ entries
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Context.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Context.kt
new file mode 100644
index 0000000..cc05606
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Context.kt
@@ -0,0 +1,11 @@
+package ru.n08i40k.polytechnic.next.utils
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import ru.n08i40k.polytechnic.next.Application
+
+fun Context.openLink(link: String) =
+ this.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(link)), null)
+
+val Context.app get() = this.applicationContext as Application
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Extensions.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Extensions.kt
index f243859..8e4005b 100644
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Extensions.kt
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Extensions.kt
@@ -1,3 +1,5 @@
+@file:Suppress("unused")
+
package ru.n08i40k.polytechnic.next.utils
import kotlinx.datetime.Clock
@@ -5,7 +7,10 @@ import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
+import java.text.SimpleDateFormat
import java.util.Calendar
+import java.util.Date
+import java.util.Locale
infix fun T?.or(data: T): T {
if (this == null)
@@ -52,4 +57,9 @@ val Instant.dateTime: LocalDateTime
fun LocalDateTime.Companion.now(): LocalDateTime {
val clock = Clock.System.now()
return clock.toLocalDateTime(TimeZone.currentSystemDefault())
+}
+
+fun Date.toString(format: String, locale: Locale = Locale.getDefault()): String {
+ val formatter = SimpleDateFormat(format, locale)
+ return formatter.format(this)
}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Lifecycle.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Lifecycle.kt
new file mode 100644
index 0000000..627cf68
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Lifecycle.kt
@@ -0,0 +1,12 @@
+package ru.n08i40k.polytechnic.next.utils
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
+
+@Composable
+fun rememberUpdatedLifecycleOwner(): LifecycleOwner {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ return remember { lifecycleOwner }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/utils/MyResult.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/MyResult.kt
new file mode 100644
index 0000000..44f0597
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/MyResult.kt
@@ -0,0 +1,6 @@
+package ru.n08i40k.polytechnic.next.utils
+
+sealed interface MyResult {
+ data class Success(val data: T) : MyResult
+ data class Failure(val exception: Exception = Exception()) : MyResult
+}
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Observable.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Observable.kt
new file mode 100644
index 0000000..a26c4a5
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/Observable.kt
@@ -0,0 +1,103 @@
+package ru.n08i40k.polytechnic.next.utils
+
+import android.content.Context
+import android.util.Log
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import java.lang.ref.WeakReference
+import java.util.UUID
+
+@Suppress("unused")
+class Observable {
+ class Subscriber(
+ private val _observable: WeakReference>,
+ val uuid: UUID,
+
+ private val _next: (T) -> Unit,
+ private val _close: (() -> Unit)? = null,
+
+ val context: Context
+ ) {
+ class LifecycleObserver(val subscriber: Subscriber) :
+ DefaultLifecycleObserver {
+ override fun onDestroy(owner: LifecycleOwner) {
+ super.onDestroy(owner)
+
+ when (val observable = subscriber.observable) {
+ null -> subscriber.destroy()
+ else -> observable.unsubscribe(subscriber.uuid)
+ }
+ }
+ }
+
+ val lifecycleObserver = LifecycleObserver(this)
+
+ val observable get() = _observable.get()
+
+ var invalid = false
+
+ init {
+ if (context is LifecycleOwner)
+ context.lifecycle.addObserver(lifecycleObserver)
+
+ Log.d("Subscriber", "New subscriber $uuid!")
+ }
+
+ fun destroy() {
+ Log.d("Subscriber", "Destroying subscriber...")
+
+ if (context is LifecycleOwner)
+ context.lifecycle.removeObserver(lifecycleObserver)
+
+ invalid = true
+ }
+
+ fun next(value: T) {
+ if (invalid)
+ throw IllegalStateException("Subscriber is invalid.")
+
+ if (observable == null)
+ throw NullPointerException("Observable is null.")
+
+ Log.d("Subscriber", "Invoking $uuid!")
+ _next(value)
+ }
+
+ fun close() {
+ if (invalid)
+ throw IllegalStateException("Subscriber is invalid.")
+
+ if (observable == null)
+ throw NullPointerException("Observable is null.")
+
+ Log.d("Subscriber", "Closing $uuid!")
+ _close?.invoke()
+
+ destroy()
+ }
+ }
+
+ private val subscribers: HashMap> = hashMapOf()
+
+ fun next(result: T) {
+ subscribers.values.forEach { it.next(result) }
+ }
+
+ fun close() {
+ subscribers.values.forEach { it.close() }
+ subscribers.clear()
+ }
+
+ fun subscribe(context: Context, next: (T) -> Unit, close: (() -> Unit)? = null): Subscriber {
+ val uuid = UUID.randomUUID()
+ val subscriber = Subscriber(WeakReference(this), uuid, next, close, context)
+
+ subscribers.put(uuid, subscriber)
+
+ return subscriber
+ }
+
+ fun unsubscribe(uuid: UUID) {
+ subscribers.remove(uuid)?.destroy()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/utils/SingleHook.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/SingleHook.kt
new file mode 100644
index 0000000..d6f61c0
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/utils/SingleHook.kt
@@ -0,0 +1,24 @@
+package ru.n08i40k.polytechnic.next.utils
+
+@Suppress("unused")
+class SingleHook {
+ private var _resolved: Boolean = false
+ private val waiters: ArrayList<(T) -> Unit> = arrayListOf()
+
+ val resolved get() = _resolved
+
+ fun resolve(result: T) {
+ if (_resolved)
+ return
+
+ _resolved = true
+ waiters.forEach { it(result) }
+ waiters.clear() // for fun :)
+ }
+
+ infix fun wait(waiter: (T) -> Unit): SingleHook {
+ waiters.add(waiter)
+
+ return this
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/work/FcmSetTokenWorker.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/work/FcmSetTokenWorker.kt
deleted file mode 100644
index 6d108e7..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/work/FcmSetTokenWorker.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package ru.n08i40k.polytechnic.next.work
-
-import android.content.Context
-import androidx.work.Worker
-import androidx.work.WorkerParameters
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.runBlocking
-import ru.n08i40k.polytechnic.next.PolytechnicApplication
-import ru.n08i40k.polytechnic.next.data.MyResult
-import ru.n08i40k.polytechnic.next.settings.settingsDataStore
-
-class FcmSetTokenWorker(context: Context, workerParams: WorkerParameters) :
- Worker(context, workerParams) {
- override fun doWork(): Result {
- val fcmToken = inputData.getString("TOKEN") ?: return Result.failure()
-
- val accessToken = runBlocking {
- applicationContext.settingsDataStore.data.map { it.accessToken }.first()
- }
- if (accessToken.isEmpty())
- return Result.retry()
-
- val setResult = runBlocking {
- (applicationContext as PolytechnicApplication)
- .container
- .profileRepository
- .setFcmToken(fcmToken)
- }
-
- return when (setResult) {
- is MyResult.Success -> Result.success()
- is MyResult.Failure -> Result.retry()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/work/FcmUpdateCallbackWorker.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/work/FcmUpdateCallbackWorker.kt
deleted file mode 100644
index dd75e79..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/work/FcmUpdateCallbackWorker.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package ru.n08i40k.polytechnic.next.work
-
-import android.content.Context
-import androidx.work.Worker
-import androidx.work.WorkerParameters
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.runBlocking
-import ru.n08i40k.polytechnic.next.data.MyResult
-import ru.n08i40k.polytechnic.next.network.request.fcm.FcmUpdateCallback
-import ru.n08i40k.polytechnic.next.network.tryFuture
-import ru.n08i40k.polytechnic.next.settings.settingsDataStore
-
-class FcmUpdateCallbackWorker(context: Context, workerParams: WorkerParameters) :
- Worker(context, workerParams) {
- override fun doWork(): Result {
- val version = inputData.getString("VERSION") ?: return Result.failure()
-
- val accessToken = runBlocking {
- applicationContext.settingsDataStore.data.map { it.accessToken }.first()
- }
- if (accessToken.isEmpty())
- return Result.retry()
-
- val result = runBlocking {
- tryFuture {
- FcmUpdateCallback(this@FcmUpdateCallbackWorker.applicationContext, version, it, it)
- }
- }
-
- return when (result) {
- is MyResult.Success -> Result.success()
- is MyResult.Failure -> Result.retry()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/work/LinkUpdateWorker.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/work/LinkUpdateWorker.kt
deleted file mode 100644
index eb7cec5..0000000
--- a/app/src/main/java/ru/n08i40k/polytechnic/next/work/LinkUpdateWorker.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package ru.n08i40k.polytechnic.next.work
-
-import android.content.Context
-import androidx.work.Worker
-import androidx.work.WorkerParameters
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.runBlocking
-import ru.n08i40k.polytechnic.next.PolytechnicApplication
-import ru.n08i40k.polytechnic.next.settings.settingsDataStore
-
-class LinkUpdateWorker(context: Context, params: WorkerParameters) :
- Worker(context, params) {
- override fun doWork(): Result {
- val accessToken = runBlocking {
- applicationContext.settingsDataStore.data.map { it.accessToken }.first()
- }
- if (accessToken.isEmpty())
- return Result.retry()
-
- runBlocking {
- (applicationContext as PolytechnicApplication)
- .container
- .scheduleRepository
- .getGroup()
- }
-
- return Result.success()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/worker/UpdateFCMTokenWorker.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/worker/UpdateFCMTokenWorker.kt
new file mode 100644
index 0000000..d89cd93
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/worker/UpdateFCMTokenWorker.kt
@@ -0,0 +1,77 @@
+package ru.n08i40k.polytechnic.next.worker
+
+import android.content.Context
+import androidx.work.BackoffPolicy
+import androidx.work.Constraints
+import androidx.work.CoroutineWorker
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import androidx.work.workDataOf
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
+import ru.n08i40k.polytechnic.next.app.appContainer
+import ru.n08i40k.polytechnic.next.settings.settings
+import ru.n08i40k.polytechnic.next.utils.MyResult
+import java.time.Duration
+import java.util.logging.Logger
+
+class UpdateFCMTokenWorker(context: Context, workerParams: WorkerParameters) :
+ CoroutineWorker(context, workerParams) {
+ private val logger = Logger.getLogger("FCM")
+
+ override suspend fun doWork(): Result {
+ val fcmToken = inputData.getString("TOKEN") ?: run {
+ logger.warning("FCM token is missing in input data.")
+ return Result.failure()
+ }
+ val accessToken = applicationContext.settings.data.map { it.accessToken }.first()
+
+ if (accessToken.isEmpty()) {
+ logger.warning("Access token is empty. Retrying...")
+ return Result.retry()
+ }
+
+ if (applicationContext
+ .appContainer
+ .profileRepository
+ .setFCMToken(fcmToken) is MyResult.Failure
+ ) {
+ logger.warning("Failed to set FCM token in the profile repository.")
+
+ return Result.retry()
+ }
+
+ return Result.success()
+ }
+
+ companion object {
+ private const val WORK_TAG = "update-fcm-token"
+
+ fun schedule(context: Context, token: String) {
+ runBlocking {
+ context.settings.updateData {
+ it.toBuilder().setFcmToken(token).build()
+ }
+ }
+
+ val workManager = WorkManager.getInstance(context)
+ workManager.cancelAllWorkByTag(WORK_TAG)
+
+ val constraints = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+
+ val request = OneTimeWorkRequestBuilder()
+ .setConstraints(constraints)
+ .setBackoffCriteria(BackoffPolicy.LINEAR, Duration.ofMinutes(1))
+ .setInputData(workDataOf("TOKEN" to token))
+ .addTag(WORK_TAG)
+ .build()
+
+ workManager.enqueue(request)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/ru/n08i40k/polytechnic/next/worker/UpdateLinkWorker.kt b/app/src/main/java/ru/n08i40k/polytechnic/next/worker/UpdateLinkWorker.kt
new file mode 100644
index 0000000..cd0687f
--- /dev/null
+++ b/app/src/main/java/ru/n08i40k/polytechnic/next/worker/UpdateLinkWorker.kt
@@ -0,0 +1,58 @@
+package ru.n08i40k.polytechnic.next.worker
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.PeriodicWorkRequest
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import ru.n08i40k.polytechnic.next.app.appContainer
+import ru.n08i40k.polytechnic.next.settings.settings
+import java.util.concurrent.TimeUnit
+import java.util.logging.Logger
+
+class UpdateLinkWorker(context: Context, workerParams: WorkerParameters) :
+ CoroutineWorker(context, workerParams) {
+ private val logger = Logger.getLogger("Link")
+
+ override suspend fun doWork(): Result {
+ val accessToken = applicationContext.settings.data.map { it.accessToken }.first()
+
+ if (accessToken.isEmpty()) {
+ logger.warning("Access token is empty. Retrying...")
+ return Result.retry()
+ }
+
+ applicationContext
+ .appContainer
+ .scheduleRepository
+ .getGroup()
+
+ return Result.success()
+ }
+
+ companion object {
+ private const val WORK_TAG = "schedule-update"
+
+ fun schedule(context: Context) {
+ val workManager = WorkManager.getInstance(context)
+ workManager.cancelAllWorkByTag(WORK_TAG)
+
+ val remoteConfig = context.appContainer.remoteConfig
+ val updateDelay = remoteConfig.getLong("linkUpdateDelay")
+
+ if (updateDelay == 0L)
+ return
+
+ val workRequest = PeriodicWorkRequest.Builder(
+ UpdateLinkWorker::class.java,
+ updateDelay.coerceAtLeast(15), TimeUnit.MINUTES
+ )
+ .addTag(WORK_TAG)
+ .build()
+
+ workManager.enqueue(workRequest)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/proto/settings.proto b/app/src/main/proto/settings.proto
index d1d4bf7..81ae801 100644
--- a/app/src/main/proto/settings.proto
+++ b/app/src/main/proto/settings.proto
@@ -17,7 +17,12 @@ message Settings {
string user_id = 1;
string access_token = 2;
string group = 3;
+
map cache_storage = 4;
UpdateDates update_dates = 5;
+
string version = 6;
+ string suppressed_version = 7;
+
+ string fcm_token = 8;
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-mdpi/paskhalko.jpg b/app/src/main/res/drawable/paskhalko.jpg
similarity index 100%
rename from app/src/main/res/drawable-mdpi/paskhalko.jpg
rename to app/src/main/res/drawable/paskhalko.jpg
diff --git a/app/src/main/res/drawable/telegram.xml b/app/src/main/res/drawable/telegram.xml
deleted file mode 100644
index c773fa8..0000000
--- a/app/src/main/res/drawable/telegram.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index d287c2a..4358c2d 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -1,90 +1,92 @@
-
- Политехникум
- Имя пользователя
- Пароль
- Продолжить
- Авторизация
+ Политехникум+
Регистрация
- Не зарегистрированы?
- Уже зарегистрированы?
- Перезагрузить
- Преподаватель
- Преподаватели
- Длительность
- ч.
- мин.
- Перемена
- Расписание
- Профиль
+ Авторизация
+ Перемена
+ Перерыв
+ в спорт-зале
+ в %1$s каб.
Студент
Преподаватель
Администратор
- Группа
Роль
- На этот день расписания ещё нет!
+ Загрузка…
+ Не удалось загрузить данные.
+ Имя пользователя
+ Курируемая группа
+ Группа
+ Пусто
+ Иконка пользователя
+ Пароль
+ Уже зарегистрированы?
+ Продолжить
+ Не зарегистрированы?
+ Неправильный логин или пароль!
+ Не удалось отправить запрос! Попробуйте позже.
+ Нету подключения к интернету!
+ Произошла неивестная ошибка! Попробуйте позже.
+ Пользоватесь с таким именем уже зарегистрирован!
+ Группа с таким названием не существует!
+ Действия топ-бара
+ Профиль
+ Замена
+ Преподаватели
+ Расписание
+ Расписание
+ Перезагрузить
+ Иконка профиля
+ Иконка пароля
+ Иконка роли
+ Иконка группы
+ Выйти с аккаунта
+ Сменить группу
Старый пароль
Новый пароль
- Загрузка…
Сменить пароль
- Сменить имя пользователя
- Сменить группу
- Выйти с аккаунта
- Кабинеты
- Последнее локальное обновление
- Последнее обновление кеша
- Последнее обновление расписания
- Дополнительная информация
- Заменитель
- байт
- Удалить всё
- Загрузить новое расписание
- Обновления расписания
- Информирует об обновлении расписания
- Расписание обновлено!
- Расписание было обновлено Администратором.
- Расписание было обновлено на сайте политехникума.
- Скачать обновление
- Телеграм канал
- Обновление приложения
- Информирует о выходе новой версии этого приложения
- Вышла версия %1$s!
- Нажмите что бы загрузить обновление.
- Текущая пара
- Отображает текущую пару или перемену в уведомлении
- Загрузка расписания…
- Это уведомление обновится в течении нескольких секунд!
- До конца %1$d ч. %2$d мин.
- %1$s\n| Далее в %2$s - %3$s
- Конец пар
- Пары закончились!
- Ура, можно идти домой! Наверное :(
- каб.
- в %1$s каб.
- в спорт-зале
- Пары ещё не начались
- До начала пар %1$d ч. %2$d мин.
+ Сменить имя
Вы просматриваете устаревшее расписание!
- Некорректное имя пользователя или пароль!
- Неудалось отправить запрос, попробуйте позже!
- Пожалуйста обновите приложение!
- Произошла неизвестная ошибка! Попробуйте позже.
- Не удалось получить список названий групп!
- Пользователь с таким именем уже зарегистрирован!
- Группа с таким названием не существует!
- Нет подключения к интернету!
- Сегодня
Вчера
+ Сегодня
Завтра
- Преподаватели
- ФИО преподавателя
- Преподаватель не выбран или вы допустили ошибку в его ФИО.
- Не удалось получить список ФИО преподавателей!
+ В этот день нету пар :)
+ мин
Консультация
Самостоятельная работа
ЗАЧЁТ
ЗАЧЁТ С ОЦЕНКОЙ
ЭКЗАМЕН
- Курируемая группа
- Преподаватель
+ Перемена
+ Иконка VK
+ К этому аккаунту VK уже привязан другой аккаунт.
+ Иконка ручной регистрации
+ Вручную
+ или
+ Кнопка возврата в меню выбора способа регистрации
+ К этой учетной записи VK не привязано ни одного аккаунта.
+ Недействительный токен доступа VK!
+ Пожалуйста, используйте другую роль.
+ По имени пользователя
+ Поддержка версий ниже %1$s прекращена!
+ Последнее обновление расписания
+ ФИО преподавателя
+ Желаете ли вы обновиться до последней версии?
+ Вышла новая версия приложения!
+ НЕТ
+ ВЫЙТИ
+ ОБНОВИТЬ
+ ЗАГЛУШИТЬ
+ Дополнительная информация
+ Последнее локальное обновление
+ Последнее обновление кеша
+ Скачать обновление
+ Телеграм канал
+ Расписание обновлено!
+ Расписание обновлено администратором.
+ Расписание обновлено на сайте политехникума.
+ Вышла версия %1$s!
+ Нажмите, чтобы загрузить новую версию.
+ Обновления расписания
+ Уведомления об обновлении расписания
+ Обновления приложения
+ Уведомления о выходе новой версии этого приложения
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 028dd59..f8c6127 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,4 +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
index 24b4c3d..a4e34a5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,90 +1,92 @@
- Polytechnic
-
- Username
- Password
- Proceed
- Sign In
+ Polytechnic+
Sign Up
- Not registered?
- Already registered?
- Reload
- Teacher
- Teachers
- Duration
- h.
- min.
- Break
- Schedule
- Profile
+ Sign In
+ Break
+ Break
+ in gym
+ in %1$s cab.
Student
Teacher
Administrator
- Group
Role
- There is no schedule for this day yet!
+ Loading…
+ Failed to fetch data.
+ Username
+ Supervised group
+ Group
+ Empty
+ User icon
+ Password
+ Already registered?
+ Proceed
+ Not registered?
+ Invalid credentials!
+ Failed to send request! Try again later.
+ No internet connection!
+ An unknown error has occurred! Please try again later.
+ A user with this name already registered!
+ A group with this name does not exist!
+ Top app bar actions
+ Profile
+ Replacer
+ Teachers
+ Schedule
+ Schedule
+ Reload
+ Profile icon
+ Password icon
+ Role icon
+ Group icon
+ Sign Out
+ Change group
Old password
New password
Change password
Change username
- Loading…
- Change group
- Sign out
- Cabinets
- Last local update
- Last server cache update
- Last server schedule update
- Additional information
- Replacer
- bytes
- Clear
- Set new
- Schedule update
- Inform when schedule has been updated
- Schedule has been updated!
- Schedule was updated by Administrator.
- Schedule was updated on polytechnic website.
- Download update
- Telegram channel
- Application update
- Inform about new version of this app has been released
- Version %1$s released!
- Click to download new version.
- Current lesson
- View current lesson and breaks in notification
- Loading schedule…
- This notification will be updated in several seconds!
- To end %1$d h. %2$d min.
- %1$s\n| After in %2$s - %3$s
- Lessons end
- Lessons finished!
- TODO
- cab.
- in %1$s cab.
- in gym
- Lessons haven\'t started yet
- %1$d h. %2$d min. before lessons start
- You are viewing an outdated schedule!
- Invalid credentials!
- Failed to send request, try again later!
- Please update the application!
- An unknown error has occurred! Please try again later.
- Failed to get list of group names!
- A user with this name is already registered!
- A group with this name does not exist!
- No internet connection!
- Today
+ You are viewing outdated schedule!
Yesterday
+ Today
Tomorrow
- Teachers
- Teacher name
- Teacher not selected or you made mistake in his name.
- Failed to fetch teacher names!
+ This day doesn\'t contains any lessons :)
+ min
Consultation
Independent work
EXAM*
EXAM* WITH GRADE
EXAM
- Supervised group
- Teacher
+ Break
+ VK icon
+ Manual
+ or
+ Back icon
+ Another account is already linked to this VK account.
+ Manual icon
+ Please, use another role.
+ Manual
+ There are no accounts linked to this VK account.
+ Invalid VK access token!
+ Support for versions below %1$s has been discontinued!
+ Would you like to update to the latest version?
+ A new version of the application has been released!
+ NO
+ EXIT
+ UPDATE
+ SUPPRESS
+ Additional information
+ Last local update
+ Last server cache update
+ Last server schedule update
+ Teacher name
+ Download update
+ Telegram channel
+ Schedule has been updated!
+ Schedule was updated by Administrator.
+ Schedule was updated on polytechnic website.
+ Version %1$s released!
+ Click to download a new version.
+ Schedule update
+ Inform when schedule has been updated
+ Application update
+ Inform about a new version of this app has been released
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index a58e19b..b2ed9bf 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/app/src/test/java/ru/n08i40k/polytechnic/next/ExampleUnitTest.kt b/app/src/test/java/ru/n08i40k/polytechnic/next/ExampleUnitTest.kt
index 2db2bb5..f913706 100644
--- a/app/src/test/java/ru/n08i40k/polytechnic/next/ExampleUnitTest.kt
+++ b/app/src/test/java/ru/n08i40k/polytechnic/next/ExampleUnitTest.kt
@@ -1,27 +1,17 @@
package ru.n08i40k.polytechnic.next
-import android.content.Context
import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.junit.MockitoJUnitRunner
-import org.mockito.kotlin.doReturn
-import org.mockito.kotlin.mock
-import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
-@RunWith(MockitoJUnitRunner.Silent::class)
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
class ExampleUnitTest {
@Test
- fun getNameAndCabinetsShort_isNotThrow() {
- val mockContext = mock {
- on { getString(R.string.in_gym_lc) } doReturn "с/з"
- on { getString(R.string.lesson_break) } doReturn "Перемена"
- }
- val group = FakeScheduleRepository.exampleGroup
-
- for (day in group.days) {
- for (lesson in day.lessons) {
- lesson.getNameAndCabinetsShort(mockContext)
- }
- }
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
}
}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index 49e403d..7befeb3 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -2,8 +2,7 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
-
- alias(libs.plugins.compose.compiler) apply false
+ alias(libs.plugins.kotlin.compose) apply false
id("com.google.devtools.ksp") version "2.0.10-1.0.24" apply false
@@ -13,4 +12,6 @@ plugins {
alias(libs.plugins.google.firebase.crashlytics) apply false
id("com.google.dagger.hilt.android") version "2.51.1" apply false
+
+ id("vkid.manifest.placeholders") version "1.1.0" apply true
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index cc867a1..c1c843c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,63 +1,73 @@
[versions]
-accompanistSwiperefresh = "0.36.0"
-agp = "8.7.3"
-firebaseBom = "33.7.0"
-hiltAndroid = "2.53.1"
-hiltAndroidCompiler = "2.53.1"
-hiltNavigationCompose = "1.2.0"
+agp = "8.8.0"
+desugar_jdk_libs = "2.1.4"
kotlin = "2.0.10"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
-kotlinxSerializationJson = "1.7.3"
lifecycleRuntimeKtx = "2.8.7"
-activityCompose = "1.9.3"
-composeBom = "2024.12.01"
-mockitoKotlin = "5.4.0"
+activityCompose = "1.10.0"
+composeBom = "2025.01.00"
+accompanistSwiperefresh = "0.36.0"
+firebaseBom = "33.8.0"
+hiltAndroid = "2.53.1"
+hiltAndroidCompiler = "2.53.1"
+hiltNavigationCompose = "1.2.0"
+kotlinxSerializationJson = "1.7.3"
protobufLite = "3.0.1"
volley = "1.2.1"
-datastore = "1.1.1"
+datastore = "1.1.2"
navigationCompose = "2.8.5"
googleFirebaseCrashlytics = "3.0.2"
workRuntime = "2.10.0"
+vkid = "2.2.2"
[libraries]
-accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanistSwiperefresh" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
-androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
-androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "workRuntime" }
-androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntime" }
-hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidCompiler" }
-hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
+desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
-androidx-datastore = { group ="androidx.datastore", name = "datastore", version.ref="datastore"}
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
-androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version = "1.7.6" }
+androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
+
+accompanist-swiperefresh = { module = "com.google.accompanist:accompanist-swiperefresh", version.ref = "accompanistSwiperefresh" }
+
+androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "workRuntime" }
+androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntime" }
+
+androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
+hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidCompiler" }
+hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
+
+androidx-datastore = { group ="androidx.datastore", name = "datastore", version.ref="datastore"}
+protobuf-lite = { module = "com.google.protobuf:protobuf-lite", version.ref = "protobufLite" }
+
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.6.1" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
-mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" }
-protobuf-lite = { module = "com.google.protobuf:protobuf-lite", version.ref = "protobufLite" }
volley = { group = "com.android.volley", name = "volley", version.ref = "volley" }
+
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
+
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
-firebase-analytics = { module = "com.google.firebase:firebase-analytics", version = "22.1.2" }
-firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics", version = "19.3.0" }
+firebase-analytics = { module = "com.google.firebase:firebase-analytics", version = "22.2.0" }
+firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics", version = "19.4.0" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging", version = "24.1.0" }
-firebase-config = { group = "com.google.firebase", name = "firebase-config", version = "22.0.1" }
+firebase-config = { group = "com.google.firebase", name = "firebase-config", version = "22.1.0" }
+vk-vkid = {group = "com.vk.id", name = "vkid", version.ref = "vkid" }
+vk-onetap-compose = {group = "com.vk.id", name = "onetap-compose", version.ref = "vkid" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
google-firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "googleFirebaseCrashlytics" }
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e40c8b5..54ccd37 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Sat Sep 07 22:06:56 GMT+04:00 2024
+#Tue Jan 07 20:58:39 AMT 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/settings.gradle.kts b/settings.gradle.kts
index a84acc8..5f88230 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,3 +1,5 @@
+import java.net.URI
+
pluginManagement {
repositories {
google {
@@ -7,18 +9,24 @@ pluginManagement {
includeGroupByRegex("androidx.*")
}
}
+ maven(url = "https://artifactory-external.vkpartner.ru/artifactory/vkid-sdk-android/")
mavenCentral()
gradlePluginPortal()
}
}
+
+@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
+ maven {
+ url = URI("https://artifactory-external.vkpartner.ru/artifactory/vkid-sdk-android/")
+ }
}
}
-rootProject.name = "PolytecnicNext"
+rootProject.name = "Polytechnic"
include(":app")
\ No newline at end of file