mirror of
https://github.com/n08i40k/polytechnic-android.git
synced 2025-12-06 09:47:48 +03:00
3.0.0 / 3.0.1
This commit is contained in:
1
.idea/.name
generated
1
.idea/.name
generated
@@ -1 +0,0 @@
|
|||||||
PolytecnicNext
|
|
||||||
7
.idea/codeStyles/Project.xml
generated
7
.idea/codeStyles/Project.xml
generated
@@ -1,9 +1,5 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
<option name="LINE_SEPARATOR" value=" " />
|
|
||||||
<JetCodeStyleSettings>
|
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
|
||||||
</JetCodeStyleSettings>
|
|
||||||
<codeStyleSettings language="XML">
|
<codeStyleSettings language="XML">
|
||||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||||
<indentOptions>
|
<indentOptions>
|
||||||
@@ -117,8 +113,5 @@
|
|||||||
</rules>
|
</rules>
|
||||||
</arrangement>
|
</arrangement>
|
||||||
</codeStyleSettings>
|
</codeStyleSettings>
|
||||||
<codeStyleSettings language="kotlin">
|
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
|
||||||
</codeStyleSettings>
|
|
||||||
</code_scheme>
|
</code_scheme>
|
||||||
</component>
|
</component>
|
||||||
2
.idea/codeStyles/codeStyleConfig.xml
generated
2
.idea/codeStyles/codeStyleConfig.xml
generated
@@ -1,5 +1,5 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<state>
|
<state>
|
||||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
</state>
|
</state>
|
||||||
</component>
|
</component>
|
||||||
6
.idea/compiler.xml
generated
6
.idea/compiler.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="CompilerConfiguration">
|
|
||||||
<bytecodeTargetLevel target="21" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
10
.idea/deploymentTargetSelector.xml
generated
10
.idea/deploymentTargetSelector.xml
generated
@@ -2,16 +2,8 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="deploymentTargetSelector">
|
<component name="deploymentTargetSelector">
|
||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="Polytechnic.app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DropdownSelection timestamp="2024-10-10T18:25:08.106861775Z">
|
|
||||||
<Target type="DEFAULT_BOOT">
|
|
||||||
<handle>
|
|
||||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=482a22d" />
|
|
||||||
</handle>
|
|
||||||
</Target>
|
|
||||||
</DropdownSelection>
|
|
||||||
<DialogSelection />
|
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
3
.idea/dictionaries/n08i40k.xml
generated
3
.idea/dictionaries/n08i40k.xml
generated
@@ -1,3 +0,0 @@
|
|||||||
<component name="ProjectDictionaryState">
|
|
||||||
<dictionary name="n08i40k" />
|
|
||||||
</component>
|
|
||||||
14
.idea/discord.xml
generated
14
.idea/discord.xml
generated
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="DiscordProjectSettings">
|
|
||||||
<option name="show" value="PROJECT_FILES" />
|
|
||||||
<option name="description" value="" />
|
|
||||||
<option name="applicationTheme" value="default" />
|
|
||||||
<option name="iconsTheme" value="default" />
|
|
||||||
<option name="button1Title" value="" />
|
|
||||||
<option name="button1Url" value="" />
|
|
||||||
<option name="button2Title" value="" />
|
|
||||||
<option name="button2Url" value="" />
|
|
||||||
<option name="customApplicationId" value="" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
3
.idea/kotlinc.xml
generated
3
.idea/kotlinc.xml
generated
@@ -1,5 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="Kotlin2JsCompilerArguments">
|
||||||
|
<option name="moduleKind" value="plain" />
|
||||||
|
</component>
|
||||||
<component name="Kotlin2JvmCompilerArguments">
|
<component name="Kotlin2JvmCompilerArguments">
|
||||||
<option name="jvmTarget" value="1.8" />
|
<option name="jvmTarget" value="1.8" />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
7
.idea/misc.xml
generated
7
.idea/misc.xml
generated
@@ -1,11 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="EntryPointsManager">
|
|
||||||
<list size="2">
|
|
||||||
<item index="0" class="java.lang.String" itemvalue="dagger.Module" />
|
|
||||||
<item index="1" class="java.lang.String" itemvalue="dagger.Provides" />
|
|
||||||
</list>
|
|
||||||
</component>
|
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
|||||||
21
LICENSE
21
LICENSE
@@ -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.
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
|
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
||||||
import com.google.protobuf.gradle.id
|
import com.google.protobuf.gradle.id
|
||||||
import com.google.protobuf.gradle.proto
|
import com.google.protobuf.gradle.proto
|
||||||
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.compose.compiler)
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
|
||||||
kotlin("plugin.serialization") version "2.0.20"
|
kotlin("plugin.serialization") version "2.0.20"
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
@@ -18,9 +18,22 @@ plugins {
|
|||||||
alias(libs.plugins.google.firebase.crashlytics)
|
alias(libs.plugins.google.firebase.crashlytics)
|
||||||
|
|
||||||
id("com.google.dagger.hilt.android")
|
id("com.google.dagger.hilt.android")
|
||||||
|
|
||||||
|
id("vkid.manifest.placeholders") version "1.1.0" apply true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val localProperties = gradleLocalProperties(rootDir, providers)
|
||||||
|
|
||||||
android {
|
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"
|
namespace = "ru.n08i40k.polytechnic.next"
|
||||||
compileSdk = 35
|
compileSdk = 35
|
||||||
|
|
||||||
@@ -33,13 +46,10 @@ android {
|
|||||||
applicationId = "ru.n08i40k.polytechnic.next"
|
applicationId = "ru.n08i40k.polytechnic.next"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 23
|
versionCode = 25
|
||||||
versionName = "2.3.0"
|
versionName = "3.0.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
|
||||||
useSupportLibrary = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -49,11 +59,14 @@ android {
|
|||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "11"
|
jvmTarget = "11"
|
||||||
@@ -64,11 +77,6 @@ android {
|
|||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion = "1.5.1"
|
kotlinCompilerExtensionVersion = "1.5.1"
|
||||||
}
|
}
|
||||||
packaging {
|
|
||||||
resources {
|
|
||||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("main") {
|
getByName("main") {
|
||||||
@@ -90,6 +98,15 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
|
|
||||||
|
// vk
|
||||||
|
implementation(libs.vk.vkid)
|
||||||
|
implementation(libs.vk.onetap.compose)
|
||||||
|
|
||||||
|
// internet
|
||||||
|
implementation(libs.volley)
|
||||||
|
|
||||||
// work manager
|
// work manager
|
||||||
implementation(libs.androidx.work.runtime)
|
implementation(libs.androidx.work.runtime)
|
||||||
implementation(libs.androidx.work.runtime.ktx)
|
implementation(libs.androidx.work.runtime.ktx)
|
||||||
@@ -116,6 +133,7 @@ dependencies {
|
|||||||
implementation(libs.kotlinx.serialization.json)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation(libs.kotlinx.datetime)
|
implementation(libs.kotlinx.datetime)
|
||||||
|
|
||||||
|
// default
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
@@ -124,13 +142,10 @@ dependencies {
|
|||||||
implementation(libs.androidx.ui.graphics)
|
implementation(libs.androidx.ui.graphics)
|
||||||
implementation(libs.androidx.ui.tooling.preview)
|
implementation(libs.androidx.ui.tooling.preview)
|
||||||
implementation(libs.androidx.material3)
|
implementation(libs.androidx.material3)
|
||||||
implementation(libs.volley)
|
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
|
|
||||||
|
// test
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
testImplementation(libs.mockito.kotlin)
|
|
||||||
|
|
||||||
|
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
|
|||||||
23
app/proguard-rules.pro
vendored
23
app/proguard-rules.pro
vendored
@@ -19,3 +19,26 @@
|
|||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
-renamesourcefileattribute SourceFile
|
-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 { *; }
|
||||||
@@ -19,6 +19,6 @@ class ExampleInstrumentedTest {
|
|||||||
fun useAppContext() {
|
fun useAppContext() {
|
||||||
// Context of the app under test.
|
// Context of the app under test.
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
assertEquals("ru.n08i40k.polytecnic.next", appContext.packageName)
|
assertEquals("ru.n08i40k.polytechnic.next", appContext.packageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,48 +2,40 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<!-- bruh -->
|
<!-- чтооооо не может быть, мне нужен интернет? правда? -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<!-- For posting notifications from FCM and CLV services -->
|
<!-- нихуя себе что это такое -->
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<!-- For CLV service able to work -->
|
<!-- ну это по приколу добавил конечно же -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".PolytechnicApplication"
|
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:name=".Application"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:theme="@style/Theme.PolytechnicNext"
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.Polytechnic"
|
||||||
tools:targetApi="35">
|
tools:targetApi="35">
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.MyFirebaseMessagingService"
|
android:name=".service.FCMService"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".service.CurrentLessonViewService"
|
|
||||||
android:exported="false"
|
|
||||||
android:foregroundServiceType="specialUse">
|
|
||||||
<property
|
|
||||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
|
||||||
android:value="Service for viewing current lesson in notification." />
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.PolytechnicNext">
|
android:theme="@style/Theme.Polytechnic">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
89
app/src/main/java/ru/n08i40k/polytechnic/next/Application.kt
Normal file
89
app/src/main/java/ru/n08i40k/polytechnic/next/Application.kt
Normal file
@@ -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<Unit> = Observable<Unit>()
|
||||||
|
)
|
||||||
|
|
||||||
|
@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<String> {
|
||||||
|
override fun onComplete(token: Task<String?>) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
116
app/src/main/java/ru/n08i40k/polytechnic/next/MainActivity.kt
Normal file
116
app/src/main/java/ru/n08i40k/polytechnic/next/MainActivity.kt
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package ru.n08i40k.polytechnic.next
|
package ru.n08i40k.polytechnic.next.app
|
||||||
|
|
||||||
object NotificationChannels {
|
object NotificationChannels {
|
||||||
const val LESSON_VIEW = "lesson-view"
|
|
||||||
const val SCHEDULE_UPDATE = "schedule-update"
|
const val SCHEDULE_UPDATE = "schedule-update"
|
||||||
const val APP_UPDATE = "app-update"
|
const val APP_UPDATE = "app-update"
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package ru.n08i40k.polytechnic.next.data
|
|
||||||
|
|
||||||
sealed interface MyResult<out R> {
|
|
||||||
data class Success<out T>(val data: T) : MyResult<T>
|
|
||||||
data class Failure(val exception: Exception) : MyResult<Nothing>
|
|
||||||
}
|
|
||||||
@@ -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<List<ScheduleReplacer>>
|
|
||||||
|
|
||||||
suspend fun setCurrent(
|
|
||||||
fileName: String,
|
|
||||||
fileData: ByteArray,
|
|
||||||
fileType: String
|
|
||||||
): MyResult<Unit>
|
|
||||||
|
|
||||||
suspend fun clear(): MyResult<Int>
|
|
||||||
}
|
|
||||||
@@ -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<ScheduleReplacer> = listOf(
|
|
||||||
ScheduleReplacer("test-etag", 236 * 1024),
|
|
||||||
ScheduleReplacer("frgsjkfhg", 623 * 1024),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getAll(): MyResult<List<ScheduleReplacer>> {
|
|
||||||
return MyResult.Success(exampleReplacers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setCurrent(
|
|
||||||
fileName: String,
|
|
||||||
fileData: ByteArray,
|
|
||||||
fileType: String
|
|
||||||
): MyResult<Unit> {
|
|
||||||
return MyResult.Success(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun clear(): MyResult<Int> {
|
|
||||||
return MyResult.Success(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<List<ScheduleReplacer>> =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
tryFuture { ScheduleReplacerGet(context, it, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override suspend fun setCurrent(
|
|
||||||
fileName: String,
|
|
||||||
fileData: ByteArray,
|
|
||||||
fileType: String
|
|
||||||
): MyResult<Nothing> =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
tryFuture { ScheduleReplacerSet(context, fileName, fileData, fileType, it, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun clear(): MyResult<Int> {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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<Profile>
|
|
||||||
|
|
||||||
suspend fun setFcmToken(token: String): MyResult<Unit>
|
|
||||||
}
|
|
||||||
@@ -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<Profile> {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
delay(1500)
|
|
||||||
|
|
||||||
if (counter++ % 3 == 0)
|
|
||||||
MyResult.Failure(Exception())
|
|
||||||
else
|
|
||||||
MyResult.Success(exampleProfile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setFcmToken(token: String): MyResult<Unit> {
|
|
||||||
return MyResult.Success(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Profile> =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
tryFuture { ProfileMe(context, it, it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun setFcmToken(token: String): MyResult<Unit> =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
tryFuture { FcmSetToken(context, token, it, it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,8 +26,8 @@ data class Day(
|
|||||||
|
|
||||||
val street: String? = null
|
val street: String? = null
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
constructor(name: String, date: Instant, lessons: List<Lesson>) : this(
|
constructor(name: String, date: Instant, lessons: List<Lesson>, street: String?) : this(
|
||||||
name, date.toEpochMilliseconds(), lessons
|
name, date.toEpochMilliseconds(), lessons, street
|
||||||
)
|
)
|
||||||
|
|
||||||
val date: Instant
|
val date: Instant
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package ru.n08i40k.polytechnic.next.model
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import ru.n08i40k.polytechnic.next.utils.dateTime
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
@@ -14,12 +15,16 @@ data class GroupOrTeacher(
|
|||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
val currentIdx: Int?
|
val currentIdx: Int?
|
||||||
get() {
|
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 null
|
||||||
|
|
||||||
return currentDay
|
return days.indexOf(day[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
val current: Day?
|
val current: Day?
|
||||||
@@ -27,6 +32,8 @@ data class GroupOrTeacher(
|
|||||||
return days.getOrNull(currentIdx ?: return null)
|
return days.getOrNull(currentIdx ?: return null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: вернуть
|
||||||
|
@Suppress("unused")
|
||||||
val currentKV: Pair<Int, Day>?
|
val currentKV: Pair<Int, Day>?
|
||||||
get() {
|
get() {
|
||||||
val idx = currentIdx ?: return null
|
val idx = currentIdx ?: return null
|
||||||
|
|||||||
@@ -18,42 +18,32 @@ data class Lesson(
|
|||||||
val group: String? = null,
|
val group: String? = null,
|
||||||
val subGroups: List<SubGroup>
|
val subGroups: List<SubGroup>
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
val duration: Int
|
// TODO: вернуть
|
||||||
get() {
|
@Suppress("unused")
|
||||||
val startMinutes = time.start.dayMinutes
|
val duration get() = time.end.dayMinutes - time.start.dayMinutes
|
||||||
val endMinutes = time.end.dayMinutes
|
|
||||||
|
|
||||||
return endMinutes - startMinutes
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
fun getNameAndCabinetsShort(context: Context): String {
|
fun getNameAndCabinetsShort(context: Context): String {
|
||||||
val name =
|
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
|
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())
|
if (cabinetList.size == 1 && cabinetList[0] == "с/з")
|
||||||
return limitedName
|
return "$shortName ${context.getString(R.string.in_gym_lc)}"
|
||||||
|
|
||||||
if (cabinets.size == 1 && cabinets[0] == "с/з")
|
val cabinets =
|
||||||
return buildString {
|
context.getString(R.string.in_cabinets_short_lc, cabinetList.joinToString(", "))
|
||||||
append(limitedName)
|
return "$shortName $cabinets"
|
||||||
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(", ")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,8 +5,9 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class Profile(
|
data class Profile(
|
||||||
val id: String,
|
val id: String,
|
||||||
val accessToken: String,
|
|
||||||
val username: String,
|
val username: String,
|
||||||
val group: String,
|
val group: String,
|
||||||
val role: UserRole
|
val role: UserRole,
|
||||||
|
val accessToken: String? = null,
|
||||||
|
val vkId: Int? = null
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network
|
package ru.n08i40k.polytechnic.next.network
|
||||||
|
|
||||||
object NetworkValues {
|
object NetworkValues {
|
||||||
const val API_HOST = "https://polytechnic.n08i40k.ru:5050/api/"
|
const val API_HOST = "https://polytechnic.n08i40k.ru/api/"
|
||||||
}
|
}
|
||||||
@@ -6,13 +6,12 @@ import com.android.volley.toolbox.StringRequest
|
|||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
|
|
||||||
open class RequestBase(
|
open class RequestBase(
|
||||||
protected val context: Context,
|
|
||||||
method: Int,
|
method: Int,
|
||||||
url: String?,
|
url: String?,
|
||||||
listener: Response.Listener<String>,
|
listener: Response.Listener<String>,
|
||||||
errorListener: Response.ErrorListener?
|
errorListener: Response.ErrorListener?
|
||||||
) : StringRequest(method, NetworkValues.API_HOST + url, listener, 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")
|
Logger.getLogger("RequestBase").info("Sending request to $url")
|
||||||
NetworkConnection.getInstance(context).addToRequestQueue(this)
|
NetworkConnection.getInstance(context).addToRequestQueue(this)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network
|
package ru.n08i40k.polytechnic.next.network
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import com.android.volley.VolleyError
|
import com.android.volley.VolleyError
|
||||||
import com.android.volley.toolbox.RequestFuture
|
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.ExecutionException
|
||||||
import java.util.concurrent.TimeoutException
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
fun <ResultT, RequestT : RequestBase> tryFuture(
|
fun <ResultT, RequestT : RequestBase> tryFuture(
|
||||||
|
context: Context,
|
||||||
buildRequest: (RequestFuture<ResultT>) -> RequestT
|
buildRequest: (RequestFuture<ResultT>) -> RequestT
|
||||||
): MyResult<ResultT> {
|
): MyResult<ResultT> {
|
||||||
val future = RequestFuture.newFuture<ResultT>()
|
val future = RequestFuture.newFuture<ResultT>()
|
||||||
|
|
||||||
buildRequest(future).send()
|
buildRequest(future).send(context)
|
||||||
|
|
||||||
return tryGet(future)
|
return tryGet(future)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 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.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.DataOutputStream
|
import java.io.DataOutputStream
|
||||||
@@ -11,13 +10,13 @@ import java.io.UnsupportedEncodingException
|
|||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
open class AuthorizedMultipartRequest(
|
open class AuthorizedMultipartRequest(
|
||||||
context: Context,
|
appContainer: AppContainer,
|
||||||
method: Int,
|
method: Int,
|
||||||
url: String,
|
url: String,
|
||||||
listener: Response.Listener<String>,
|
listener: Response.Listener<String>,
|
||||||
errorListener: Response.ErrorListener?,
|
errorListener: Response.ErrorListener?,
|
||||||
canBeUnauthorized: Boolean = false
|
canBeUnauthorized: Boolean = false
|
||||||
) : AuthorizedRequest(context, method, url, listener, errorListener, canBeUnauthorized) {
|
) : AuthorizedRequest(appContainer, method, url, listener, errorListener, canBeUnauthorized) {
|
||||||
private val twoHyphens = "--"
|
private val twoHyphens = "--"
|
||||||
private val lineEnd = "\r\n"
|
private val lineEnd = "\r\n"
|
||||||
private val boundary = "apiclient-" + System.currentTimeMillis()
|
private val boundary = "apiclient-" + System.currentTimeMillis()
|
||||||
@@ -1,52 +1,72 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request
|
package ru.n08i40k.polytechnic.next.network.request
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.AuthFailureError
|
import com.android.volley.AuthFailureError
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
|
import com.android.volley.VolleyError
|
||||||
|
import jakarta.inject.Singleton
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.network.RequestBase
|
import ru.n08i40k.polytechnic.next.network.RequestBase
|
||||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
import ru.n08i40k.polytechnic.next.settings.settings
|
||||||
import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
|
|
||||||
|
|
||||||
open class AuthorizedRequest(
|
open class AuthorizedRequest(
|
||||||
context: Context,
|
val appContainer: AppContainer,
|
||||||
method: Int,
|
method: Int,
|
||||||
url: String,
|
url: String,
|
||||||
listener: Response.Listener<String>,
|
listener: Response.Listener<String>,
|
||||||
errorListener: Response.ErrorListener?,
|
errorListener: Response.ErrorListener?,
|
||||||
private val canBeUnauthorized: Boolean = false
|
private val canBeUnauthorized: Boolean = false,
|
||||||
) : RequestBase(
|
) : RequestBase(
|
||||||
context,
|
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
listener,
|
listener,
|
||||||
Response.ErrorListener {
|
@Singleton
|
||||||
if (!canBeUnauthorized && it is AuthFailureError) {
|
object : Response.ErrorListener {
|
||||||
runBlocking {
|
override fun onErrorResponse(error: VolleyError?) {
|
||||||
context.settingsDataStore.updateData { currentSettings ->
|
val context = appContainer.context
|
||||||
currentSettings.toBuilder().setUserId("")
|
|
||||||
.setAccessToken("").build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (context.profileViewModel != null)
|
|
||||||
context.profileViewModel!!.onUnauthorized()
|
|
||||||
}
|
|
||||||
|
|
||||||
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<String, String> {
|
override fun getHeaders(): MutableMap<String, String> {
|
||||||
val accessToken = runBlocking {
|
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)
|
// TODO: если не авторизован
|
||||||
context.profileViewModel!!.onUnauthorized()
|
// if (accessToken.isEmpty() && context.profileViewModel != null)
|
||||||
|
// context.profileViewModel!!.onUnauthorized()
|
||||||
|
|
||||||
val headers = super.getHeaders()
|
val headers = super.getHeaders()
|
||||||
headers["Authorization"] = "Bearer $accessToken"
|
headers["Authorization"] = "Bearer $accessToken"
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val appContext get() = appContainer.context
|
||||||
}
|
}
|
||||||
@@ -7,31 +7,36 @@ import com.android.volley.toolbox.StringRequest
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
import ru.n08i40k.polytechnic.next.Application
|
||||||
import ru.n08i40k.polytechnic.next.data.AppContainer
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
import ru.n08i40k.polytechnic.next.app.appContainer
|
||||||
import ru.n08i40k.polytechnic.next.network.NetworkConnection
|
import ru.n08i40k.polytechnic.next.network.NetworkConnection
|
||||||
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetCacheStatus
|
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetCacheStatus
|
||||||
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleUpdate
|
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleUpdate
|
||||||
import ru.n08i40k.polytechnic.next.network.tryFuture
|
import ru.n08i40k.polytechnic.next.network.tryFuture
|
||||||
import ru.n08i40k.polytechnic.next.network.tryGet
|
import ru.n08i40k.polytechnic.next.network.tryGet
|
||||||
|
import ru.n08i40k.polytechnic.next.utils.MyResult
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
open class CachedRequest(
|
open class CachedRequest(
|
||||||
context: Context,
|
appContainer: AppContainer,
|
||||||
method: Int,
|
method: Int,
|
||||||
private val url: String,
|
private val url: String,
|
||||||
private val listener: Response.Listener<String>,
|
private val listener: Response.Listener<String>,
|
||||||
errorListener: Response.ErrorListener?,
|
errorListener: Response.ErrorListener?,
|
||||||
) : AuthorizedRequest(context, method, url, {
|
) : AuthorizedRequest(
|
||||||
runBlocking(Dispatchers.IO) {
|
appContainer,
|
||||||
(context as PolytechnicApplication)
|
method,
|
||||||
.container.networkCacheRepository.put(url, it)
|
url,
|
||||||
}
|
{
|
||||||
listener.onResponse(it)
|
runBlocking(Dispatchers.IO) {
|
||||||
}, errorListener) {
|
appContainer.networkCacheRepository.put(url, it)
|
||||||
private val appContainer: AppContainer = (context as PolytechnicApplication).container
|
}
|
||||||
|
listener.onResponse(it)
|
||||||
|
},
|
||||||
|
errorListener
|
||||||
|
) {
|
||||||
|
|
||||||
private suspend fun getXlsUrl(): MyResult<String> = withContext(Dispatchers.IO) {
|
private suspend fun getXlsUrl(): MyResult<String> = withContext(Dispatchers.IO) {
|
||||||
val mainPageFuture = RequestFuture.newFuture<String>()
|
val mainPageFuture = RequestFuture.newFuture<String>()
|
||||||
@@ -41,7 +46,7 @@ open class CachedRequest(
|
|||||||
mainPageFuture,
|
mainPageFuture,
|
||||||
mainPageFuture
|
mainPageFuture
|
||||||
)
|
)
|
||||||
NetworkConnection.getInstance(context).addToRequestQueue(request)
|
NetworkConnection.getInstance(appContext).addToRequestQueue(request)
|
||||||
|
|
||||||
val response = tryGet(mainPageFuture)
|
val response = tryGet(mainPageFuture)
|
||||||
if (response is MyResult.Failure)
|
if (response is MyResult.Failure)
|
||||||
@@ -49,7 +54,8 @@ open class CachedRequest(
|
|||||||
|
|
||||||
val pageData = (response as MyResult.Success).data
|
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 =
|
val pattern: Pattern =
|
||||||
Pattern.compile(remoteConfig.getString("linkParserRegex"), Pattern.MULTILINE)
|
Pattern.compile(remoteConfig.getString("linkParserRegex"), Pattern.MULTILINE)
|
||||||
@@ -67,10 +73,10 @@ open class CachedRequest(
|
|||||||
when (val xlsUrl = getXlsUrl()) {
|
when (val xlsUrl = getXlsUrl()) {
|
||||||
is MyResult.Failure -> xlsUrl
|
is MyResult.Failure -> xlsUrl
|
||||||
is MyResult.Success -> {
|
is MyResult.Success -> {
|
||||||
tryFuture {
|
tryFuture(appContext) { it ->
|
||||||
ScheduleUpdate(
|
ScheduleUpdate(
|
||||||
|
appContext.appContainer,
|
||||||
ScheduleUpdate.RequestDto(xlsUrl.data),
|
ScheduleUpdate.RequestDto(xlsUrl.data),
|
||||||
context,
|
|
||||||
it,
|
it,
|
||||||
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 logger = Logger.getLogger("CachedRequest")
|
||||||
val repository = appContainer.networkCacheRepository
|
val cache = appContainer.networkCacheRepository
|
||||||
|
|
||||||
val cacheStatusResult = tryFuture {
|
val cacheStatusResult = tryFuture(context) {
|
||||||
ScheduleGetCacheStatus(context, it, it)
|
ScheduleGetCacheStatus(appContainer, it, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cacheStatusResult is MyResult.Success) {
|
if (cacheStatusResult is MyResult.Success) {
|
||||||
val cacheStatus = cacheStatusResult.data
|
val cacheStatus = cacheStatusResult.data
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
repository.setUpdateDates(
|
cache.setUpdateDates(
|
||||||
cacheStatus.lastCacheUpdate,
|
cacheStatus.lastCacheUpdate,
|
||||||
cacheStatus.lastScheduleUpdate
|
cacheStatus.lastScheduleUpdate
|
||||||
)
|
)
|
||||||
repository.setHash(cacheStatus.cacheHash)
|
cache.setHash(cacheStatus.cacheHash)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cacheStatus.cacheUpdateRequired) {
|
if (cacheStatus.cacheUpdateRequired) {
|
||||||
@@ -105,11 +112,11 @@ open class CachedRequest(
|
|||||||
when (updateResult) {
|
when (updateResult) {
|
||||||
is MyResult.Success -> {
|
is MyResult.Success -> {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
repository.setUpdateDates(
|
cache.setUpdateDates(
|
||||||
updateResult.data.lastCacheUpdate,
|
updateResult.data.lastCacheUpdate,
|
||||||
updateResult.data.lastScheduleUpdate
|
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!")
|
logger.warning("Failed to get cache status!")
|
||||||
}
|
}
|
||||||
|
|
||||||
val cachedResponse = runBlocking { repository.get(url) }
|
val cachedResponse = runBlocking { cache.get(url) }
|
||||||
if (cachedResponse != null) {
|
if (cachedResponse != null) {
|
||||||
listener.onResponse(cachedResponse.data)
|
listener.onResponse(cachedResponse.data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
super.send()
|
super.send(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.auth
|
package ru.n08i40k.polytechnic.next.network.request.auth
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||||
|
|
||||||
class AuthChangePassword(
|
class AuthChangePassword(
|
||||||
|
appContainer: AppContainer,
|
||||||
private val data: RequestDto,
|
private val data: RequestDto,
|
||||||
context: Context,
|
|
||||||
listener: Response.Listener<Nothing>,
|
listener: Response.Listener<Nothing>,
|
||||||
errorListener: Response.ErrorListener?
|
errorListener: Response.ErrorListener?
|
||||||
) : AuthorizedRequest(
|
) : AuthorizedRequest(
|
||||||
context,
|
appContainer,
|
||||||
Method.POST,
|
Method.POST,
|
||||||
"v1/auth/change-password",
|
"v1/auth/change-password",
|
||||||
{ listener.onResponse(null) },
|
{ listener.onResponse(null) },
|
||||||
|
|||||||
@@ -1,25 +1,45 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.auth
|
package ru.n08i40k.polytechnic.next.network.request.auth
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
|
import com.android.volley.VolleyError
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import ru.n08i40k.polytechnic.next.model.Profile
|
import ru.n08i40k.polytechnic.next.model.Profile
|
||||||
import ru.n08i40k.polytechnic.next.network.RequestBase
|
import ru.n08i40k.polytechnic.next.network.RequestBase
|
||||||
|
import ru.n08i40k.polytechnic.next.utils.EnumAsStringSerializer
|
||||||
|
|
||||||
class AuthSignIn(
|
class AuthSignIn(
|
||||||
private val data: RequestDto,
|
private val data: RequestDto,
|
||||||
context: Context,
|
|
||||||
listener: Response.Listener<Profile>,
|
listener: Response.Listener<Profile>,
|
||||||
errorListener: Response.ErrorListener?
|
errorListener: Response.ErrorListener?
|
||||||
) : RequestBase(
|
) : RequestBase(
|
||||||
context,
|
|
||||||
Method.POST,
|
Method.POST,
|
||||||
"v2/auth/sign-in",
|
"v1/auth/sign-in",
|
||||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||||
errorListener
|
errorListener
|
||||||
) {
|
) {
|
||||||
|
companion object {
|
||||||
|
private class ErrorCodeSerializer : EnumAsStringSerializer<ErrorCode>(
|
||||||
|
"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>(error.networkResponse.data.decodeToString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RequestDto(val username: String, val password: String)
|
data class RequestDto(val username: String, val password: String)
|
||||||
|
|
||||||
|
|||||||
@@ -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<Profile>,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,56 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.auth
|
package ru.n08i40k.polytechnic.next.network.request.auth
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
|
import com.android.volley.VolleyError
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import ru.n08i40k.polytechnic.next.model.Profile
|
import ru.n08i40k.polytechnic.next.model.Profile
|
||||||
import ru.n08i40k.polytechnic.next.model.UserRole
|
import ru.n08i40k.polytechnic.next.model.UserRole
|
||||||
import ru.n08i40k.polytechnic.next.network.RequestBase
|
import ru.n08i40k.polytechnic.next.network.RequestBase
|
||||||
|
import ru.n08i40k.polytechnic.next.utils.EnumAsStringSerializer
|
||||||
|
|
||||||
class AuthSignUp(
|
class AuthSignUp(
|
||||||
private val data: RequestDto,
|
private val data: RequestDto,
|
||||||
context: Context,
|
|
||||||
listener: Response.Listener<Profile>,
|
listener: Response.Listener<Profile>,
|
||||||
errorListener: Response.ErrorListener?
|
errorListener: Response.ErrorListener?
|
||||||
) : RequestBase(
|
) : RequestBase(
|
||||||
context,
|
|
||||||
Method.POST,
|
Method.POST,
|
||||||
"v2/auth/sign-up",
|
"v1/auth/sign-up",
|
||||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||||
errorListener
|
errorListener
|
||||||
) {
|
) {
|
||||||
|
companion object {
|
||||||
|
private class ErrorCodeSerializer : EnumAsStringSerializer<ErrorCode>(
|
||||||
|
"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>(error.networkResponse.data.decodeToString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class RequestDto(
|
data class RequestDto(
|
||||||
val username: String,
|
val username: String,
|
||||||
val password: String,
|
val password: String,
|
||||||
val group: String,
|
val group: String,
|
||||||
val role: UserRole
|
val role: UserRole,
|
||||||
|
val version: String
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun getBody(): ByteArray {
|
override fun getBody(): ByteArray {
|
||||||
|
|||||||
@@ -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<Profile>,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,26 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.fcm
|
package ru.n08i40k.polytechnic.next.network.request.fcm
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||||
|
|
||||||
class FcmSetToken(
|
class FcmSetToken(
|
||||||
context: Context,
|
appContainer: AppContainer,
|
||||||
token: String,
|
token: String,
|
||||||
listener: Response.Listener<Unit>,
|
listener: Response.Listener<Unit>,
|
||||||
errorListener: Response.ErrorListener?,
|
errorListener: Response.ErrorListener?,
|
||||||
) : AuthorizedRequest(
|
) : AuthorizedRequest(
|
||||||
context, Method.POST,
|
appContainer,
|
||||||
"v1/fcm/set-token/$token",
|
Method.PATCH,
|
||||||
|
"v1/fcm/set-token?token=$token",
|
||||||
{ listener.onResponse(Unit) },
|
{ listener.onResponse(Unit) },
|
||||||
errorListener,
|
errorListener,
|
||||||
true
|
true
|
||||||
)
|
) {
|
||||||
|
override fun getHeaders(): MutableMap<String, String> {
|
||||||
|
val headers = super.getHeaders()
|
||||||
|
headers.remove("Content-Type")
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.fcm
|
package ru.n08i40k.polytechnic.next.network.request.fcm
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||||
|
|
||||||
|
// TODO: вернуть
|
||||||
|
@Suppress("unused")
|
||||||
class FcmUpdateCallback(
|
class FcmUpdateCallback(
|
||||||
context: Context,
|
appContainer: AppContainer,
|
||||||
version: String,
|
version: String,
|
||||||
listener: Response.Listener<Unit>,
|
listener: Response.Listener<Unit>,
|
||||||
errorListener: Response.ErrorListener?,
|
errorListener: Response.ErrorListener?,
|
||||||
) : AuthorizedRequest(
|
) : AuthorizedRequest(
|
||||||
context, Method.POST,
|
appContainer,
|
||||||
|
Method.POST,
|
||||||
"v1/fcm/update-callback/$version",
|
"v1/fcm/update-callback/$version",
|
||||||
{ listener.onResponse(Unit) },
|
{ listener.onResponse(Unit) },
|
||||||
errorListener,
|
errorListener,
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.profile
|
package ru.n08i40k.polytechnic.next.network.request.profile
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||||
|
|
||||||
class ProfileChangeGroup(
|
class ProfileChangeGroup(
|
||||||
|
appContainer: AppContainer,
|
||||||
private val data: RequestDto,
|
private val data: RequestDto,
|
||||||
context: Context,
|
|
||||||
listener: Response.Listener<Nothing>,
|
listener: Response.Listener<Nothing>,
|
||||||
errorListener: Response.ErrorListener?
|
errorListener: Response.ErrorListener?
|
||||||
) : AuthorizedRequest(
|
) : AuthorizedRequest(
|
||||||
context,
|
appContainer,
|
||||||
Method.POST,
|
Method.POST,
|
||||||
"v1/users/change-group",
|
"v1/users/change-group",
|
||||||
{ listener.onResponse(null) },
|
{ listener.onResponse(null) },
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.profile
|
package ru.n08i40k.polytechnic.next.network.request.profile
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||||
|
|
||||||
class ProfileChangeUsername(
|
class ProfileChangeUsername(
|
||||||
|
appContainer: AppContainer,
|
||||||
private val data: RequestDto,
|
private val data: RequestDto,
|
||||||
context: Context,
|
|
||||||
listener: Response.Listener<Nothing>,
|
listener: Response.Listener<Nothing>,
|
||||||
errorListener: Response.ErrorListener?
|
errorListener: Response.ErrorListener?
|
||||||
) : AuthorizedRequest(
|
) : AuthorizedRequest(
|
||||||
context,
|
appContainer,
|
||||||
Method.POST,
|
Method.POST,
|
||||||
"v1/users/change-username",
|
"v1/users/change-username",
|
||||||
{ listener.onResponse(null) },
|
{ listener.onResponse(null) },
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.profile
|
package ru.n08i40k.polytechnic.next.network.request.profile
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.model.Profile
|
import ru.n08i40k.polytechnic.next.model.Profile
|
||||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||||
|
|
||||||
class ProfileMe(
|
class ProfileMe(
|
||||||
context: Context,
|
appContainer: AppContainer,
|
||||||
listener: Response.Listener<Profile>,
|
listener: Response.Listener<Profile>,
|
||||||
errorListener: Response.ErrorListener?
|
errorListener: Response.ErrorListener?
|
||||||
) : AuthorizedRequest(
|
) : AuthorizedRequest(
|
||||||
context,
|
appContainer,
|
||||||
Method.GET,
|
Method.GET,
|
||||||
"v2/users/me",
|
"v1/users/me",
|
||||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||||
errorListener
|
errorListener
|
||||||
)
|
)
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.schedule
|
package ru.n08i40k.polytechnic.next.network.request.schedule
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
|
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
|
||||||
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
|
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
|
||||||
|
|
||||||
class ScheduleGet(
|
class ScheduleGet(
|
||||||
context: Context,
|
appContainer: AppContainer,
|
||||||
listener: Response.Listener<ResponseDto>,
|
listener: Response.Listener<ResponseDto>,
|
||||||
errorListener: Response.ErrorListener? = null
|
errorListener: Response.ErrorListener? = null
|
||||||
) : CachedRequest(
|
) : CachedRequest(
|
||||||
context,
|
appContainer,
|
||||||
Method.GET,
|
Method.GET,
|
||||||
"v4/schedule/group",
|
"v1/schedule/group",
|
||||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||||
errorListener
|
errorListener
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.schedule
|
package ru.n08i40k.polytechnic.next.network.request.schedule
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||||
|
|
||||||
class ScheduleGetCacheStatus(
|
class ScheduleGetCacheStatus(
|
||||||
context: Context,
|
appContainer: AppContainer,
|
||||||
listener: Response.Listener<ResponseDto>,
|
listener: Response.Listener<ResponseDto>,
|
||||||
errorListener: Response.ErrorListener? = null
|
errorListener: Response.ErrorListener? = null
|
||||||
) : AuthorizedRequest(
|
) : AuthorizedRequest(
|
||||||
context,
|
appContainer,
|
||||||
Method.GET,
|
Method.GET,
|
||||||
"v2/schedule/cache-status",
|
"v1/schedule/cache-status",
|
||||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||||
errorListener
|
errorListener
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.schedule
|
package ru.n08i40k.polytechnic.next.network.request.schedule
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import ru.n08i40k.polytechnic.next.network.RequestBase
|
import ru.n08i40k.polytechnic.next.network.RequestBase
|
||||||
|
|
||||||
class ScheduleGetGroupNames(
|
class ScheduleGetGroupNames(
|
||||||
context: Context,
|
|
||||||
listener: Response.Listener<ResponseDto>,
|
listener: Response.Listener<ResponseDto>,
|
||||||
errorListener: Response.ErrorListener? = null
|
errorListener: Response.ErrorListener? = null
|
||||||
) : RequestBase(
|
) : RequestBase(
|
||||||
context,
|
|
||||||
Method.GET,
|
Method.GET,
|
||||||
"v2/schedule/group-names",
|
"v1/schedule/group-names",
|
||||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||||
errorListener
|
errorListener
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.schedule
|
package ru.n08i40k.polytechnic.next.network.request.schedule
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
|
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
|
||||||
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
|
import ru.n08i40k.polytechnic.next.network.request.CachedRequest
|
||||||
|
|
||||||
class ScheduleGetTeacher(
|
class ScheduleGetTeacher(
|
||||||
context: Context,
|
appContainer: AppContainer,
|
||||||
teacher: String,
|
teacher: String,
|
||||||
listener: Response.Listener<ResponseDto>,
|
listener: Response.Listener<ResponseDto>,
|
||||||
errorListener: Response.ErrorListener? = null
|
errorListener: Response.ErrorListener? = null
|
||||||
) : CachedRequest(
|
) : CachedRequest(
|
||||||
context,
|
appContainer,
|
||||||
Method.GET,
|
Method.GET,
|
||||||
"v3/schedule/teacher/$teacher",
|
"v1/schedule/teacher/$teacher",
|
||||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||||
errorListener
|
errorListener
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.schedule
|
package ru.n08i40k.polytechnic.next.network.request.schedule
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import ru.n08i40k.polytechnic.next.network.RequestBase
|
import ru.n08i40k.polytechnic.next.network.RequestBase
|
||||||
|
|
||||||
class ScheduleGetTeacherNames(
|
class ScheduleGetTeacherNames(
|
||||||
context: Context,
|
|
||||||
listener: Response.Listener<ResponseDto>,
|
listener: Response.Listener<ResponseDto>,
|
||||||
errorListener: Response.ErrorListener? = null
|
errorListener: Response.ErrorListener? = null
|
||||||
) : RequestBase(
|
) : RequestBase(
|
||||||
context,
|
|
||||||
Method.GET,
|
Method.GET,
|
||||||
"v2/schedule/teacher-names",
|
"v1/schedule/teacher-names",
|
||||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||||
errorListener
|
errorListener
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.schedule
|
package ru.n08i40k.polytechnic.next.network.request.schedule
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||||
|
|
||||||
class ScheduleUpdate(
|
class ScheduleUpdate(
|
||||||
|
appContainer: AppContainer,
|
||||||
private val data: RequestDto,
|
private val data: RequestDto,
|
||||||
context: Context,
|
|
||||||
listener: Response.Listener<ScheduleGetCacheStatus.ResponseDto>,
|
listener: Response.Listener<ScheduleGetCacheStatus.ResponseDto>,
|
||||||
errorListener: Response.ErrorListener? = null
|
errorListener: Response.ErrorListener? = null
|
||||||
) : AuthorizedRequest(
|
) : AuthorizedRequest(
|
||||||
context,
|
appContainer,
|
||||||
Method.PATCH,
|
Method.PATCH,
|
||||||
"v4/schedule/update-download-url",
|
"v1/schedule/update-download-url",
|
||||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||||
errorListener
|
errorListener
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.scheduleReplacer
|
package ru.n08i40k.polytechnic.next.network.request.scheduleReplacer
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||||
|
|
||||||
|
// TODO: вернуть
|
||||||
|
@Suppress("unused")
|
||||||
class ScheduleReplacerClear(
|
class ScheduleReplacerClear(
|
||||||
context: Context,
|
appContainer: AppContainer,
|
||||||
listener: Response.Listener<ResponseDto>,
|
listener: Response.Listener<ResponseDto>,
|
||||||
errorListener: Response.ErrorListener?
|
errorListener: Response.ErrorListener?
|
||||||
) : AuthorizedRequest(
|
) : AuthorizedRequest(
|
||||||
context,
|
appContainer,
|
||||||
Method.POST,
|
Method.POST,
|
||||||
"v1/schedule-replacer/clear",
|
"v1/schedule-replacer/clear",
|
||||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.scheduleReplacer
|
package ru.n08i40k.polytechnic.next.network.request.scheduleReplacer
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
import com.android.volley.Response
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
|
import ru.n08i40k.polytechnic.next.model.ScheduleReplacer
|
||||||
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
import ru.n08i40k.polytechnic.next.network.request.AuthorizedRequest
|
||||||
|
|
||||||
|
// TODO: вернуть
|
||||||
|
@Suppress("unused")
|
||||||
class ScheduleReplacerGet(
|
class ScheduleReplacerGet(
|
||||||
context: Context,
|
appContainer: AppContainer,
|
||||||
listener: Response.Listener<List<ScheduleReplacer>>,
|
listener: Response.Listener<List<ScheduleReplacer>>,
|
||||||
errorListener: Response.ErrorListener?
|
errorListener: Response.ErrorListener?
|
||||||
) : AuthorizedRequest(
|
) : AuthorizedRequest(
|
||||||
context,
|
appContainer,
|
||||||
Method.GET,
|
Method.GET,
|
||||||
"v1/schedule-replacer/get",
|
"v1/schedule-replacer/get",
|
||||||
{ listener.onResponse(Json.decodeFromString(it)) },
|
{ listener.onResponse(Json.decodeFromString(it)) },
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
package ru.n08i40k.polytechnic.next.network.request.scheduleReplacer
|
package ru.n08i40k.polytechnic.next.network.request.scheduleReplacer
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.android.volley.Response
|
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(
|
class ScheduleReplacerSet(
|
||||||
context: Context,
|
appContainer: AppContainer,
|
||||||
private val fileName: String,
|
private val fileName: String,
|
||||||
private val fileData: ByteArray,
|
private val fileData: ByteArray,
|
||||||
private val fileType: String,
|
private val fileType: String,
|
||||||
private val listener: Response.Listener<Nothing>,
|
private val listener: Response.Listener<Nothing>,
|
||||||
errorListener: Response.ErrorListener?
|
errorListener: Response.ErrorListener?
|
||||||
) : AuthorizedMultipartRequest(
|
) : AuthorizedMultipartRequest(
|
||||||
context,
|
appContainer,
|
||||||
Method.POST,
|
Method.POST,
|
||||||
"v1/schedule-replacer/set",
|
"v1/schedule-replacer/set",
|
||||||
{ listener.onResponse(null) },
|
{ listener.onResponse(null) },
|
||||||
|
|||||||
@@ -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<ResponseDto>,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.CachedResponse
|
||||||
import ru.n08i40k.polytechnic.next.UpdateDates
|
import ru.n08i40k.polytechnic.next.UpdateDates
|
||||||
@@ -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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@@ -8,23 +7,26 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import ru.n08i40k.polytechnic.next.CachedResponse
|
import ru.n08i40k.polytechnic.next.CachedResponse
|
||||||
import ru.n08i40k.polytechnic.next.UpdateDates
|
import ru.n08i40k.polytechnic.next.UpdateDates
|
||||||
import ru.n08i40k.polytechnic.next.data.cache.NetworkCacheRepository
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
import ru.n08i40k.polytechnic.next.repository.cache.NetworkCacheRepository
|
||||||
|
import ru.n08i40k.polytechnic.next.settings.settings
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LocalNetworkCacheRepository
|
class LocalNetworkCacheRepository
|
||||||
@Inject constructor(private val applicationContext: Context) : NetworkCacheRepository {
|
@Inject constructor(private val appContainer: AppContainer) : NetworkCacheRepository {
|
||||||
private val cacheMap: MutableMap<String, CachedResponse> = mutableMapOf()
|
private val cacheMap: MutableMap<String, CachedResponse> = mutableMapOf()
|
||||||
private var updateDates: UpdateDates = UpdateDates.newBuilder().build()
|
private var updateDates: UpdateDates = UpdateDates.newBuilder().build()
|
||||||
private var hash: String? = null
|
private var hash: String? = null
|
||||||
|
|
||||||
|
private val context get() = appContainer.context
|
||||||
|
|
||||||
init {
|
init {
|
||||||
cacheMap.clear()
|
cacheMap.clear()
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
cacheMap.putAll(
|
cacheMap.putAll(
|
||||||
applicationContext
|
context
|
||||||
.settingsDataStore
|
.settings
|
||||||
.data
|
.data
|
||||||
.map { settings -> settings.cacheStorageMap }.first()
|
.map { settings -> settings.cacheStorageMap }.first()
|
||||||
)
|
)
|
||||||
@@ -32,7 +34,7 @@ class LocalNetworkCacheRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun get(url: String): CachedResponse? {
|
override suspend fun get(url: String): CachedResponse? {
|
||||||
// Если кешированого ответа нет, то возвращаем null
|
// Если кешированного ответа нет, то возвращаем null
|
||||||
// Если хеши не совпадают и локальный хеш присутствует, то возвращаем null
|
// Если хеши не совпадают и локальный хеш присутствует, то возвращаем null
|
||||||
|
|
||||||
val response = cacheMap[url] ?: return null
|
val response = cacheMap[url] ?: return null
|
||||||
@@ -92,7 +94,7 @@ class LocalNetworkCacheRepository
|
|||||||
.setSchedule(schedule).build()
|
.setSchedule(schedule).build()
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
applicationContext.settingsDataStore.updateData {
|
context.settings.updateData {
|
||||||
it
|
it
|
||||||
.toBuilder()
|
.toBuilder()
|
||||||
.setUpdateDates(updateDates)
|
.setUpdateDates(updateDates)
|
||||||
@@ -104,7 +106,7 @@ class LocalNetworkCacheRepository
|
|||||||
|
|
||||||
private suspend fun save() {
|
private suspend fun save() {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
applicationContext.settingsDataStore.updateData {
|
context.settings.updateData {
|
||||||
it
|
it
|
||||||
.toBuilder()
|
.toBuilder()
|
||||||
.putAllCacheStorage(cacheMap)
|
.putAllCacheStorage(cacheMap)
|
||||||
@@ -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.CachedResponse
|
||||||
import ru.n08i40k.polytechnic.next.UpdateDates
|
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? {
|
override suspend fun get(url: String): CachedResponse? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -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<Profile>
|
||||||
|
|
||||||
|
suspend fun setFCMToken(token: String): MyResult<Unit>
|
||||||
|
|
||||||
|
suspend fun signOut()
|
||||||
|
}
|
||||||
@@ -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<Profile> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
delay(1500)
|
||||||
|
|
||||||
|
if (++getCounter % 3 == 0)
|
||||||
|
MyResult.Failure(Exception())
|
||||||
|
else
|
||||||
|
MyResult.Success(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setFCMToken(token: String): MyResult<Unit> =
|
||||||
|
MyResult.Success(Unit)
|
||||||
|
|
||||||
|
override suspend fun signOut() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Profile> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
tryFuture(container.context) {
|
||||||
|
ProfileMe(
|
||||||
|
container,
|
||||||
|
it,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setFCMToken(token: String): MyResult<Unit> =
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.model.GroupOrTeacher
|
||||||
|
import ru.n08i40k.polytechnic.next.utils.MyResult
|
||||||
|
|
||||||
interface ScheduleRepository {
|
interface ScheduleRepository {
|
||||||
suspend fun getGroup(): MyResult<GroupOrTeacher>
|
suspend fun getGroup(): MyResult<GroupOrTeacher>
|
||||||
@@ -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.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.toInstant
|
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.Day
|
||||||
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
|
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
|
||||||
import ru.n08i40k.polytechnic.next.model.Lesson
|
import ru.n08i40k.polytechnic.next.model.Lesson
|
||||||
import ru.n08i40k.polytechnic.next.model.LessonTime
|
import ru.n08i40k.polytechnic.next.model.LessonTime
|
||||||
import ru.n08i40k.polytechnic.next.model.LessonType
|
import ru.n08i40k.polytechnic.next.model.LessonType
|
||||||
import ru.n08i40k.polytechnic.next.model.SubGroup
|
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
|
import ru.n08i40k.polytechnic.next.utils.now
|
||||||
|
|
||||||
private fun genLocalDateTime(hour: Int, minute: Int): Instant {
|
private fun genLocalDateTime(hour: Int, minute: Int): Instant {
|
||||||
@@ -36,8 +35,7 @@ private fun genBreak(start: Instant, end: Instant): Lesson {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
class FakeScheduleRepository : ScheduleRepository {
|
class MockScheduleRepository : ScheduleRepository {
|
||||||
@Suppress("SpellCheckingInspection")
|
|
||||||
companion object {
|
companion object {
|
||||||
val exampleGroup = GroupOrTeacher(
|
val exampleGroup = GroupOrTeacher(
|
||||||
name = "ИС-214/23", days = arrayListOf(
|
name = "ИС-214/23", days = arrayListOf(
|
||||||
@@ -144,7 +142,8 @@ class FakeScheduleRepository : ScheduleRepository {
|
|||||||
),
|
),
|
||||||
group = null
|
group = null
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
street = "Железнодорожная 13",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -254,34 +253,28 @@ class FakeScheduleRepository : ScheduleRepository {
|
|||||||
),
|
),
|
||||||
group = "ИС-214/23"
|
group = "ИС-214/23"
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
null
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val group = MutableStateFlow<GroupOrTeacher?>(exampleGroup)
|
|
||||||
private val teacher = MutableStateFlow<GroupOrTeacher?>(exampleTeacher)
|
|
||||||
|
|
||||||
private var updateCounter: Int = 0
|
private var updateCounter: Int = 0
|
||||||
|
|
||||||
override suspend fun getGroup(): MyResult<GroupOrTeacher> {
|
override suspend fun getGroup(): MyResult<GroupOrTeacher> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
delay(1500)
|
delay(1500)
|
||||||
if (updateCounter++ % 3 == 0) MyResult.Failure(
|
if (updateCounter++ % 3 == 0) MyResult.Failure()
|
||||||
IllegalStateException()
|
else MyResult.Success(exampleGroup)
|
||||||
)
|
|
||||||
else MyResult.Success(group.value!!)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getTeacher(name: String): MyResult<GroupOrTeacher> {
|
override suspend fun getTeacher(name: String): MyResult<GroupOrTeacher> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
delay(1500)
|
delay(1500)
|
||||||
if (updateCounter++ % 3 == 0) MyResult.Failure(
|
if (updateCounter++ % 3 == 0) MyResult.Failure()
|
||||||
IllegalStateException()
|
else MyResult.Success(exampleTeacher)
|
||||||
)
|
|
||||||
else MyResult.Success(teacher.value!!)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
|
|
||||||
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
|
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
|
||||||
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGet
|
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGet
|
||||||
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetTeacher
|
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetTeacher
|
||||||
import ru.n08i40k.polytechnic.next.network.tryFuture
|
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<GroupOrTeacher> =
|
override suspend fun getGroup(): MyResult<GroupOrTeacher> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val response = tryFuture {
|
val response = tryFuture(context) {
|
||||||
ScheduleGet(
|
ScheduleGet(
|
||||||
context,
|
container,
|
||||||
it,
|
it,
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (response) {
|
when (response) {
|
||||||
is MyResult.Failure -> response
|
|
||||||
is MyResult.Success -> MyResult.Success(response.data.group)
|
is MyResult.Success -> MyResult.Success(response.data.group)
|
||||||
|
is MyResult.Failure -> response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getTeacher(name: String): MyResult<GroupOrTeacher> =
|
override suspend fun getTeacher(name: String): MyResult<GroupOrTeacher> =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val response = tryFuture {
|
val response = tryFuture(context) {
|
||||||
ScheduleGetTeacher(
|
ScheduleGetTeacher(
|
||||||
context,
|
container,
|
||||||
name,
|
name,
|
||||||
it,
|
it,
|
||||||
it
|
it
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<FcmSetTokenWorker>()
|
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,18 +13,17 @@ import java.io.OutputStream
|
|||||||
object SettingsSerializer : Serializer<Settings> {
|
object SettingsSerializer : Serializer<Settings> {
|
||||||
override val defaultValue: Settings = Settings.getDefaultInstance()
|
override val defaultValue: Settings = Settings.getDefaultInstance()
|
||||||
|
|
||||||
override suspend fun readFrom(input: InputStream): Settings {
|
override suspend fun readFrom(input: InputStream): Settings =
|
||||||
try {
|
try {
|
||||||
return Settings.parseFrom(input)
|
Settings.parseFrom(input)
|
||||||
} catch (exception: InvalidProtocolBufferException) {
|
} catch (exception: InvalidProtocolBufferException) {
|
||||||
throw CorruptionException("Cannot read proto.", exception)
|
throw CorruptionException("Cannot read proto.", exception)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
|
override suspend fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
val Context.settingsDataStore: DataStore<Settings> by dataStore(
|
val Context.settings: DataStore<Settings> by dataStore(
|
||||||
fileName = "settings.pb",
|
fileName = "settings.pb",
|
||||||
serializer = SettingsSerializer
|
serializer = SettingsSerializer
|
||||||
)
|
)
|
||||||
@@ -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<FcmUpdateCallbackWorker>()
|
|
||||||
.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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +1,276 @@
|
|||||||
package ru.n08i40k.polytechnic.next.ui
|
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.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.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.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
import ru.n08i40k.polytechnic.next.Application
|
||||||
import ru.n08i40k.polytechnic.next.ui.auth.AuthScreen
|
import ru.n08i40k.polytechnic.next.R
|
||||||
import ru.n08i40k.polytechnic.next.ui.main.MainScreen
|
import ru.n08i40k.polytechnic.next.settings.settings
|
||||||
import ru.n08i40k.polytechnic.next.ui.theme.AppTheme
|
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<SemVersion> {
|
||||||
|
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
|
@Composable
|
||||||
fun PolytechnicApp() {
|
fun PolytechnicApp() {
|
||||||
AppTheme(darkTheme = true, content = {
|
if (!checkUpdate())
|
||||||
val navController = rememberNavController()
|
return
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
val accessToken = runBlocking {
|
val navController = rememberNavController()
|
||||||
context.settingsDataStore.data.map { it.accessToken }.first()
|
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(
|
composable(AppRoute.MAIN.route) {
|
||||||
navController = navController,
|
MainScreen(navController)
|
||||||
startDestination = if (accessToken.isEmpty()) "auth" else "main"
|
|
||||||
) {
|
|
||||||
composable(route = "auth") {
|
|
||||||
AuthScreen(navController)
|
|
||||||
}
|
|
||||||
|
|
||||||
composable(route = "main") {
|
|
||||||
MainScreen(navController)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<String?>(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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<T>(
|
||||||
|
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 <T> rememberInputValue(
|
||||||
|
defaultValue: T,
|
||||||
|
checkNow: Boolean = false,
|
||||||
|
errorCheck: (T) -> Boolean = { false }
|
||||||
|
): MutableState<InputValue<T>> {
|
||||||
|
return remember { mutableStateOf(InputValue<T>(defaultValue, errorCheck, checkNow)) }
|
||||||
|
}
|
||||||
@@ -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 = "")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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<GroupScheduleViewModel>(LocalContext.current as ComponentActivity)
|
|
||||||
|
|
||||||
// teacher view model
|
|
||||||
val teacherScheduleViewModel =
|
|
||||||
hiltViewModel<TeacherScheduleViewModel>(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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<GroupScheduleViewModel>(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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Uri?>(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<ScheduleReplacer> = FakeScheduleReplacerRepository.exampleReplacers) {
|
|
||||||
Surface {
|
|
||||||
LazyColumn(
|
|
||||||
contentPadding = PaddingValues(0.dp, 5.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(500.dp)
|
|
||||||
) {
|
|
||||||
items(replacers) {
|
|
||||||
ReplacerElement(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<String> {
|
|
||||||
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<Boolean> = 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<String> =
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<String>,
|
|
||||||
) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<String> {
|
|
||||||
val teacherNames = remember { arrayListOf<String>() }
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,77 +10,76 @@ import kotlinx.coroutines.flow.stateIn
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.n08i40k.polytechnic.next.UpdateDates
|
import ru.n08i40k.polytechnic.next.UpdateDates
|
||||||
import ru.n08i40k.polytechnic.next.data.AppContainer
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
|
||||||
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
|
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
|
||||||
|
import ru.n08i40k.polytechnic.next.utils.MyResult
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
sealed interface GroupScheduleUiState {
|
sealed interface GroupUiState {
|
||||||
val isLoading: Boolean
|
val isLoading: Boolean
|
||||||
|
|
||||||
data class NoData(
|
data class NoData(
|
||||||
override val isLoading: Boolean
|
override val isLoading: Boolean
|
||||||
) : GroupScheduleUiState
|
) : GroupUiState
|
||||||
|
|
||||||
data class HasData(
|
data class HasData(
|
||||||
val group: GroupOrTeacher,
|
val group: GroupOrTeacher,
|
||||||
val updateDates: UpdateDates,
|
val updateDates: UpdateDates,
|
||||||
val lastUpdateAt: Long,
|
val lastUpdateAt: Long,
|
||||||
override val isLoading: Boolean
|
override val isLoading: Boolean
|
||||||
) : GroupScheduleUiState
|
) : GroupUiState
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class GroupScheduleViewModelState(
|
private data class GroupViewModelState(
|
||||||
val group: GroupOrTeacher? = null,
|
val group: GroupOrTeacher? = null,
|
||||||
val updateDates: UpdateDates? = null,
|
val updateDates: UpdateDates? = null,
|
||||||
val lastUpdateAt: Long = 0,
|
val lastUpdateAt: Long = 0,
|
||||||
val isLoading: Boolean = false
|
val isLoading: Boolean = false
|
||||||
) {
|
) {
|
||||||
fun toUiState(): GroupScheduleUiState = if (group == null) {
|
fun toUiState(): GroupUiState =
|
||||||
GroupScheduleUiState.NoData(isLoading)
|
if (group == null)
|
||||||
} else {
|
GroupUiState.NoData(isLoading)
|
||||||
GroupScheduleUiState.HasData(group, updateDates!!, lastUpdateAt, isLoading)
|
else
|
||||||
}
|
GroupUiState.HasData(group, updateDates!!, lastUpdateAt, isLoading)
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class GroupScheduleViewModel @Inject constructor(
|
class GroupViewModel @Inject constructor(
|
||||||
appContainer: AppContainer
|
appContainer: AppContainer
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val scheduleRepository = appContainer.scheduleRepository
|
private val scheduleRepository = appContainer.scheduleRepository
|
||||||
private val networkCacheRepository = appContainer.networkCacheRepository
|
private val networkCacheRepository = appContainer.networkCacheRepository
|
||||||
private val viewModelState = MutableStateFlow(GroupScheduleViewModelState(isLoading = true))
|
|
||||||
|
|
||||||
val uiState = viewModelState
|
private val state = MutableStateFlow(GroupViewModelState(isLoading = true))
|
||||||
.map(GroupScheduleViewModelState::toUiState)
|
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState())
|
val uiState = state
|
||||||
|
.map(GroupViewModelState::toUiState)
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Companion.Eagerly, state.value.toUiState())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refresh() {
|
fun refresh() {
|
||||||
viewModelState.update { it.copy(isLoading = true) }
|
state.update { it.copy(isLoading = true) }
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = scheduleRepository.getGroup()
|
val result = scheduleRepository.getGroup()
|
||||||
|
|
||||||
viewModelState.update {
|
state.update {
|
||||||
when (result) {
|
when (result) {
|
||||||
is MyResult.Success -> {
|
is MyResult.Success -> it.copy(
|
||||||
val updateDates = networkCacheRepository.getUpdateDates()
|
group = result.data,
|
||||||
|
updateDates = networkCacheRepository.getUpdateDates(),
|
||||||
it.copy(
|
lastUpdateAt = Date().time,
|
||||||
group = result.data,
|
isLoading = false
|
||||||
updateDates = updateDates,
|
)
|
||||||
lastUpdateAt = Date().time,
|
|
||||||
isLoading = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is MyResult.Failure -> it.copy(
|
is MyResult.Failure -> it.copy(
|
||||||
group = null,
|
group = null,
|
||||||
|
updateDates = null,
|
||||||
|
lastUpdateAt = 0,
|
||||||
isLoading = false
|
isLoading = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,30 @@
|
|||||||
package ru.n08i40k.polytechnic.next.ui.model
|
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.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.data.users.ProfileRepository
|
|
||||||
import ru.n08i40k.polytechnic.next.model.Profile
|
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 {
|
sealed interface ProfileUiState {
|
||||||
val isLoading: Boolean
|
val isLoading: Boolean
|
||||||
|
|
||||||
data class NoProfile(
|
data class NoData(
|
||||||
override val isLoading: Boolean
|
override val isLoading: Boolean
|
||||||
) : ProfileUiState
|
) : ProfileUiState
|
||||||
|
|
||||||
data class HasProfile(
|
data class HasData(
|
||||||
val profile: Profile,
|
override val isLoading: Boolean,
|
||||||
override val isLoading: Boolean
|
val profile: Profile
|
||||||
) : ProfileUiState
|
) : ProfileUiState
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,59 +32,53 @@ private data class ProfileViewModelState(
|
|||||||
val profile: Profile? = null,
|
val profile: Profile? = null,
|
||||||
val isLoading: Boolean = false
|
val isLoading: Boolean = false
|
||||||
) {
|
) {
|
||||||
fun toUiState(): ProfileUiState = if (profile == null) {
|
fun toUiState(): ProfileUiState = when (profile) {
|
||||||
ProfileUiState.NoProfile(isLoading)
|
null -> ProfileUiState.NoData(isLoading)
|
||||||
} else {
|
else -> ProfileUiState.HasData(isLoading, profile)
|
||||||
ProfileUiState.HasProfile(profile, isLoading)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
class ProfileViewModel(
|
class ProfileViewModel @Inject constructor(
|
||||||
private val profileRepository: ProfileRepository,
|
appContainer: AppContainer
|
||||||
val onUnauthorized: () -> Unit
|
|
||||||
) : ViewModel() {
|
) : 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)
|
.map(ProfileViewModelState::toUiState)
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState())
|
.stateIn(viewModelScope, SharingStarted.Eagerly, state.value.toUiState())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
refreshProfile()
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshProfile(callback: () -> Unit = {}) {
|
// TODO: сделать хук на unauthorized и сделать так что бы waiter удалялся, если сход контекст
|
||||||
viewModelState.update { it.copy(isLoading = true) }
|
|
||||||
|
fun refresh(): SingleHook<Profile?> {
|
||||||
|
val singleHook = SingleHook<Profile?>()
|
||||||
|
|
||||||
|
state.update { it.copy(isLoading = true) }
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = profileRepository.getProfile()
|
repository.getProfile().let { result ->
|
||||||
|
state.update {
|
||||||
viewModelState.update {
|
when (result) {
|
||||||
when (result) {
|
is MyResult.Failure -> it.copy(null, false)
|
||||||
is MyResult.Success -> it.copy(profile = result.data, isLoading = false)
|
is MyResult.Success -> it.copy(result.data, false)
|
||||||
is MyResult.Failure -> it.copy(profile = null, isLoading = false)
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
callback()
|
singleHook.resolve(
|
||||||
|
if (result is MyResult.Success)
|
||||||
|
result.data
|
||||||
|
else
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
return singleHook
|
||||||
fun provideFactory(
|
|
||||||
profileRepository: ProfileRepository,
|
|
||||||
onUnauthorized: () -> Unit
|
|
||||||
): ViewModelProvider.Factory =
|
|
||||||
object : ViewModelProvider.Factory {
|
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
||||||
@Suppress("UNCHECKED_CAST") return ProfileViewModel(
|
|
||||||
profileRepository,
|
|
||||||
onUnauthorized
|
|
||||||
) as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var Context.profileViewModel: ProfileViewModel? by mutableStateOf(null)
|
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
package ru.n08i40k.polytechnic.next.ui.model
|
package ru.n08i40k.polytechnic.next.ui.model
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.google.firebase.remoteconfig.ConfigUpdate
|
import com.google.firebase.remoteconfig.ConfigUpdate
|
||||||
import com.google.firebase.remoteconfig.ConfigUpdateListener
|
import com.google.firebase.remoteconfig.ConfigUpdateListener
|
||||||
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
|
|
||||||
import com.google.firebase.remoteconfig.FirebaseRemoteConfigException
|
import com.google.firebase.remoteconfig.FirebaseRemoteConfigException
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
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 java.util.logging.Logger
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class RemoteConfigUiState(
|
data class RemoteConfigUiState(
|
||||||
val minVersion: String,
|
val minVersion: String,
|
||||||
@@ -24,12 +23,13 @@ data class RemoteConfigUiState(
|
|||||||
val linkUpdateDelay: Long,
|
val linkUpdateDelay: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
class RemoteConfigViewModel(
|
class RemoteConfigViewModel @Inject constructor(
|
||||||
private val appContext: Context,
|
appContainer: AppContainer
|
||||||
private val remoteConfig: FirebaseRemoteConfig,
|
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val viewModelState = MutableStateFlow(
|
private val remoteConfig = appContainer.remoteConfig
|
||||||
|
|
||||||
|
private val state = MutableStateFlow(
|
||||||
RemoteConfigUiState(
|
RemoteConfigUiState(
|
||||||
minVersion = remoteConfig.getString("minVersion"),
|
minVersion = remoteConfig.getString("minVersion"),
|
||||||
currVersion = remoteConfig.getString("currVersion"),
|
currVersion = remoteConfig.getString("currVersion"),
|
||||||
@@ -40,17 +40,14 @@ class RemoteConfigViewModel(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val uiState = viewModelState
|
val uiState = state
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value)
|
.stateIn(viewModelScope, SharingStarted.Eagerly, state.value)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
(appContext as MainActivity)
|
|
||||||
.scheduleLinkUpdate(viewModelState.value.linkUpdateDelay)
|
|
||||||
|
|
||||||
remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener {
|
remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener {
|
||||||
override fun onUpdate(configUpdate: ConfigUpdate) {
|
override fun onUpdate(configUpdate: ConfigUpdate) {
|
||||||
remoteConfig.activate().addOnCompleteListener {
|
remoteConfig.activate().addOnCompleteListener {
|
||||||
viewModelState.update {
|
state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
minVersion = remoteConfig.getString("minVersion"),
|
minVersion = remoteConfig.getString("minVersion"),
|
||||||
currVersion = remoteConfig.getString("currVersion"),
|
currVersion = remoteConfig.getString("currVersion"),
|
||||||
@@ -60,8 +57,6 @@ class RemoteConfigViewModel(
|
|||||||
linkUpdateDelay = remoteConfig.getLong("linkUpdateDelay"),
|
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 <T : ViewModel> create(modelClass: Class<T>): T {
|
|
||||||
@Suppress("UNCHECKED_CAST") return RemoteConfigViewModel(
|
|
||||||
appContext,
|
|
||||||
remoteConfig,
|
|
||||||
) as T
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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<ScheduleReplacer>,
|
|
||||||
) : ScheduleReplacerUiState
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class ScheduleReplacerViewModelState(
|
|
||||||
val isLoading: Boolean = false,
|
|
||||||
val replacers: List<ScheduleReplacer>? = 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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,89 +10,95 @@ import kotlinx.coroutines.flow.stateIn
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.n08i40k.polytechnic.next.UpdateDates
|
import ru.n08i40k.polytechnic.next.UpdateDates
|
||||||
import ru.n08i40k.polytechnic.next.data.AppContainer
|
import ru.n08i40k.polytechnic.next.app.AppContainer
|
||||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
|
||||||
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
|
import ru.n08i40k.polytechnic.next.model.GroupOrTeacher
|
||||||
|
import ru.n08i40k.polytechnic.next.utils.MyResult
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
sealed interface TeacherScheduleUiState {
|
sealed interface SearchUiState {
|
||||||
val isLoading: Boolean
|
val isLoading: Boolean
|
||||||
|
|
||||||
data class NoData(
|
data class NoData(
|
||||||
override val isLoading: Boolean
|
override val isLoading: Boolean
|
||||||
) : TeacherScheduleUiState
|
) : SearchUiState
|
||||||
|
|
||||||
data class HasData(
|
data class HasData(
|
||||||
val teacher: GroupOrTeacher,
|
val teacher: GroupOrTeacher,
|
||||||
val updateDates: UpdateDates,
|
val updateDates: UpdateDates,
|
||||||
val lastUpdateAt: Long,
|
val lastUpdateAt: Long,
|
||||||
override val isLoading: Boolean
|
override val isLoading: Boolean
|
||||||
) : TeacherScheduleUiState
|
) : SearchUiState
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class TeacherScheduleViewModelState(
|
private data class SearchViewModelState(
|
||||||
val teacher: GroupOrTeacher? = null,
|
val teacher: GroupOrTeacher? = null,
|
||||||
val updateDates: UpdateDates? = null,
|
val updateDates: UpdateDates? = null,
|
||||||
val lastUpdateAt: Long = 0,
|
val lastUpdateAt: Long = 0,
|
||||||
val isLoading: Boolean = false
|
val isLoading: Boolean = false
|
||||||
) {
|
) {
|
||||||
fun toUiState(): TeacherScheduleUiState = if (teacher == null) {
|
fun toUiState(): SearchUiState =
|
||||||
TeacherScheduleUiState.NoData(isLoading)
|
if (teacher == null) SearchUiState.NoData(isLoading)
|
||||||
} else {
|
else SearchUiState.HasData(teacher, updateDates!!, lastUpdateAt, isLoading)
|
||||||
TeacherScheduleUiState.HasData(teacher, updateDates!!, lastUpdateAt, isLoading)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class TeacherScheduleViewModel @Inject constructor(
|
class SearchViewModel @Inject constructor(
|
||||||
appContainer: AppContainer
|
appContainer: AppContainer
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val scheduleRepository = appContainer.scheduleRepository
|
private val scheduleRepository = appContainer.scheduleRepository
|
||||||
private val networkCacheRepository = appContainer.networkCacheRepository
|
private val networkCacheRepository = appContainer.networkCacheRepository
|
||||||
private val viewModelState = MutableStateFlow(TeacherScheduleViewModelState(isLoading = true))
|
|
||||||
|
|
||||||
val uiState = viewModelState
|
private val state = MutableStateFlow(SearchViewModelState(isLoading = true))
|
||||||
.map(TeacherScheduleViewModelState::toUiState)
|
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState())
|
val uiState = state
|
||||||
|
.map(SearchViewModelState::toUiState)
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Eagerly, state.value.toUiState())
|
||||||
|
|
||||||
|
private var teacherName: String? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
fetch(null)
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetch(name: String?) {
|
fun set(name: String?) {
|
||||||
if (name == null) {
|
teacherName = name
|
||||||
viewModelState.update {
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
state.update { it.copy(isLoading = true) }
|
||||||
|
|
||||||
|
if (teacherName == null) {
|
||||||
|
state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
teacher = null,
|
teacher = null,
|
||||||
|
updateDates = null,
|
||||||
|
lastUpdateAt = 0,
|
||||||
isLoading = false
|
isLoading = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelState.update { it.copy(isLoading = true) }
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = scheduleRepository.getTeacher(name)
|
scheduleRepository.getTeacher(teacherName!!).let { result ->
|
||||||
|
state.update {
|
||||||
viewModelState.update {
|
when (result) {
|
||||||
when (result) {
|
is MyResult.Success -> it.copy(
|
||||||
is MyResult.Success -> {
|
|
||||||
val updateDates = networkCacheRepository.getUpdateDates()
|
|
||||||
|
|
||||||
it.copy(
|
|
||||||
teacher = result.data,
|
teacher = result.data,
|
||||||
updateDates = updateDates,
|
updateDates = networkCacheRepository.getUpdateDates(),
|
||||||
lastUpdateAt = Date().time,
|
lastUpdateAt = Date().time,
|
||||||
isLoading = false
|
isLoading = false
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
is MyResult.Failure -> it.copy(
|
is MyResult.Failure -> it.copy(
|
||||||
teacher = null,
|
teacher = null,
|
||||||
isLoading = false
|
updateDates = null,
|
||||||
)
|
lastUpdateAt = 0,
|
||||||
|
isLoading = false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<BottomNavItem>) {
|
||||||
|
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)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user