mirror of
https://github.com/n08i40k/polytechnic-android.git
synced 2025-12-06 17:57:46 +03:00
Первый коммит
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
package ru.n08i40k.polytechnic.next
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("ru.n08i40k.polytecnic.next", appContext.packageName)
|
||||
}
|
||||
}
|
||||
29
app/src/main/AndroidManifest.xml
Normal file
29
app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".PolytechnicApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@drawable/ic_launcher_round"
|
||||
android:theme="@style/Theme.PolytechnicNext"
|
||||
tools:targetApi="35">
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.PolytechnicNext">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,9 @@
|
||||
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()
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.n08i40k.polytechnic.next
|
||||
|
||||
import android.app.Application
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package ru.n08i40k.polytechnic.next.data
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import ru.n08i40k.polytechnic.next.PolytechnicApplication
|
||||
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 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 javax.inject.Singleton
|
||||
|
||||
interface AppContainer {
|
||||
val scheduleRepository: ScheduleRepository
|
||||
val profileRepository: ProfileRepository
|
||||
}
|
||||
|
||||
class MockAppContainer : AppContainer {
|
||||
override val scheduleRepository: ScheduleRepository by lazy { FakeScheduleRepository() }
|
||||
override val profileRepository: ProfileRepository by lazy { FakeProfileRepository() }
|
||||
}
|
||||
|
||||
class RemoteAppContainer(private val applicationContext: Context) : AppContainer {
|
||||
override val scheduleRepository: ScheduleRepository by lazy {
|
||||
RemoteScheduleRepository(
|
||||
applicationContext
|
||||
)
|
||||
}
|
||||
override val profileRepository: ProfileRepository by lazy {
|
||||
RemoteProfileRepository(
|
||||
applicationContext
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object AppModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppContainer(application: Application): AppContainer {
|
||||
return RemoteAppContainer(application.applicationContext)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.n08i40k.polytechnic.next.data
|
||||
|
||||
import java.lang.Exception
|
||||
|
||||
sealed interface MyResult<out R> {
|
||||
data class Success<out T>(val data: T) : MyResult<T>
|
||||
data class Failure(val exception: Exception) : MyResult<Nothing>
|
||||
}
|
||||
|
||||
fun <T> MyResult<T>.successOr(fallback: T): T {
|
||||
return (this as? MyResult.Success<T>)?.data ?: fallback
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package ru.n08i40k.polytechnic.next.data.schedule
|
||||
|
||||
import ru.n08i40k.polytechnic.next.model.Group
|
||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
||||
|
||||
interface ScheduleRepository {
|
||||
suspend fun getGroup(): MyResult<Group>
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package ru.n08i40k.polytechnic.next.data.schedule.impl
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import ru.n08i40k.polytechnic.next.model.Day
|
||||
import ru.n08i40k.polytechnic.next.model.Group
|
||||
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Lesson
|
||||
import ru.n08i40k.polytechnic.next.model.LessonTime
|
||||
import ru.n08i40k.polytechnic.next.model.LessonType
|
||||
import ru.n08i40k.polytechnic.next.data.MyResult;
|
||||
|
||||
class FakeScheduleRepository : ScheduleRepository {
|
||||
companion object {
|
||||
val exampleGroup = Group(
|
||||
name = "ИС-214/23", days = arrayListOf(
|
||||
Day(
|
||||
name = "Понедельник",
|
||||
nonNullIndices = arrayListOf(0, 1, 2, 3, 4, 5),
|
||||
defaultIndices = arrayListOf(2, 3, 4, 5),
|
||||
customIndices = arrayListOf(0, 1),
|
||||
lessons = arrayListOf(
|
||||
Lesson(
|
||||
type = LessonType.CUSTOM,
|
||||
defaultIndex = -1,
|
||||
name = "Линейка",
|
||||
time = LessonTime(510, 520),
|
||||
cabinets = arrayListOf(),
|
||||
teacherNames = arrayListOf(),
|
||||
),
|
||||
Lesson(
|
||||
type = LessonType.CUSTOM,
|
||||
defaultIndex = -1,
|
||||
name = "Разговор о важном",
|
||||
time = LessonTime(525, 555),
|
||||
cabinets = arrayListOf(),
|
||||
teacherNames = arrayListOf(),
|
||||
),
|
||||
Lesson(
|
||||
type = LessonType.DEFAULT,
|
||||
defaultIndex = 1,
|
||||
name = "Элементы высшей математики",
|
||||
time = LessonTime(565, 645),
|
||||
cabinets = arrayListOf("31"),
|
||||
teacherNames = arrayListOf("Цацаева Т.Н."),
|
||||
),
|
||||
Lesson(
|
||||
type = LessonType.DEFAULT,
|
||||
defaultIndex = 2,
|
||||
name = "Операционные системы и среды",
|
||||
time = LessonTime(655, 735),
|
||||
cabinets = arrayListOf("42"),
|
||||
teacherNames = arrayListOf("Сергачева А.О."),
|
||||
),
|
||||
Lesson(
|
||||
type = LessonType.DEFAULT,
|
||||
defaultIndex = 3,
|
||||
name = "Физическая культура",
|
||||
time = LessonTime(755, 835),
|
||||
cabinets = arrayListOf("c/3"),
|
||||
teacherNames = arrayListOf("Васюнин В.Г."),
|
||||
),
|
||||
Lesson(
|
||||
type = LessonType.DEFAULT,
|
||||
defaultIndex = 4,
|
||||
name = "МДК.05.01 Проектирование и дизайн информационных систем",
|
||||
time = LessonTime(845, 925),
|
||||
cabinets = arrayListOf("43"),
|
||||
teacherNames = arrayListOf("Ивашова А.Н."),
|
||||
),
|
||||
null,
|
||||
null,
|
||||
)
|
||||
), Day(
|
||||
name = "Вторник",
|
||||
nonNullIndices = arrayListOf(0, 1, 2),
|
||||
defaultIndices = arrayListOf(0, 1, 2),
|
||||
customIndices = arrayListOf(),
|
||||
lessons = arrayListOf(
|
||||
Lesson(
|
||||
type = LessonType.DEFAULT,
|
||||
defaultIndex = 1,
|
||||
name = "Стандартизация, сертификация и техническое документоведение",
|
||||
time = LessonTime(525, 605),
|
||||
cabinets = arrayListOf("42"),
|
||||
teacherNames = arrayListOf("Сергачева А.О."),
|
||||
),
|
||||
Lesson(
|
||||
type = LessonType.DEFAULT,
|
||||
defaultIndex = 2,
|
||||
name = "Элементы высшей математики",
|
||||
time = LessonTime(620, 700),
|
||||
cabinets = arrayListOf("31"),
|
||||
teacherNames = arrayListOf("Цацаева Т.Н."),
|
||||
),
|
||||
Lesson(
|
||||
type = LessonType.DEFAULT,
|
||||
defaultIndex = 3,
|
||||
name = "Основы проектирования баз данных",
|
||||
time = LessonTime(720, 800),
|
||||
cabinets = arrayListOf("21"),
|
||||
teacherNames = arrayListOf("Чинарева Е.А."),
|
||||
),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
), Day(
|
||||
name = "Среда",
|
||||
nonNullIndices = arrayListOf(0, 1, 2),
|
||||
defaultIndices = arrayListOf(0, 1, 2),
|
||||
customIndices = arrayListOf(),
|
||||
lessons = arrayListOf(
|
||||
Lesson(
|
||||
type = LessonType.DEFAULT,
|
||||
defaultIndex = 1,
|
||||
name = "Операционные системы и среды",
|
||||
time = LessonTime(525, 605),
|
||||
cabinets = arrayListOf("42"),
|
||||
teacherNames = arrayListOf("Сергачева А.О."),
|
||||
),
|
||||
Lesson(
|
||||
type = LessonType.DEFAULT,
|
||||
defaultIndex = 2,
|
||||
name = "Элементы высшей математики",
|
||||
time = LessonTime(620, 700),
|
||||
cabinets = arrayListOf("31"),
|
||||
teacherNames = arrayListOf("Цацаева Т.Н."),
|
||||
),
|
||||
Lesson(
|
||||
type = LessonType.DEFAULT,
|
||||
defaultIndex = 3,
|
||||
name = "МДК.05.01 Проектирование и дизайн информационных систем",
|
||||
time = LessonTime(720, 800),
|
||||
cabinets = arrayListOf("43"),
|
||||
teacherNames = arrayListOf("Ивашова А.Н."),
|
||||
),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private val group = MutableStateFlow<Group?>(exampleGroup)
|
||||
|
||||
private var updateCounter: Int = 0
|
||||
|
||||
override suspend fun getGroup(): MyResult<Group> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
delay(1500)
|
||||
if (updateCounter++ % 3 == 0) MyResult.Failure(
|
||||
IllegalStateException()
|
||||
)
|
||||
else MyResult.Success(group.value!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package ru.n08i40k.polytechnic.next.data.schedule.impl
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.toolbox.RequestFuture
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
||||
import ru.n08i40k.polytechnic.next.data.schedule.ScheduleRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Group
|
||||
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetRequest
|
||||
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetRequestData
|
||||
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetResponse
|
||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||
import java.lang.Thread.sleep
|
||||
|
||||
class RemoteScheduleRepository(private val context: Context) : ScheduleRepository {
|
||||
override suspend fun getGroup(): MyResult<Group> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val groupName = runBlocking {
|
||||
context.settingsDataStore.data.map { settings -> settings.group }.first()
|
||||
}
|
||||
|
||||
if (groupName.isEmpty())
|
||||
return@withContext MyResult.Failure(RuntimeException("No group name provided!"))
|
||||
|
||||
val responseFuture = RequestFuture.newFuture<ScheduleGetResponse>()
|
||||
ScheduleGetRequest(
|
||||
ScheduleGetRequestData(groupName),
|
||||
context,
|
||||
responseFuture,
|
||||
responseFuture
|
||||
).send()
|
||||
|
||||
try {
|
||||
MyResult.Success(responseFuture.get().group)
|
||||
} catch (exception: Exception) {
|
||||
MyResult.Failure(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
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>
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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
|
||||
import java.lang.Exception
|
||||
|
||||
class FakeProfileRepository : ProfileRepository {
|
||||
private var counter = 0
|
||||
|
||||
companion object {
|
||||
val exampleProfile =
|
||||
Profile("66db32d24030a07e02d974c5", "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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package ru.n08i40k.polytechnic.next.data.users.impl
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.toolbox.RequestFuture
|
||||
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.data.profile.UsersMeRequest
|
||||
|
||||
class RemoteProfileRepository(private val context: Context) : ProfileRepository {
|
||||
override suspend fun getProfile(): MyResult<Profile> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val responseFuture = RequestFuture.newFuture<Profile>()
|
||||
UsersMeRequest(
|
||||
context,
|
||||
responseFuture,
|
||||
responseFuture
|
||||
).send()
|
||||
|
||||
try {
|
||||
MyResult.Success(responseFuture.get())
|
||||
} catch (exception: Exception) {
|
||||
MyResult.Failure(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
app/src/main/java/ru/n08i40k/polytechnic/next/model/Group.kt
Normal file
43
app/src/main/java/ru/n08i40k/polytechnic/next/model/Group.kt
Normal file
@@ -0,0 +1,43 @@
|
||||
package ru.n08i40k.polytechnic.next.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import ru.n08i40k.polytechnic.next.utils.EnumAsIntSerializer
|
||||
|
||||
@Serializable
|
||||
data class LessonTime(val start: Int, val end: Int)
|
||||
|
||||
private class LessonTypeIntSerializer : EnumAsIntSerializer<LessonType>(
|
||||
"LessonType",
|
||||
{ it.value },
|
||||
{ v -> LessonType.entries.first { it.value == v } }
|
||||
)
|
||||
|
||||
@Serializable(with = LessonTypeIntSerializer::class)
|
||||
enum class LessonType(val value: Int) {
|
||||
DEFAULT(0), CUSTOM(1)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Lesson(
|
||||
val type: LessonType,
|
||||
val defaultIndex: Int,
|
||||
val name: String,
|
||||
val time: LessonTime?,
|
||||
val cabinets: ArrayList<String>,
|
||||
val teacherNames: ArrayList<String>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Day(
|
||||
val name: String,
|
||||
val nonNullIndices: ArrayList<Int>,
|
||||
val defaultIndices: ArrayList<Int>,
|
||||
val customIndices: ArrayList<Int>,
|
||||
val lessons: ArrayList<Lesson?>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Group(
|
||||
val name: String,
|
||||
val days: ArrayList<Day?>
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
package ru.n08i40k.polytechnic.next.model
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Face
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import kotlinx.serialization.Serializable
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.utils.EnumAsStringSerializer
|
||||
|
||||
private class UserRoleStringSerializer : EnumAsStringSerializer<UserRole>(
|
||||
"UserRole",
|
||||
{ it.value },
|
||||
{ v -> UserRole.entries.first { it.value == v } }
|
||||
)
|
||||
|
||||
@Serializable(with = UserRoleStringSerializer::class)
|
||||
enum class UserRole(val value: String, val icon: ImageVector, @StringRes val stringId: Int) {
|
||||
STUDENT("STUDENT", Icons.Filled.Face, R.string.role_student),
|
||||
TEACHER("TEACHER", Icons.Filled.Person, R.string.role_teacher),
|
||||
ADMIN("ADMIN", Icons.Filled.Settings, R.string.role_admin)
|
||||
}
|
||||
|
||||
val AcceptableUserRoles = listOf(UserRole.STUDENT, UserRole.TEACHER)
|
||||
|
||||
@Serializable
|
||||
data class Profile(
|
||||
val id: String,
|
||||
val username: String,
|
||||
val group: String,
|
||||
val role: UserRole
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
package ru.n08i40k.polytechnic.next.network
|
||||
|
||||
object NetworkValues {
|
||||
const val API_HOST = "https://192.168.0.103:5050/api/v1/"
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package ru.n08i40k.polytechnic.next.network
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.RequestQueue
|
||||
import com.android.volley.Response
|
||||
import com.android.volley.toolbox.HurlStack
|
||||
import com.android.volley.toolbox.StringRequest
|
||||
import com.android.volley.toolbox.Volley
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.logging.Logger
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
|
||||
class NetworkConnection(ctx: Context) {
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: NetworkConnection? = null
|
||||
|
||||
fun getInstance(ctx: Context) = INSTANCE ?: synchronized(this) {
|
||||
INSTANCE ?: NetworkConnection(ctx).also { INSTANCE = it }
|
||||
}
|
||||
}
|
||||
|
||||
private val sslSocketFactory: SSLSocketFactory by lazy {
|
||||
val trustAllCerts =
|
||||
arrayOf<TrustManager>(@SuppressLint("CustomX509TrustManager") object :
|
||||
X509TrustManager {
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<X509Certificate>, authType: String
|
||||
) {
|
||||
}
|
||||
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<X509Certificate>, authType: String
|
||||
) {
|
||||
}
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> {
|
||||
return arrayOf()
|
||||
}
|
||||
})
|
||||
|
||||
val sslContext = SSLContext.getInstance("SSL")
|
||||
sslContext.init(null, trustAllCerts, null)
|
||||
|
||||
sslContext.socketFactory
|
||||
}
|
||||
|
||||
private val requestQueue: RequestQueue by lazy {
|
||||
Volley.newRequestQueue(ctx.applicationContext, HurlStack(null, sslSocketFactory))
|
||||
}
|
||||
|
||||
fun <T> addToRequestQueue(req: Request<T>) {
|
||||
requestQueue.add(req)
|
||||
}
|
||||
}
|
||||
|
||||
open class RequestBase(
|
||||
protected val context: Context,
|
||||
method: Int,
|
||||
url: String?,
|
||||
listener: Response.Listener<String>,
|
||||
errorListener: Response.ErrorListener?
|
||||
) : StringRequest(method, NetworkValues.API_HOST + url, listener, errorListener) {
|
||||
fun send() {
|
||||
Logger.getLogger("RequestBase").info("Sending request to $url")
|
||||
NetworkConnection.getInstance(context).addToRequestQueue(this)
|
||||
}
|
||||
|
||||
override fun getHeaders(): MutableMap<String, String> {
|
||||
val headers = mutableMapOf<String, String>()
|
||||
headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
|
||||
return headers
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.n08i40k.polytechnic.next.network
|
||||
|
||||
class ResponseBase {
|
||||
fun handleResponse(response: String?) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.AuthFailureError
|
||||
import com.android.volley.Response
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import ru.n08i40k.polytechnic.next.network.RequestBase
|
||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||
import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
|
||||
|
||||
open class AuthorizedRequest(
|
||||
context: Context,
|
||||
method: Int,
|
||||
url: String?,
|
||||
listener: Response.Listener<String>,
|
||||
errorListener: Response.ErrorListener?,
|
||||
private val canBeUnauthorized: Boolean = false
|
||||
) : RequestBase(
|
||||
context,
|
||||
method,
|
||||
url,
|
||||
listener,
|
||||
Response.ErrorListener {
|
||||
if (!canBeUnauthorized && it is AuthFailureError) {
|
||||
runBlocking {
|
||||
context.settingsDataStore.updateData { currentSettings ->
|
||||
currentSettings.toBuilder().setUserId("")
|
||||
.setAccessToken("").build()
|
||||
}
|
||||
}
|
||||
context.profileViewModel!!.onUnauthorized()
|
||||
}
|
||||
|
||||
errorListener?.onErrorResponse(it)
|
||||
}) {
|
||||
override fun getHeaders(): MutableMap<String, String> {
|
||||
val accessToken = runBlocking {
|
||||
context.settingsDataStore.data.map { settings -> settings.accessToken }.first()
|
||||
}
|
||||
|
||||
if (accessToken.isEmpty())
|
||||
context.profileViewModel!!.onUnauthorized()
|
||||
|
||||
val headers = super.getHeaders()
|
||||
headers["Authorization"] = "Bearer $accessToken"
|
||||
|
||||
return headers
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
|
||||
|
||||
class ChangePasswordRequest(
|
||||
private val data: ChangePasswordRequestData,
|
||||
context: Context,
|
||||
listener: Response.Listener<Nothing>,
|
||||
errorListener: Response.ErrorListener?
|
||||
) : AuthorizedRequest(
|
||||
context,
|
||||
Method.POST,
|
||||
"auth/change-password",
|
||||
Response.Listener<String> { listener.onResponse(null) },
|
||||
errorListener,
|
||||
canBeUnauthorized = true
|
||||
) {
|
||||
override fun getBody(): ByteArray {
|
||||
return Json.encodeToString(data).toByteArray()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ChangePasswordRequestData(val oldPassword: String, val newPassword: String)
|
||||
@@ -0,0 +1,24 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.RequestBase
|
||||
|
||||
class LoginRequest(
|
||||
private val data: LoginRequestData,
|
||||
context: Context,
|
||||
listener: Response.Listener<LoginResponseData>,
|
||||
errorListener: Response.ErrorListener?
|
||||
) : RequestBase(
|
||||
context,
|
||||
Method.POST,
|
||||
"auth/sign-in",
|
||||
Response.Listener<String> { response -> listener.onResponse(Json.decodeFromString(response)) },
|
||||
errorListener
|
||||
) {
|
||||
override fun getBody(): ByteArray {
|
||||
return Json.encodeToString(data).toByteArray()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class LoginRequestData(val username: String, val password: String)
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class LoginResponseData(val id: String, val accessToken: String)
|
||||
@@ -0,0 +1,24 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.RequestBase
|
||||
|
||||
class RegisterRequest(
|
||||
private val data: RegisterRequestData,
|
||||
context: Context,
|
||||
listener: Response.Listener<RegisterResponseData>,
|
||||
errorListener: Response.ErrorListener?
|
||||
) : RequestBase(
|
||||
context,
|
||||
Method.POST,
|
||||
"auth/sign-up",
|
||||
Response.Listener<String> { response -> listener.onResponse(Json.decodeFromString(response)) },
|
||||
errorListener
|
||||
) {
|
||||
override fun getBody(): ByteArray {
|
||||
return Json.encodeToString(data).toByteArray()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import ru.n08i40k.polytechnic.next.model.UserRole
|
||||
|
||||
@Serializable
|
||||
data class RegisterRequestData(
|
||||
val username: String,
|
||||
val password: String,
|
||||
val group: String,
|
||||
val role: UserRole
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.auth
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RegisterResponseData(val id: String, val accessToken: String)
|
||||
@@ -0,0 +1,24 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.profile
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
|
||||
|
||||
class ChangeGroupRequest(
|
||||
private val data: ChangeGroupRequestData,
|
||||
context: Context,
|
||||
listener: Response.Listener<Nothing>,
|
||||
errorListener: Response.ErrorListener?
|
||||
) : AuthorizedRequest(
|
||||
context,
|
||||
Method.POST,
|
||||
"users/change-group",
|
||||
Response.Listener<String> { listener.onResponse(null) },
|
||||
errorListener
|
||||
) {
|
||||
override fun getBody(): ByteArray {
|
||||
return Json.encodeToString(data).toByteArray()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.profile
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ChangeGroupRequestData(val group: String)
|
||||
@@ -0,0 +1,24 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.profile
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
|
||||
|
||||
class ChangeUsernameRequest(
|
||||
private val data: ChangeUsernameRequestData,
|
||||
context: Context,
|
||||
listener: Response.Listener<Nothing>,
|
||||
errorListener: Response.ErrorListener?
|
||||
) : AuthorizedRequest(
|
||||
context,
|
||||
Method.POST,
|
||||
"users/change-username",
|
||||
Response.Listener<String> { listener.onResponse(null) },
|
||||
errorListener
|
||||
) {
|
||||
override fun getBody(): ByteArray {
|
||||
return Json.encodeToString(data).toByteArray()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.profile
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ChangeUsernameRequestData(val username: String)
|
||||
@@ -0,0 +1,19 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.profile
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.model.Profile
|
||||
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
|
||||
|
||||
class UsersMeRequest(
|
||||
context: Context,
|
||||
listener: Response.Listener<Profile>,
|
||||
errorListener: Response.ErrorListener?
|
||||
) : AuthorizedRequest(
|
||||
context, Method.GET, "users/me", Response.Listener<String> { response ->
|
||||
listener.onResponse(
|
||||
Json.decodeFromString<Profile>(response)
|
||||
)
|
||||
}, errorListener
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.schedule
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
|
||||
|
||||
class ScheduleGetRequest(
|
||||
private val data: ScheduleGetRequestData,
|
||||
context: Context,
|
||||
listener: Response.Listener<ScheduleGetResponse>,
|
||||
errorListener: Response.ErrorListener? = null
|
||||
) : AuthorizedRequest(
|
||||
context, Method.POST, "schedule/get-group", Response.Listener<String> { response ->
|
||||
listener.onResponse(Json.decodeFromString<ScheduleGetResponse>(response))
|
||||
}, errorListener
|
||||
) {
|
||||
override fun getBody(): ByteArray {
|
||||
return Json.encodeToString(data).toByteArray()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.schedule
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Response
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.n08i40k.polytechnic.next.network.data.AuthorizedRequest
|
||||
|
||||
class ScheduleGetGroupNamesRequest(
|
||||
context: Context,
|
||||
listener: Response.Listener<ScheduleGetGroupNamesResponseData>,
|
||||
errorListener: Response.ErrorListener? = null
|
||||
) : AuthorizedRequest(
|
||||
context, Method.GET, "schedule/get-group-names", Response.Listener<String> { response ->
|
||||
listener.onResponse(Json.decodeFromString<ScheduleGetGroupNamesResponseData>(response))
|
||||
}, errorListener
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.schedule
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ScheduleGetGroupNamesResponseData(
|
||||
val names: ArrayList<String>,
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.schedule
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ScheduleGetRequestData(val name: String)
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.n08i40k.polytechnic.next.network.data.schedule
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import ru.n08i40k.polytechnic.next.model.Group
|
||||
|
||||
@Serializable
|
||||
data class ScheduleGetResponse(
|
||||
val updatedAt: String,
|
||||
val group: Group,
|
||||
val etag: String,
|
||||
val lastChangedDays: ArrayList<Int>
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
package ru.n08i40k.polytechnic.next.settings
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.CorruptionException
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.core.Serializer
|
||||
import androidx.datastore.dataStore
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import ru.n08i40k.polytechnic.next.Settings
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
object SettingsSerializer : Serializer<Settings> {
|
||||
override val defaultValue: Settings = Settings.getDefaultInstance()
|
||||
|
||||
override suspend fun readFrom(input: InputStream): Settings {
|
||||
try {
|
||||
return Settings.parseFrom(input)
|
||||
} catch (exception: InvalidProtocolBufferException) {
|
||||
throw CorruptionException("Cannot read proto.", exception)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
|
||||
}
|
||||
|
||||
val Context.settingsDataStore: DataStore<Settings> by dataStore(
|
||||
fileName = "settings.pb",
|
||||
serializer = SettingsSerializer
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
package ru.n08i40k.polytechnic.next.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LoadingContent(
|
||||
empty: Boolean,
|
||||
emptyContent: @Composable () -> Unit = { FullScreenLoading() },
|
||||
loading: Boolean,
|
||||
onRefresh: () -> Unit,
|
||||
verticalArrangement: Arrangement.Vertical = Arrangement.Center,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
if (empty) emptyContent()
|
||||
else {
|
||||
PullToRefreshBox(
|
||||
isRefreshing = loading, onRefresh = onRefresh
|
||||
) {
|
||||
LazyColumn(Modifier.fillMaxSize(), verticalArrangement = verticalArrangement) {
|
||||
item {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun FullScreenLoading() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.wrapContentSize(Alignment.Center)
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package ru.n08i40k.polytechnic.next.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
Box(Modifier.windowInsetsPadding(WindowInsets.safeContent.only(WindowInsetsSides.Top))) {
|
||||
PolytechnicApp()
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
settingsDataStore.data.first()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package ru.n08i40k.polytechnic.next.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import ru.n08i40k.polytechnic.next.data.AppContainer
|
||||
import ru.n08i40k.polytechnic.next.data.MockAppContainer
|
||||
import ru.n08i40k.polytechnic.next.ui.auth.AuthScreen
|
||||
import ru.n08i40k.polytechnic.next.ui.main.MainScreen
|
||||
import ru.n08i40k.polytechnic.next.ui.theme.AppTheme
|
||||
|
||||
|
||||
@Preview(showBackground = true, showSystemUi = true)
|
||||
@Composable
|
||||
fun PolytechnicApp() {
|
||||
AppTheme(darkTheme = true, content = {
|
||||
val navController = rememberNavController()
|
||||
|
||||
NavHost(
|
||||
navController = navController, startDestination = "auth"
|
||||
) {
|
||||
composable(route = "auth") {
|
||||
AuthScreen(navController)
|
||||
}
|
||||
|
||||
composable(route = "main") {
|
||||
MainScreen(navController)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.auth
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
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 com.android.volley.AuthFailureError
|
||||
import com.android.volley.ClientError
|
||||
import com.android.volley.TimeoutError
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.model.AcceptableUserRoles
|
||||
import ru.n08i40k.polytechnic.next.model.UserRole
|
||||
import ru.n08i40k.polytechnic.next.network.data.auth.LoginRequest
|
||||
import ru.n08i40k.polytechnic.next.network.data.auth.LoginRequestData
|
||||
import ru.n08i40k.polytechnic.next.network.data.auth.RegisterRequest
|
||||
import ru.n08i40k.polytechnic.next.network.data.auth.RegisterRequestData
|
||||
import ru.n08i40k.polytechnic.next.network.data.profile.UsersMeRequest
|
||||
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun LoginForm(
|
||||
mutableVisible: MutableState<Boolean> = mutableStateOf(true),
|
||||
navController: NavHostController = rememberNavController(),
|
||||
scope: CoroutineScope = rememberCoroutineScope(),
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val mutableIsLoading = remember { mutableStateOf(false) }
|
||||
|
||||
var username by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var visible by mutableVisible
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.login_title),
|
||||
modifier = Modifier.padding(10.dp),
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
|
||||
val mutableUsernameError = remember { mutableStateOf(false) }
|
||||
val mutablePasswordError = remember { mutableStateOf(false) }
|
||||
|
||||
var usernameError by mutableUsernameError
|
||||
var passwordError by mutablePasswordError
|
||||
|
||||
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 = { visible = false }) {
|
||||
Text(text = stringResource(R.string.not_registered))
|
||||
}
|
||||
|
||||
Button(onClick = {
|
||||
if (username.length < 4) usernameError = true
|
||||
if (password.isEmpty()) passwordError = true
|
||||
|
||||
if (usernameError || passwordError) return@Button
|
||||
|
||||
tryLogin(
|
||||
username,
|
||||
password,
|
||||
mutableUsernameError,
|
||||
mutablePasswordError,
|
||||
mutableIsLoading,
|
||||
context,
|
||||
snackbarHostState,
|
||||
scope,
|
||||
navController
|
||||
)
|
||||
|
||||
mutableIsLoading.value = true
|
||||
focusManager.clearFocus()
|
||||
}) {
|
||||
Text(
|
||||
text = stringResource(R.string.login),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun RegisterForm(
|
||||
mutableVisible: MutableState<Boolean> = mutableStateOf(true),
|
||||
navController: NavHostController = rememberNavController(),
|
||||
scope: CoroutineScope = rememberCoroutineScope(),
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val mutableIsLoading = remember { mutableStateOf(false) }
|
||||
|
||||
var username by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var group by remember { mutableStateOf("") }
|
||||
val mutableRole = remember { mutableStateOf(UserRole.STUDENT) }
|
||||
|
||||
var visible by mutableVisible
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.register_title),
|
||||
modifier = Modifier.padding(10.dp),
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
|
||||
val mutableUsernameError = remember { mutableStateOf(false) }
|
||||
var usernameError by mutableUsernameError
|
||||
|
||||
var passwordError by remember { mutableStateOf(false) }
|
||||
|
||||
val mutableGroupError = remember { mutableStateOf(false) }
|
||||
var groupError by mutableGroupError
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = group,
|
||||
singleLine = true,
|
||||
onValueChange = {
|
||||
groupError = false
|
||||
group = it
|
||||
},
|
||||
label = { Text(stringResource(R.string.group)) },
|
||||
isError = groupError
|
||||
)
|
||||
|
||||
RoleSelector(mutableRole)
|
||||
|
||||
TextButton(onClick = { visible = false }) {
|
||||
Text(text = stringResource(R.string.already_registered))
|
||||
}
|
||||
|
||||
Button(
|
||||
enabled = !mutableIsLoading.value,
|
||||
onClick = {
|
||||
if (username.length < 4) usernameError = true
|
||||
if (password.isEmpty()) passwordError = true
|
||||
if (group.isEmpty()) groupError = true
|
||||
|
||||
if (usernameError || passwordError || groupError) return@Button
|
||||
|
||||
tryRegister(
|
||||
username,
|
||||
password,
|
||||
group,
|
||||
mutableRole.value,
|
||||
mutableUsernameError,
|
||||
mutableGroupError,
|
||||
mutableIsLoading,
|
||||
context,
|
||||
snackbarHostState,
|
||||
scope,
|
||||
navController
|
||||
)
|
||||
|
||||
mutableIsLoading.value = true
|
||||
focusManager.clearFocus()
|
||||
}) {
|
||||
Text(
|
||||
text = stringResource(R.string.register),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun AuthForm(
|
||||
mutableIsLogin: MutableState<Boolean> = mutableStateOf(true),
|
||||
navController: NavHostController = rememberNavController(),
|
||||
scope: CoroutineScope = rememberCoroutineScope(),
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
) {
|
||||
var isLogin by mutableIsLogin
|
||||
|
||||
val mutableVisible = remember { mutableStateOf(true) }
|
||||
var visible by mutableVisible
|
||||
|
||||
val animatedAlpha by animateFloatAsState(
|
||||
targetValue = if (visible) 1.0f else 0f, label = "alpha"
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.graphicsLayer {
|
||||
alpha = animatedAlpha
|
||||
if (alpha == 0F) {
|
||||
if (!visible) isLogin = isLogin.not()
|
||||
visible = true
|
||||
}
|
||||
},
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
if (isLogin)
|
||||
LoginForm(mutableVisible, navController, scope, snackbarHostState)
|
||||
else
|
||||
RegisterForm(mutableVisible, navController, scope, snackbarHostState)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun RoleSelector(mutableRole: MutableState<UserRole> = mutableStateOf(UserRole.STUDENT)) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
var role by mutableRole
|
||||
|
||||
Box(
|
||||
modifier = Modifier.wrapContentSize()
|
||||
) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = !expanded }
|
||||
) {
|
||||
TextField(
|
||||
label = { Text(stringResource(R.string.role)) },
|
||||
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable),
|
||||
value = stringResource(role.stringId),
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = role.icon,
|
||||
contentDescription = "role icon"
|
||||
)
|
||||
},
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }) {
|
||||
AcceptableUserRoles.forEach {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(it.stringId)) },
|
||||
onClick = {
|
||||
role = it
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun tryLogin(
|
||||
// data
|
||||
username: String,
|
||||
password: String,
|
||||
|
||||
// errors
|
||||
mutableUsernameError: MutableState<Boolean>,
|
||||
mutablePasswordError: MutableState<Boolean>,
|
||||
|
||||
// additional
|
||||
mutableIsLoading: MutableState<Boolean>,
|
||||
context: Context,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
scope: CoroutineScope,
|
||||
navController: NavHostController
|
||||
) {
|
||||
var isLoading by mutableIsLoading
|
||||
|
||||
LoginRequest(LoginRequestData(username, password), context, {
|
||||
scope.launch { snackbarHostState.showSnackbar("Cool!") }
|
||||
|
||||
runBlocking {
|
||||
context.settingsDataStore.updateData { currentSettings ->
|
||||
currentSettings
|
||||
.toBuilder()
|
||||
.setUserId(it.id)
|
||||
.setAccessToken(it.accessToken)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
UsersMeRequest(context, {
|
||||
runBlocking {
|
||||
context.settingsDataStore.updateData { currentSettings ->
|
||||
currentSettings
|
||||
.toBuilder()
|
||||
.setGroup(it.group)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
navController.navigate("main")
|
||||
}, {}).send()
|
||||
}, {
|
||||
isLoading = false
|
||||
|
||||
if (it is TimeoutError) {
|
||||
scope.launch { snackbarHostState.showSnackbar("Request timed out!") }
|
||||
}
|
||||
|
||||
if (it is ClientError && it.networkResponse.statusCode == 400) scope.launch {
|
||||
snackbarHostState.showSnackbar("Request schema not identical!")
|
||||
}
|
||||
|
||||
if (it is AuthFailureError) scope.launch {
|
||||
mutableUsernameError.value = true
|
||||
mutablePasswordError.value = true
|
||||
snackbarHostState.showSnackbar("Invalid credentials!")
|
||||
}
|
||||
|
||||
|
||||
it.printStackTrace()
|
||||
}).send()
|
||||
}
|
||||
|
||||
fun tryRegister(
|
||||
// data
|
||||
username: String,
|
||||
password: String,
|
||||
group: String,
|
||||
role: UserRole,
|
||||
|
||||
// errors
|
||||
mutableUsernameError: MutableState<Boolean>,
|
||||
mutableGroupError: MutableState<Boolean>,
|
||||
|
||||
// additional
|
||||
mutableIsLoading: MutableState<Boolean>,
|
||||
context: Context,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
scope: CoroutineScope,
|
||||
navController: NavHostController
|
||||
) {
|
||||
var isLoading by mutableIsLoading
|
||||
|
||||
RegisterRequest(
|
||||
RegisterRequestData(
|
||||
username,
|
||||
password,
|
||||
group,
|
||||
role
|
||||
), context, {
|
||||
scope.launch { snackbarHostState.showSnackbar("Cool!") }
|
||||
|
||||
runBlocking {
|
||||
context.settingsDataStore.updateData { currentSettings ->
|
||||
currentSettings.toBuilder().setUserId(it.id)
|
||||
.setAccessToken(it.accessToken).build()
|
||||
}
|
||||
}
|
||||
|
||||
navController.navigate("main")
|
||||
}, {
|
||||
isLoading = false
|
||||
|
||||
if (it is TimeoutError) {
|
||||
scope.launch { snackbarHostState.showSnackbar("Request timed out!") }
|
||||
}
|
||||
|
||||
if (it is ClientError) scope.launch {
|
||||
val statusCode = it.networkResponse.statusCode
|
||||
|
||||
when (statusCode) {
|
||||
400 -> snackbarHostState.showSnackbar("Request schema not identical!")
|
||||
409 -> {
|
||||
mutableUsernameError.value = true
|
||||
snackbarHostState.showSnackbar("User already exists!")
|
||||
}
|
||||
|
||||
404 -> {
|
||||
mutableGroupError.value = true
|
||||
snackbarHostState.showSnackbar("Group doesn't exists!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (it is AuthFailureError) scope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
"Invalid credentials!"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
it.printStackTrace()
|
||||
}).send()
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun AuthScreen(navController: NavHostController = rememberNavController()) {
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val accessToken: String = runBlocking {
|
||||
context.settingsDataStore.data.map { settings -> settings.accessToken }.first()
|
||||
}
|
||||
|
||||
if (accessToken.isNotEmpty()) navController.navigate("main")
|
||||
}
|
||||
|
||||
val mutableIsLogin = remember { mutableStateOf(true) }
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
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
|
||||
) {
|
||||
Card {
|
||||
AuthForm(
|
||||
mutableIsLogin,
|
||||
navController,
|
||||
scope,
|
||||
snackbarHostState
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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.DateRange
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
|
||||
data class BottomNavItem(
|
||||
@StringRes val label: Int, val icon: ImageVector, val route: String
|
||||
)
|
||||
|
||||
object Constants {
|
||||
val bottomNavItem = listOf(
|
||||
BottomNavItem(R.string.profile, Icons.Filled.AccountCircle, "profile"),
|
||||
BottomNavItem(R.string.schedule, Icons.Filled.DateRange, "schedule")
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.main
|
||||
|
||||
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.slideIn
|
||||
import androidx.compose.animation.slideOut
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.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.settings.settingsDataStore
|
||||
import ru.n08i40k.polytechnic.next.ui.main.profile.ProfileScreen
|
||||
import ru.n08i40k.polytechnic.next.ui.main.schedule.ScheduleScreen
|
||||
import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel
|
||||
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
|
||||
import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
|
||||
|
||||
|
||||
@Composable
|
||||
private fun NavHostContainer(
|
||||
navController: NavHostController,
|
||||
padding: PaddingValues,
|
||||
scheduleViewModel: ScheduleViewModel
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Constants.bottomNavItem[1].route,
|
||||
modifier = Modifier.padding(paddingValues = padding),
|
||||
enterTransition = {
|
||||
slideIn(
|
||||
animationSpec = tween(
|
||||
500,
|
||||
delayMillis = 250,
|
||||
easing = LinearOutSlowInEasing
|
||||
)
|
||||
) { fullSize -> IntOffset(-fullSize.width, 0) }
|
||||
},
|
||||
exitTransition = {
|
||||
slideOut(
|
||||
animationSpec = tween(
|
||||
500,
|
||||
easing = FastOutSlowInEasing
|
||||
)
|
||||
) { fullSize -> IntOffset(fullSize.width, 0) }
|
||||
},
|
||||
builder = {
|
||||
composable("profile") {
|
||||
ProfileScreen(LocalContext.current.profileViewModel!!) { context.profileViewModel!!.refreshProfile() }
|
||||
}
|
||||
|
||||
composable("schedule") {
|
||||
ScheduleScreen(scheduleViewModel) { scheduleViewModel.refreshGroup() }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomNavBar(navController: NavHostController) {
|
||||
NavigationBar {
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
|
||||
val currentRoute = navBackStackEntry?.destination?.route
|
||||
|
||||
Constants.bottomNavItem.forEach { navItem ->
|
||||
NavigationBarItem(
|
||||
selected = navItem.route == currentRoute,
|
||||
onClick = { if (navItem.route != currentRoute) navController.navigate(navItem.route) },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = navItem.icon,
|
||||
contentDescription = stringResource(navItem.label)
|
||||
)
|
||||
},
|
||||
label = { Text(stringResource(navItem.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")
|
||||
}
|
||||
|
||||
val scheduleViewModel =
|
||||
hiltViewModel<ScheduleViewModel>(LocalContext.current as ComponentActivity)
|
||||
|
||||
LocalContext.current.profileViewModel =
|
||||
viewModel(
|
||||
factory = ProfileViewModel.provideFactory(
|
||||
profileRepository = mainViewModel.appContainer.profileRepository,
|
||||
onUnauthorized = { appNavController.navigate("auth") })
|
||||
)
|
||||
|
||||
val navController = rememberNavController()
|
||||
Scaffold(
|
||||
bottomBar = { BottomNavBar(navController = navController) }
|
||||
) { paddingValues ->
|
||||
NavHostContainer(
|
||||
navController,
|
||||
paddingValues,
|
||||
scheduleViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.main.profile
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Email
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.android.volley.ClientError
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Profile
|
||||
import ru.n08i40k.polytechnic.next.network.data.profile.ChangeGroupRequest
|
||||
import ru.n08i40k.polytechnic.next.network.data.profile.ChangeGroupRequestData
|
||||
import ru.n08i40k.polytechnic.next.network.data.schedule.ScheduleGetGroupNamesRequest
|
||||
|
||||
private enum class ChangeGroupError {
|
||||
NOT_EXISTS
|
||||
}
|
||||
|
||||
private fun tryChangeGroup(
|
||||
context: Context,
|
||||
group: String,
|
||||
onError: (ChangeGroupError) -> Unit,
|
||||
onSuccess: (String) -> Unit
|
||||
) {
|
||||
ChangeGroupRequest(ChangeGroupRequestData(group), context, {
|
||||
onSuccess(group)
|
||||
}, {
|
||||
if (it is ClientError && it.networkResponse.statusCode == 404)
|
||||
onError(ChangeGroupError.NOT_EXISTS)
|
||||
else throw it
|
||||
}).send()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getGroups(context: Context): ArrayList<String> {
|
||||
val groupPlaceholder = stringResource(R.string.loading);
|
||||
|
||||
val groups = remember { arrayListOf(groupPlaceholder) }
|
||||
|
||||
LaunchedEffect(groups) {
|
||||
ScheduleGetGroupNamesRequest(context, {
|
||||
groups.clear()
|
||||
groups.addAll(it.names)
|
||||
}, {
|
||||
throw it
|
||||
}).send()
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun GroupSelector(
|
||||
value: String = "ИС-214/24",
|
||||
onValueChange: (String) -> Unit = {},
|
||||
isError: Boolean = false,
|
||||
readOnly: Boolean = false,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier.wrapContentSize()
|
||||
) {
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = {
|
||||
expanded = !readOnly && !expanded
|
||||
}
|
||||
) {
|
||||
TextField(
|
||||
label = { Text(stringResource(R.string.group)) },
|
||||
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable),
|
||||
value = value,
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Email,
|
||||
contentDescription = "group"
|
||||
)
|
||||
},
|
||||
onValueChange = {},
|
||||
isError = isError,
|
||||
readOnly = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
val groups = getGroups(context)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }) {
|
||||
groups.forEach {
|
||||
DropdownMenuItem(
|
||||
text = { Text(it) },
|
||||
onClick = {
|
||||
onValueChange(it)
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
internal fun ChangeGroupDialog(
|
||||
context: Context = LocalContext.current,
|
||||
profile: Profile = FakeProfileRepository.exampleProfile,
|
||||
onChange: (String) -> Unit = {},
|
||||
onDismiss: () -> Unit = {}
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card {
|
||||
var group by remember { mutableStateOf("ИС-214/23") }
|
||||
var groupError by remember { mutableStateOf(false) }
|
||||
|
||||
var processing by remember { mutableStateOf(false) }
|
||||
|
||||
Column(modifier = Modifier.width(IntrinsicSize.Max)) {
|
||||
val modifier = Modifier.fillMaxWidth()
|
||||
|
||||
GroupSelector(
|
||||
value = group,
|
||||
onValueChange = { group = it },
|
||||
isError = groupError,
|
||||
readOnly = processing
|
||||
)
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
Button(
|
||||
modifier = modifier,
|
||||
onClick = {
|
||||
processing = true
|
||||
focusManager.clearFocus()
|
||||
|
||||
tryChangeGroup(
|
||||
context = context,
|
||||
group = group,
|
||||
onError = {
|
||||
when (it) {
|
||||
ChangeGroupError.NOT_EXISTS -> {
|
||||
groupError = true
|
||||
}
|
||||
}
|
||||
|
||||
processing = false
|
||||
},
|
||||
onSuccess = onChange
|
||||
)
|
||||
},
|
||||
enabled = !(groupError || processing)
|
||||
) {
|
||||
Text(stringResource(R.string.change_group))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
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.data.auth.ChangePasswordRequest
|
||||
import ru.n08i40k.polytechnic.next.network.data.auth.ChangePasswordRequestData
|
||||
|
||||
private enum class ChangePasswordError {
|
||||
INCORRECT_CURRENT_PASSWORD,
|
||||
SAME_PASSWORDS
|
||||
}
|
||||
|
||||
private fun tryChangePassword(
|
||||
context: Context,
|
||||
oldPassword: String,
|
||||
newPassword: String,
|
||||
onError: (ChangePasswordError) -> Unit,
|
||||
onSuccess: () -> Unit
|
||||
) {
|
||||
ChangePasswordRequest(ChangePasswordRequestData(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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.android.volley.ClientError
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Profile
|
||||
import ru.n08i40k.polytechnic.next.network.data.profile.ChangeUsernameRequest
|
||||
import ru.n08i40k.polytechnic.next.network.data.profile.ChangeUsernameRequestData
|
||||
|
||||
private enum class ChangeUsernameError {
|
||||
INCORRECT_LENGTH,
|
||||
ALREADY_EXISTS
|
||||
}
|
||||
|
||||
private fun tryChangeUsername(
|
||||
context: Context,
|
||||
username: String,
|
||||
onError: (ChangeUsernameError) -> Unit,
|
||||
onSuccess: () -> Unit
|
||||
) {
|
||||
ChangeUsernameRequest(ChangeUsernameRequestData(username), context, {
|
||||
onSuccess()
|
||||
}, {
|
||||
if (it is ClientError && it.networkResponse.statusCode == 409)
|
||||
onError(ChangeUsernameError.ALREADY_EXISTS)
|
||||
if (it is ClientError && it.networkResponse.statusCode == 400)
|
||||
onError(ChangeUsernameError.INCORRECT_LENGTH)
|
||||
else throw it
|
||||
}).send()
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
internal fun ChangeUsernameDialog(
|
||||
context: Context = LocalContext.current,
|
||||
profile: Profile = FakeProfileRepository.exampleProfile,
|
||||
onChange: () -> Unit = {},
|
||||
onDismiss: () -> Unit = {}
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card {
|
||||
var username by remember { mutableStateOf("") }
|
||||
var usernameError by remember { mutableStateOf(false) }
|
||||
|
||||
var processing by remember { mutableStateOf(false) }
|
||||
|
||||
Column(modifier = Modifier.width(IntrinsicSize.Max)) {
|
||||
val modifier = Modifier.fillMaxWidth()
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = modifier,
|
||||
value = username,
|
||||
isError = usernameError,
|
||||
onValueChange = {
|
||||
username = it
|
||||
usernameError = it.isEmpty()
|
||||
|| username == profile.username
|
||||
|| username.length < 4
|
||||
|| username.length > 10
|
||||
},
|
||||
label = { Text(text = stringResource(R.string.username)) },
|
||||
readOnly = processing
|
||||
)
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
Button(
|
||||
modifier = modifier,
|
||||
onClick = {
|
||||
processing = true
|
||||
focusManager.clearFocus()
|
||||
|
||||
tryChangeUsername(
|
||||
context = context,
|
||||
username = username,
|
||||
onError = {
|
||||
usernameError = when (it) {
|
||||
ChangeUsernameError.ALREADY_EXISTS -> true
|
||||
ChangeUsernameError.INCORRECT_LENGTH -> true
|
||||
}
|
||||
|
||||
processing = false
|
||||
},
|
||||
onSuccess = onChange
|
||||
)
|
||||
},
|
||||
enabled = !(usernameError || processing)
|
||||
) {
|
||||
Text(stringResource(R.string.change_username))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
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.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.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.ScheduleViewModel
|
||||
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()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (passwordChanging) {
|
||||
ChangePasswordDialog(
|
||||
context,
|
||||
profile,
|
||||
{ passwordChanging = false }
|
||||
) { passwordChanging = false }
|
||||
}
|
||||
|
||||
if (usernameChanging) {
|
||||
ChangeUsernameDialog(
|
||||
context,
|
||||
profile,
|
||||
{
|
||||
usernameChanging = false
|
||||
context.profileViewModel!!.refreshProfile()
|
||||
}
|
||||
) { usernameChanging = false }
|
||||
}
|
||||
|
||||
if (groupChanging) {
|
||||
val scheduleViewModel =
|
||||
hiltViewModel<ScheduleViewModel>(LocalContext.current as ComponentActivity)
|
||||
|
||||
ChangeGroupDialog(
|
||||
context,
|
||||
profile,
|
||||
{ group ->
|
||||
groupChanging = false
|
||||
runBlocking {
|
||||
context.settingsDataStore.updateData {
|
||||
it.toBuilder().setGroup(group).build()
|
||||
}
|
||||
}
|
||||
context.profileViewModel!!.refreshProfile {
|
||||
scheduleViewModel.refreshGroup()
|
||||
}
|
||||
}
|
||||
) { groupChanging = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.main.profile
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.MockAppContainer
|
||||
import ru.n08i40k.polytechnic.next.ui.LoadingContent
|
||||
import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState
|
||||
import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel
|
||||
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ProfileScreen(
|
||||
profileViewModel: ProfileViewModel = ProfileViewModel(MockAppContainer().profileRepository) {},
|
||||
onRefreshProfile: () -> Unit = {}
|
||||
) {
|
||||
val uiState by profileViewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LoadingContent(
|
||||
empty = when (uiState) {
|
||||
is ProfileUiState.NoProfile -> uiState.isLoading
|
||||
is ProfileUiState.HasProfile -> false
|
||||
},
|
||||
loading = uiState.isLoading,
|
||||
onRefresh = onRefreshProfile,
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
when (uiState) {
|
||||
is ProfileUiState.HasProfile -> {
|
||||
ProfileCard((uiState as ProfileUiState.HasProfile).profile)
|
||||
}
|
||||
|
||||
is ProfileUiState.NoProfile -> {
|
||||
TextButton(onClick = onRefreshProfile, modifier = Modifier.fillMaxSize()) {
|
||||
Text(stringResource(R.string.reload), textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.main.schedule
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Day
|
||||
import ru.n08i40k.polytechnic.next.model.Lesson
|
||||
import ru.n08i40k.polytechnic.next.model.LessonType
|
||||
import java.util.Calendar
|
||||
|
||||
private fun getCurrentMinutes(): Int {
|
||||
return Calendar.getInstance()
|
||||
.get(Calendar.HOUR_OF_DAY) * 60 + Calendar.getInstance()
|
||||
.get(Calendar.MINUTE)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getMinutes(): Int {
|
||||
var value by remember { mutableIntStateOf(getCurrentMinutes()) }
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
val runnable = {
|
||||
value = getCurrentMinutes()
|
||||
}
|
||||
|
||||
handler.postDelayed(runnable, 60_000)
|
||||
|
||||
onDispose {
|
||||
handler.removeCallbacks(runnable)
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun DayCard(
|
||||
modifier: Modifier = Modifier,
|
||||
day: Day? = FakeScheduleRepository.exampleGroup.days[0],
|
||||
current: Boolean = true
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
colors = CardDefaults.cardColors(containerColor = if (current) MaterialTheme.colorScheme.surfaceContainerHighest else MaterialTheme.colorScheme.surfaceContainerLowest)
|
||||
) {
|
||||
if (day == null) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResource(R.string.day_null)
|
||||
)
|
||||
return@Card
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
text = day.name,
|
||||
)
|
||||
|
||||
val currentMinutes = getMinutes()
|
||||
|
||||
val isCurrentLesson: (lesson: Lesson) -> Boolean = {
|
||||
current
|
||||
&& it.time != null
|
||||
&& currentMinutes >= it.time.start
|
||||
&& currentMinutes <= it.time.end
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(0.5.dp)
|
||||
) {
|
||||
if (day.nonNullIndices.isEmpty()) {
|
||||
Text("Can't get schedule!")
|
||||
} else {
|
||||
val defaultCardColors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
val customCardColors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onTertiaryContainer
|
||||
)
|
||||
val noneCardColors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
|
||||
)
|
||||
|
||||
for (i in day.nonNullIndices.first()..day.nonNullIndices.last()) {
|
||||
val lesson = day.lessons[i]!!
|
||||
|
||||
val cardColors = when (lesson.type) {
|
||||
LessonType.DEFAULT -> defaultCardColors
|
||||
LessonType.CUSTOM -> customCardColors
|
||||
}
|
||||
|
||||
val mutableExpanded = remember { mutableStateOf(false) }
|
||||
|
||||
val lessonBoxModifier = remember {
|
||||
Modifier
|
||||
.padding(PaddingValues(2.5.dp, 0.dp))
|
||||
.clickable { mutableExpanded.value = true }
|
||||
.background(cardColors.containerColor)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = if (isCurrentLesson(lesson)) lessonBoxModifier.border(
|
||||
border = BorderStroke(
|
||||
3.5.dp,
|
||||
Color(
|
||||
cardColors.containerColor.red * 0.5F,
|
||||
cardColors.containerColor.green * 0.5F,
|
||||
cardColors.containerColor.blue * 0.5F,
|
||||
1F
|
||||
)
|
||||
)
|
||||
) else lessonBoxModifier
|
||||
) {
|
||||
LessonRow(
|
||||
day, lesson, cardColors
|
||||
)
|
||||
}
|
||||
if (i != day.nonNullIndices.last()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(PaddingValues(2.5.dp, 0.dp))
|
||||
.background(noneCardColors.containerColor)
|
||||
) {
|
||||
FreeLessonRow(
|
||||
lesson,
|
||||
day.lessons[day.nonNullIndices[day.nonNullIndices.indexOf(i) + 1]]!!,
|
||||
noneCardColors
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (mutableExpanded.value) LessonExtraInfo(
|
||||
lesson, mutableExpanded
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.main.schedule
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.lerp
|
||||
import ru.n08i40k.polytechnic.next.data.schedule.impl.FakeScheduleRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Group
|
||||
import java.util.Calendar
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Preview(showBackground = true, showSystemUi = true)
|
||||
@Composable
|
||||
fun DayPager(group: Group = FakeScheduleRepository.exampleGroup) {
|
||||
val currentDay = (Calendar.getInstance().get(Calendar.DAY_OF_WEEK) - 2)
|
||||
val calendarDay = currentDay
|
||||
.coerceAtLeast(0)
|
||||
.coerceAtMost(group.days.size - 1)
|
||||
|
||||
val pagerState = rememberPagerState(initialPage = calendarDay, pageCount = { group.days.size })
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
contentPadding = PaddingValues(horizontal = 20.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) { page ->
|
||||
DayCard(
|
||||
modifier = Modifier.graphicsLayer {
|
||||
val offset = pagerState.getOffsetDistanceInPages(page).absoluteValue
|
||||
|
||||
lerp(
|
||||
start = 0.95f, stop = 1f, fraction = 1f - offset.coerceIn(0f, 1f)
|
||||
).also { scale ->
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}
|
||||
alpha = lerp(
|
||||
start = 0.5f, stop = 1f, fraction = 1f - offset.coerceIn(0f, 1f)
|
||||
)
|
||||
},
|
||||
day = group.days[page],
|
||||
current = currentDay == page
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.main.schedule
|
||||
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
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.TextAlign
|
||||
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.Day
|
||||
import ru.n08i40k.polytechnic.next.model.Lesson
|
||||
import ru.n08i40k.polytechnic.next.model.LessonTime
|
||||
|
||||
@Composable
|
||||
fun LessonExtraInfo(
|
||||
lesson: Lesson = FakeScheduleRepository.exampleGroup.days[0]!!.lessons[0]!!,
|
||||
mutableExpanded: MutableState<Boolean> = mutableStateOf(true)
|
||||
) {
|
||||
Dialog(onDismissRequest = { mutableExpanded.value = false }) {
|
||||
Column(modifier = Modifier.padding(5.dp)) {
|
||||
if (lesson.teacherNames.isNotEmpty()) {
|
||||
val teachers = buildString {
|
||||
append(stringResource(if (lesson.teacherNames.count() > 1) R.string.lesson_teachers else R.string.lesson_teacher))
|
||||
append(" - ")
|
||||
append(lesson.teacherNames.joinToString(", "))
|
||||
}
|
||||
Text(teachers)
|
||||
}
|
||||
|
||||
val duration = buildString {
|
||||
append(stringResource(R.string.lesson_duration))
|
||||
append(" - ")
|
||||
val duration = if (lesson.time != null) lesson.time.end - lesson.time.start else 0
|
||||
|
||||
val hours = duration / 60
|
||||
val minutes = duration % 60
|
||||
|
||||
append(hours)
|
||||
append(stringResource(R.string.hours))
|
||||
append(" ")
|
||||
append(minutes)
|
||||
append(stringResource(R.string.minutes))
|
||||
}
|
||||
|
||||
Text(duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class LessonTimeFormat {
|
||||
FROM_TO, ONLY_MINUTES_DURATION
|
||||
}
|
||||
|
||||
private fun numWithZero(num: Int): String {
|
||||
return "0".repeat(if (num <= 9) 1 else 0) + num.toString()
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun LessonViewRow(
|
||||
idx: Int = 1,
|
||||
time: LessonTime? = LessonTime(0, 60),
|
||||
timeFormat: LessonTimeFormat = LessonTimeFormat.FROM_TO,
|
||||
name: String = "Test",
|
||||
teacherNames: String? = "Хомченко Н.Е.",
|
||||
cabinets: ArrayList<String> = arrayListOf("14", "31"),
|
||||
cardColors: CardColors = CardDefaults.cardColors(),
|
||||
verticalPadding: Dp = 10.dp
|
||||
) {
|
||||
val contentColor =
|
||||
if (timeFormat == LessonTimeFormat.FROM_TO) cardColors.contentColor else cardColors.disabledContentColor
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(10.dp, verticalPadding),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = if (idx == -1) "1" else idx.toString(),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (idx == -1) Color(0) else contentColor
|
||||
)
|
||||
|
||||
Spacer(Modifier.width(7.5.dp))
|
||||
|
||||
if (time != null) {
|
||||
val formattedTime: ArrayList<String> = when (timeFormat) {
|
||||
LessonTimeFormat.FROM_TO -> {
|
||||
val startHour = numWithZero(time.start / 60)
|
||||
val startMinute = numWithZero(time.start % 60)
|
||||
|
||||
val endHour = numWithZero(time.end / 60)
|
||||
val endMinute = numWithZero(time.end % 60)
|
||||
|
||||
arrayListOf("$startHour:$startMinute", "$endHour:$endMinute")
|
||||
}
|
||||
|
||||
LessonTimeFormat.ONLY_MINUTES_DURATION -> {
|
||||
val duration = time.end - time.start
|
||||
|
||||
arrayListOf("$duration " + stringResource(R.string.minutes))
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(0.25f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = formattedTime[0],
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = contentColor
|
||||
)
|
||||
if (formattedTime.count() > 1) {
|
||||
Text(
|
||||
text = formattedTime[1],
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = contentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(7.5.dp))
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(0.85f),
|
||||
text = name,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = contentColor
|
||||
)
|
||||
if (!teacherNames.isNullOrEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(0.85f),
|
||||
text = teacherNames,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = contentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.End,
|
||||
text = cabinets.joinToString(", "),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
color = contentColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun FreeLessonRow(
|
||||
lesson: Lesson = FakeScheduleRepository.exampleGroup.days[0]!!.lessons[0]!!,
|
||||
nextLesson: Lesson = FakeScheduleRepository.exampleGroup.days[0]!!.lessons[1]!!,
|
||||
cardColors: CardColors = CardDefaults.cardColors()
|
||||
) {
|
||||
LessonViewRow(
|
||||
-1,
|
||||
if (lesson.time != null && nextLesson.time != null) LessonTime(
|
||||
lesson.time.end, nextLesson.time.start
|
||||
) else null,
|
||||
LessonTimeFormat.ONLY_MINUTES_DURATION,
|
||||
stringResource(R.string.lesson_break),
|
||||
null,
|
||||
arrayListOf(),
|
||||
cardColors,
|
||||
2.5.dp
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun LessonRow(
|
||||
day: Day = FakeScheduleRepository.exampleGroup.days[0]!!,
|
||||
lesson: Lesson = FakeScheduleRepository.exampleGroup.days[0]!!.lessons[0]!!,
|
||||
cardColors: CardColors = CardDefaults.cardColors()
|
||||
) {
|
||||
LessonViewRow(
|
||||
lesson.defaultIndex,
|
||||
lesson.time,
|
||||
LessonTimeFormat.FROM_TO,
|
||||
lesson.name,
|
||||
lesson.teacherNames.joinToString(", "),
|
||||
lesson.cabinets,
|
||||
cardColors,
|
||||
5.dp
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.main.schedule
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import ru.n08i40k.polytechnic.next.R
|
||||
import ru.n08i40k.polytechnic.next.data.MockAppContainer
|
||||
import ru.n08i40k.polytechnic.next.ui.LoadingContent
|
||||
import ru.n08i40k.polytechnic.next.ui.model.ScheduleUiState
|
||||
import ru.n08i40k.polytechnic.next.ui.model.ScheduleViewModel
|
||||
|
||||
@Preview(showBackground = true, showSystemUi = true)
|
||||
@Composable
|
||||
fun ScheduleScreen(
|
||||
scheduleViewModel: ScheduleViewModel = ScheduleViewModel(MockAppContainer()),
|
||||
onRefreshSchedule: () -> Unit = {}
|
||||
) {
|
||||
val uiState by scheduleViewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LoadingContent(
|
||||
empty = when (uiState) {
|
||||
is ScheduleUiState.NoSchedule -> uiState.isLoading
|
||||
is ScheduleUiState.HasSchedule -> false
|
||||
},
|
||||
loading = uiState.isLoading,
|
||||
onRefresh = onRefreshSchedule
|
||||
) {
|
||||
when (uiState) {
|
||||
is ScheduleUiState.HasSchedule -> {
|
||||
DayPager((uiState as ScheduleUiState.HasSchedule).group)
|
||||
}
|
||||
|
||||
is ScheduleUiState.NoSchedule -> {
|
||||
TextButton(onClick = onRefreshSchedule, modifier = Modifier.fillMaxSize()) {
|
||||
Text(stringResource(R.string.reload), textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.model
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.n08i40k.polytechnic.next.data.MyResult
|
||||
import ru.n08i40k.polytechnic.next.data.users.ProfileRepository
|
||||
import ru.n08i40k.polytechnic.next.model.Profile
|
||||
|
||||
sealed interface ProfileUiState {
|
||||
val isLoading: Boolean
|
||||
|
||||
data class NoProfile(
|
||||
override val isLoading: Boolean
|
||||
) : ProfileUiState
|
||||
|
||||
data class HasProfile(
|
||||
val profile: Profile,
|
||||
override val isLoading: Boolean
|
||||
) : ProfileUiState
|
||||
}
|
||||
|
||||
private data class ProfileViewModelState(
|
||||
val profile: Profile? = null,
|
||||
val isLoading: Boolean = false
|
||||
) {
|
||||
fun toUiState(): ProfileUiState = if (profile == null) {
|
||||
ProfileUiState.NoProfile(isLoading)
|
||||
} else {
|
||||
ProfileUiState.HasProfile(profile, isLoading)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ProfileViewModel(
|
||||
private val profileRepository: ProfileRepository,
|
||||
val onUnauthorized: () -> Unit
|
||||
) : ViewModel() {
|
||||
private val viewModelState = MutableStateFlow(ProfileViewModelState(isLoading = true))
|
||||
|
||||
val uiState = viewModelState
|
||||
.map(ProfileViewModelState::toUiState)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState())
|
||||
|
||||
init {
|
||||
refreshProfile()
|
||||
}
|
||||
|
||||
fun refreshProfile(callback: () -> Unit = {}) {
|
||||
viewModelState.update { it.copy(isLoading = true) }
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = profileRepository.getProfile()
|
||||
|
||||
viewModelState.update {
|
||||
when (result) {
|
||||
is MyResult.Success -> it.copy(profile = result.data, isLoading = false)
|
||||
is MyResult.Failure -> it.copy(profile = null, isLoading = false)
|
||||
}
|
||||
}
|
||||
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
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)
|
||||
@@ -0,0 +1,70 @@
|
||||
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.Group
|
||||
import javax.inject.Inject
|
||||
|
||||
sealed interface ScheduleUiState {
|
||||
val isLoading: Boolean
|
||||
|
||||
data class NoSchedule(
|
||||
override val isLoading: Boolean
|
||||
) : ScheduleUiState
|
||||
|
||||
data class HasSchedule(
|
||||
val group: Group,
|
||||
override val isLoading: Boolean
|
||||
) : ScheduleUiState
|
||||
}
|
||||
|
||||
private data class ScheduleViewModelState(
|
||||
val group: Group? = null,
|
||||
val isLoading: Boolean = false
|
||||
) {
|
||||
fun toUiState(): ScheduleUiState = if (group == null) {
|
||||
ScheduleUiState.NoSchedule(isLoading)
|
||||
} else {
|
||||
ScheduleUiState.HasSchedule(group, isLoading)
|
||||
}
|
||||
}
|
||||
|
||||
@HiltViewModel
|
||||
class ScheduleViewModel @Inject constructor(
|
||||
appContainer: AppContainer
|
||||
) : ViewModel() {
|
||||
private val scheduleRepository = appContainer.scheduleRepository
|
||||
private val viewModelState = MutableStateFlow(ScheduleViewModelState(isLoading = true))
|
||||
|
||||
val uiState = viewModelState
|
||||
.map(ScheduleViewModelState::toUiState)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, viewModelState.value.toUiState())
|
||||
|
||||
init {
|
||||
refreshGroup()
|
||||
}
|
||||
|
||||
fun refreshGroup() {
|
||||
viewModelState.update { it.copy(isLoading = true) }
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = scheduleRepository.getGroup()
|
||||
|
||||
viewModelState.update {
|
||||
when (result) {
|
||||
is MyResult.Success -> it.copy(group = result.data, isLoading = false)
|
||||
is MyResult.Failure -> it.copy(group = null, isLoading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
226
app/src/main/java/ru/n08i40k/polytechnic/next/ui/theme/Color.kt
Normal file
226
app/src/main/java/ru/n08i40k/polytechnic/next/ui/theme/Color.kt
Normal file
@@ -0,0 +1,226 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val primaryLight = Color(0xFF4C662B)
|
||||
val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
val primaryContainerLight = Color(0xFFCDEDA3)
|
||||
val onPrimaryContainerLight = Color(0xFF102000)
|
||||
val secondaryLight = Color(0xFF586249)
|
||||
val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
val secondaryContainerLight = Color(0xFFDCE7C8)
|
||||
val onSecondaryContainerLight = Color(0xFF151E0B)
|
||||
val tertiaryLight = Color(0xFF386663)
|
||||
val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
val tertiaryContainerLight = Color(0xFFBCECE7)
|
||||
val onTertiaryContainerLight = Color(0xFF00201E)
|
||||
val errorLight = Color(0xFFBA1A1A)
|
||||
val onErrorLight = Color(0xFFFFFFFF)
|
||||
val errorContainerLight = Color(0xFFFFDAD6)
|
||||
val onErrorContainerLight = Color(0xFF410002)
|
||||
val backgroundLight = Color(0xFFF9FAEF)
|
||||
val onBackgroundLight = Color(0xFF1A1C16)
|
||||
val surfaceLight = Color(0xFFF9FAEF)
|
||||
val onSurfaceLight = Color(0xFF1A1C16)
|
||||
val surfaceVariantLight = Color(0xFFE1E4D5)
|
||||
val onSurfaceVariantLight = Color(0xFF44483D)
|
||||
val outlineLight = Color(0xFF75796C)
|
||||
val outlineVariantLight = Color(0xFFC5C8BA)
|
||||
val scrimLight = Color(0xFF000000)
|
||||
val inverseSurfaceLight = Color(0xFF2F312A)
|
||||
val inverseOnSurfaceLight = Color(0xFFF1F2E6)
|
||||
val inversePrimaryLight = Color(0xFFB1D18A)
|
||||
val surfaceDimLight = Color(0xFFDADBD0)
|
||||
val surfaceBrightLight = Color(0xFFF9FAEF)
|
||||
val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||
val surfaceContainerLowLight = Color(0xFFF3F4E9)
|
||||
val surfaceContainerLight = Color(0xFFEEEFE3)
|
||||
val surfaceContainerHighLight = Color(0xFFE8E9DE)
|
||||
val surfaceContainerHighestLight = Color(0xFFE2E3D8)
|
||||
|
||||
val primaryLightMediumContrast = Color(0xFF314A12)
|
||||
val onPrimaryLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val primaryContainerLightMediumContrast = Color(0xFF617D3F)
|
||||
val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val secondaryLightMediumContrast = Color(0xFF3C462F)
|
||||
val onSecondaryLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val secondaryContainerLightMediumContrast = Color(0xFF6E785E)
|
||||
val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryLightMediumContrast = Color(0xFF1A4A47)
|
||||
val onTertiaryLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryContainerLightMediumContrast = Color(0xFF4F7D79)
|
||||
val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val errorLightMediumContrast = Color(0xFF8C0009)
|
||||
val onErrorLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val errorContainerLightMediumContrast = Color(0xFFDA342E)
|
||||
val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val backgroundLightMediumContrast = Color(0xFFF9FAEF)
|
||||
val onBackgroundLightMediumContrast = Color(0xFF1A1C16)
|
||||
val surfaceLightMediumContrast = Color(0xFFF9FAEF)
|
||||
val onSurfaceLightMediumContrast = Color(0xFF1A1C16)
|
||||
val surfaceVariantLightMediumContrast = Color(0xFFE1E4D5)
|
||||
val onSurfaceVariantLightMediumContrast = Color(0xFF404439)
|
||||
val outlineLightMediumContrast = Color(0xFF5D6155)
|
||||
val outlineVariantLightMediumContrast = Color(0xFF787C70)
|
||||
val scrimLightMediumContrast = Color(0xFF000000)
|
||||
val inverseSurfaceLightMediumContrast = Color(0xFF2F312A)
|
||||
val inverseOnSurfaceLightMediumContrast = Color(0xFFF1F2E6)
|
||||
val inversePrimaryLightMediumContrast = Color(0xFFB1D18A)
|
||||
val surfaceDimLightMediumContrast = Color(0xFFDADBD0)
|
||||
val surfaceBrightLightMediumContrast = Color(0xFFF9FAEF)
|
||||
val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val surfaceContainerLowLightMediumContrast = Color(0xFFF3F4E9)
|
||||
val surfaceContainerLightMediumContrast = Color(0xFFEEEFE3)
|
||||
val surfaceContainerHighLightMediumContrast = Color(0xFFE8E9DE)
|
||||
val surfaceContainerHighestLightMediumContrast = Color(0xFFE2E3D8)
|
||||
|
||||
val primaryLightHighContrast = Color(0xFF142700)
|
||||
val onPrimaryLightHighContrast = Color(0xFFFFFFFF)
|
||||
val primaryContainerLightHighContrast = Color(0xFF314A12)
|
||||
val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val secondaryLightHighContrast = Color(0xFF1C2511)
|
||||
val onSecondaryLightHighContrast = Color(0xFFFFFFFF)
|
||||
val secondaryContainerLightHighContrast = Color(0xFF3C462F)
|
||||
val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryLightHighContrast = Color(0xFF002725)
|
||||
val onTertiaryLightHighContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryContainerLightHighContrast = Color(0xFF1A4A47)
|
||||
val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val errorLightHighContrast = Color(0xFF4E0002)
|
||||
val onErrorLightHighContrast = Color(0xFFFFFFFF)
|
||||
val errorContainerLightHighContrast = Color(0xFF8C0009)
|
||||
val onErrorContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val backgroundLightHighContrast = Color(0xFFF9FAEF)
|
||||
val onBackgroundLightHighContrast = Color(0xFF1A1C16)
|
||||
val surfaceLightHighContrast = Color(0xFFF9FAEF)
|
||||
val onSurfaceLightHighContrast = Color(0xFF000000)
|
||||
val surfaceVariantLightHighContrast = Color(0xFFE1E4D5)
|
||||
val onSurfaceVariantLightHighContrast = Color(0xFF21251C)
|
||||
val outlineLightHighContrast = Color(0xFF404439)
|
||||
val outlineVariantLightHighContrast = Color(0xFF404439)
|
||||
val scrimLightHighContrast = Color(0xFF000000)
|
||||
val inverseSurfaceLightHighContrast = Color(0xFF2F312A)
|
||||
val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)
|
||||
val inversePrimaryLightHighContrast = Color(0xFFD6F7AC)
|
||||
val surfaceDimLightHighContrast = Color(0xFFDADBD0)
|
||||
val surfaceBrightLightHighContrast = Color(0xFFF9FAEF)
|
||||
val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)
|
||||
val surfaceContainerLowLightHighContrast = Color(0xFFF3F4E9)
|
||||
val surfaceContainerLightHighContrast = Color(0xFFEEEFE3)
|
||||
val surfaceContainerHighLightHighContrast = Color(0xFFE8E9DE)
|
||||
val surfaceContainerHighestLightHighContrast = Color(0xFFE2E3D8)
|
||||
|
||||
val primaryDark = Color(0xFFB1D18A)
|
||||
val onPrimaryDark = Color(0xFF1F3701)
|
||||
val primaryContainerDark = Color(0xFF354E16)
|
||||
val onPrimaryContainerDark = Color(0xFFCDEDA3)
|
||||
val secondaryDark = Color(0xFFBFCBAD)
|
||||
val onSecondaryDark = Color(0xFF2A331E)
|
||||
val secondaryContainerDark = Color(0xFF404A33)
|
||||
val onSecondaryContainerDark = Color(0xFFDCE7C8)
|
||||
val tertiaryDark = Color(0xFFA0D0CB)
|
||||
val onTertiaryDark = Color(0xFF003735)
|
||||
val tertiaryContainerDark = Color(0xFF1F4E4B)
|
||||
val onTertiaryContainerDark = Color(0xFFBCECE7)
|
||||
val errorDark = Color(0xFFFFB4AB)
|
||||
val onErrorDark = Color(0xFF690005)
|
||||
val errorContainerDark = Color(0xFF93000A)
|
||||
val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||
val backgroundDark = Color(0xFF12140E)
|
||||
val onBackgroundDark = Color(0xFFE2E3D8)
|
||||
val surfaceDark = Color(0xFF12140E)
|
||||
val onSurfaceDark = Color(0xFFE2E3D8)
|
||||
val surfaceVariantDark = Color(0xFF44483D)
|
||||
val onSurfaceVariantDark = Color(0xFFC5C8BA)
|
||||
val outlineDark = Color(0xFF8F9285)
|
||||
val outlineVariantDark = Color(0xFF44483D)
|
||||
val scrimDark = Color(0xFF000000)
|
||||
val inverseSurfaceDark = Color(0xFFE2E3D8)
|
||||
val inverseOnSurfaceDark = Color(0xFF2F312A)
|
||||
val inversePrimaryDark = Color(0xFF4C662B)
|
||||
val surfaceDimDark = Color(0xFF12140E)
|
||||
val surfaceBrightDark = Color(0xFF383A32)
|
||||
val surfaceContainerLowestDark = Color(0xFF0C0F09)
|
||||
val surfaceContainerLowDark = Color(0xFF1A1C16)
|
||||
val surfaceContainerDark = Color(0xFF1E201A)
|
||||
val surfaceContainerHighDark = Color(0xFF282B24)
|
||||
val surfaceContainerHighestDark = Color(0xFF33362E)
|
||||
|
||||
val primaryDarkMediumContrast = Color(0xFFB5D58E)
|
||||
val onPrimaryDarkMediumContrast = Color(0xFF0C1A00)
|
||||
val primaryContainerDarkMediumContrast = Color(0xFF7D9A59)
|
||||
val onPrimaryContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val secondaryDarkMediumContrast = Color(0xFFC4CFB1)
|
||||
val onSecondaryDarkMediumContrast = Color(0xFF101907)
|
||||
val secondaryContainerDarkMediumContrast = Color(0xFF8A9579)
|
||||
val onSecondaryContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val tertiaryDarkMediumContrast = Color(0xFFA4D4D0)
|
||||
val onTertiaryDarkMediumContrast = Color(0xFF001A19)
|
||||
val tertiaryContainerDarkMediumContrast = Color(0xFF6B9995)
|
||||
val onTertiaryContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val errorDarkMediumContrast = Color(0xFFFFBAB1)
|
||||
val onErrorDarkMediumContrast = Color(0xFF370001)
|
||||
val errorContainerDarkMediumContrast = Color(0xFFFF5449)
|
||||
val onErrorContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val backgroundDarkMediumContrast = Color(0xFF12140E)
|
||||
val onBackgroundDarkMediumContrast = Color(0xFFE2E3D8)
|
||||
val surfaceDarkMediumContrast = Color(0xFF12140E)
|
||||
val onSurfaceDarkMediumContrast = Color(0xFFFBFCF0)
|
||||
val surfaceVariantDarkMediumContrast = Color(0xFF44483D)
|
||||
val onSurfaceVariantDarkMediumContrast = Color(0xFFC9CCBE)
|
||||
val outlineDarkMediumContrast = Color(0xFFA1A497)
|
||||
val outlineVariantDarkMediumContrast = Color(0xFF818578)
|
||||
val scrimDarkMediumContrast = Color(0xFF000000)
|
||||
val inverseSurfaceDarkMediumContrast = Color(0xFFE2E3D8)
|
||||
val inverseOnSurfaceDarkMediumContrast = Color(0xFF282B24)
|
||||
val inversePrimaryDarkMediumContrast = Color(0xFF364F17)
|
||||
val surfaceDimDarkMediumContrast = Color(0xFF12140E)
|
||||
val surfaceBrightDarkMediumContrast = Color(0xFF383A32)
|
||||
val surfaceContainerLowestDarkMediumContrast = Color(0xFF0C0F09)
|
||||
val surfaceContainerLowDarkMediumContrast = Color(0xFF1A1C16)
|
||||
val surfaceContainerDarkMediumContrast = Color(0xFF1E201A)
|
||||
val surfaceContainerHighDarkMediumContrast = Color(0xFF282B24)
|
||||
val surfaceContainerHighestDarkMediumContrast = Color(0xFF33362E)
|
||||
|
||||
val primaryDarkHighContrast = Color(0xFFF4FFDF)
|
||||
val onPrimaryDarkHighContrast = Color(0xFF000000)
|
||||
val primaryContainerDarkHighContrast = Color(0xFFB5D58E)
|
||||
val onPrimaryContainerDarkHighContrast = Color(0xFF000000)
|
||||
val secondaryDarkHighContrast = Color(0xFFF4FFDF)
|
||||
val onSecondaryDarkHighContrast = Color(0xFF000000)
|
||||
val secondaryContainerDarkHighContrast = Color(0xFFC4CFB1)
|
||||
val onSecondaryContainerDarkHighContrast = Color(0xFF000000)
|
||||
val tertiaryDarkHighContrast = Color(0xFFEAFFFC)
|
||||
val onTertiaryDarkHighContrast = Color(0xFF000000)
|
||||
val tertiaryContainerDarkHighContrast = Color(0xFFA4D4D0)
|
||||
val onTertiaryContainerDarkHighContrast = Color(0xFF000000)
|
||||
val errorDarkHighContrast = Color(0xFFFFF9F9)
|
||||
val onErrorDarkHighContrast = Color(0xFF000000)
|
||||
val errorContainerDarkHighContrast = Color(0xFFFFBAB1)
|
||||
val onErrorContainerDarkHighContrast = Color(0xFF000000)
|
||||
val backgroundDarkHighContrast = Color(0xFF12140E)
|
||||
val onBackgroundDarkHighContrast = Color(0xFFE2E3D8)
|
||||
val surfaceDarkHighContrast = Color(0xFF12140E)
|
||||
val onSurfaceDarkHighContrast = Color(0xFFFFFFFF)
|
||||
val surfaceVariantDarkHighContrast = Color(0xFF44483D)
|
||||
val onSurfaceVariantDarkHighContrast = Color(0xFFF9FCED)
|
||||
val outlineDarkHighContrast = Color(0xFFC9CCBE)
|
||||
val outlineVariantDarkHighContrast = Color(0xFFC9CCBE)
|
||||
val scrimDarkHighContrast = Color(0xFF000000)
|
||||
val inverseSurfaceDarkHighContrast = Color(0xFFE2E3D8)
|
||||
val inverseOnSurfaceDarkHighContrast = Color(0xFF000000)
|
||||
val inversePrimaryDarkHighContrast = Color(0xFF1A3000)
|
||||
val surfaceDimDarkHighContrast = Color(0xFF12140E)
|
||||
val surfaceBrightDarkHighContrast = Color(0xFF383A32)
|
||||
val surfaceContainerLowestDarkHighContrast = Color(0xFF0C0F09)
|
||||
val surfaceContainerLowDarkHighContrast = Color(0xFF1A1C16)
|
||||
val surfaceContainerDarkHighContrast = Color(0xFF1E201A)
|
||||
val surfaceContainerHighDarkHighContrast = Color(0xFF282B24)
|
||||
val surfaceContainerHighestDarkHighContrast = Color(0xFF33362E)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
278
app/src/main/java/ru/n08i40k/polytechnic/next/ui/theme/Theme.kt
Normal file
278
app/src/main/java/ru/n08i40k/polytechnic/next/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,278 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val lightScheme = lightColorScheme(
|
||||
primary = primaryLight,
|
||||
onPrimary = onPrimaryLight,
|
||||
primaryContainer = primaryContainerLight,
|
||||
onPrimaryContainer = onPrimaryContainerLight,
|
||||
secondary = secondaryLight,
|
||||
onSecondary = onSecondaryLight,
|
||||
secondaryContainer = secondaryContainerLight,
|
||||
onSecondaryContainer = onSecondaryContainerLight,
|
||||
tertiary = tertiaryLight,
|
||||
onTertiary = onTertiaryLight,
|
||||
tertiaryContainer = tertiaryContainerLight,
|
||||
onTertiaryContainer = onTertiaryContainerLight,
|
||||
error = errorLight,
|
||||
onError = onErrorLight,
|
||||
errorContainer = errorContainerLight,
|
||||
onErrorContainer = onErrorContainerLight,
|
||||
background = backgroundLight,
|
||||
onBackground = onBackgroundLight,
|
||||
surface = surfaceLight,
|
||||
onSurface = onSurfaceLight,
|
||||
surfaceVariant = surfaceVariantLight,
|
||||
onSurfaceVariant = onSurfaceVariantLight,
|
||||
outline = outlineLight,
|
||||
outlineVariant = outlineVariantLight,
|
||||
scrim = scrimLight,
|
||||
inverseSurface = inverseSurfaceLight,
|
||||
inverseOnSurface = inverseOnSurfaceLight,
|
||||
inversePrimary = inversePrimaryLight,
|
||||
surfaceDim = surfaceDimLight,
|
||||
surfaceBright = surfaceBrightLight,
|
||||
surfaceContainerLowest = surfaceContainerLowestLight,
|
||||
surfaceContainerLow = surfaceContainerLowLight,
|
||||
surfaceContainer = surfaceContainerLight,
|
||||
surfaceContainerHigh = surfaceContainerHighLight,
|
||||
surfaceContainerHighest = surfaceContainerHighestLight,
|
||||
)
|
||||
|
||||
private val darkScheme = darkColorScheme(
|
||||
primary = primaryDark,
|
||||
onPrimary = onPrimaryDark,
|
||||
primaryContainer = primaryContainerDark,
|
||||
onPrimaryContainer = onPrimaryContainerDark,
|
||||
secondary = secondaryDark,
|
||||
onSecondary = onSecondaryDark,
|
||||
secondaryContainer = secondaryContainerDark,
|
||||
onSecondaryContainer = onSecondaryContainerDark,
|
||||
tertiary = tertiaryDark,
|
||||
onTertiary = onTertiaryDark,
|
||||
tertiaryContainer = tertiaryContainerDark,
|
||||
onTertiaryContainer = onTertiaryContainerDark,
|
||||
error = errorDark,
|
||||
onError = onErrorDark,
|
||||
errorContainer = errorContainerDark,
|
||||
onErrorContainer = onErrorContainerDark,
|
||||
background = backgroundDark,
|
||||
onBackground = onBackgroundDark,
|
||||
surface = surfaceDark,
|
||||
onSurface = onSurfaceDark,
|
||||
surfaceVariant = surfaceVariantDark,
|
||||
onSurfaceVariant = onSurfaceVariantDark,
|
||||
outline = outlineDark,
|
||||
outlineVariant = outlineVariantDark,
|
||||
scrim = scrimDark,
|
||||
inverseSurface = inverseSurfaceDark,
|
||||
inverseOnSurface = inverseOnSurfaceDark,
|
||||
inversePrimary = inversePrimaryDark,
|
||||
surfaceDim = surfaceDimDark,
|
||||
surfaceBright = surfaceBrightDark,
|
||||
surfaceContainerLowest = surfaceContainerLowestDark,
|
||||
surfaceContainerLow = surfaceContainerLowDark,
|
||||
surfaceContainer = surfaceContainerDark,
|
||||
surfaceContainerHigh = surfaceContainerHighDark,
|
||||
surfaceContainerHighest = surfaceContainerHighestDark,
|
||||
)
|
||||
|
||||
private val mediumContrastLightColorScheme = lightColorScheme(
|
||||
primary = primaryLightMediumContrast,
|
||||
onPrimary = onPrimaryLightMediumContrast,
|
||||
primaryContainer = primaryContainerLightMediumContrast,
|
||||
onPrimaryContainer = onPrimaryContainerLightMediumContrast,
|
||||
secondary = secondaryLightMediumContrast,
|
||||
onSecondary = onSecondaryLightMediumContrast,
|
||||
secondaryContainer = secondaryContainerLightMediumContrast,
|
||||
onSecondaryContainer = onSecondaryContainerLightMediumContrast,
|
||||
tertiary = tertiaryLightMediumContrast,
|
||||
onTertiary = onTertiaryLightMediumContrast,
|
||||
tertiaryContainer = tertiaryContainerLightMediumContrast,
|
||||
onTertiaryContainer = onTertiaryContainerLightMediumContrast,
|
||||
error = errorLightMediumContrast,
|
||||
onError = onErrorLightMediumContrast,
|
||||
errorContainer = errorContainerLightMediumContrast,
|
||||
onErrorContainer = onErrorContainerLightMediumContrast,
|
||||
background = backgroundLightMediumContrast,
|
||||
onBackground = onBackgroundLightMediumContrast,
|
||||
surface = surfaceLightMediumContrast,
|
||||
onSurface = onSurfaceLightMediumContrast,
|
||||
surfaceVariant = surfaceVariantLightMediumContrast,
|
||||
onSurfaceVariant = onSurfaceVariantLightMediumContrast,
|
||||
outline = outlineLightMediumContrast,
|
||||
outlineVariant = outlineVariantLightMediumContrast,
|
||||
scrim = scrimLightMediumContrast,
|
||||
inverseSurface = inverseSurfaceLightMediumContrast,
|
||||
inverseOnSurface = inverseOnSurfaceLightMediumContrast,
|
||||
inversePrimary = inversePrimaryLightMediumContrast,
|
||||
surfaceDim = surfaceDimLightMediumContrast,
|
||||
surfaceBright = surfaceBrightLightMediumContrast,
|
||||
surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
|
||||
surfaceContainerLow = surfaceContainerLowLightMediumContrast,
|
||||
surfaceContainer = surfaceContainerLightMediumContrast,
|
||||
surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
|
||||
surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,
|
||||
)
|
||||
|
||||
private val highContrastLightColorScheme = lightColorScheme(
|
||||
primary = primaryLightHighContrast,
|
||||
onPrimary = onPrimaryLightHighContrast,
|
||||
primaryContainer = primaryContainerLightHighContrast,
|
||||
onPrimaryContainer = onPrimaryContainerLightHighContrast,
|
||||
secondary = secondaryLightHighContrast,
|
||||
onSecondary = onSecondaryLightHighContrast,
|
||||
secondaryContainer = secondaryContainerLightHighContrast,
|
||||
onSecondaryContainer = onSecondaryContainerLightHighContrast,
|
||||
tertiary = tertiaryLightHighContrast,
|
||||
onTertiary = onTertiaryLightHighContrast,
|
||||
tertiaryContainer = tertiaryContainerLightHighContrast,
|
||||
onTertiaryContainer = onTertiaryContainerLightHighContrast,
|
||||
error = errorLightHighContrast,
|
||||
onError = onErrorLightHighContrast,
|
||||
errorContainer = errorContainerLightHighContrast,
|
||||
onErrorContainer = onErrorContainerLightHighContrast,
|
||||
background = backgroundLightHighContrast,
|
||||
onBackground = onBackgroundLightHighContrast,
|
||||
surface = surfaceLightHighContrast,
|
||||
onSurface = onSurfaceLightHighContrast,
|
||||
surfaceVariant = surfaceVariantLightHighContrast,
|
||||
onSurfaceVariant = onSurfaceVariantLightHighContrast,
|
||||
outline = outlineLightHighContrast,
|
||||
outlineVariant = outlineVariantLightHighContrast,
|
||||
scrim = scrimLightHighContrast,
|
||||
inverseSurface = inverseSurfaceLightHighContrast,
|
||||
inverseOnSurface = inverseOnSurfaceLightHighContrast,
|
||||
inversePrimary = inversePrimaryLightHighContrast,
|
||||
surfaceDim = surfaceDimLightHighContrast,
|
||||
surfaceBright = surfaceBrightLightHighContrast,
|
||||
surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
|
||||
surfaceContainerLow = surfaceContainerLowLightHighContrast,
|
||||
surfaceContainer = surfaceContainerLightHighContrast,
|
||||
surfaceContainerHigh = surfaceContainerHighLightHighContrast,
|
||||
surfaceContainerHighest = surfaceContainerHighestLightHighContrast,
|
||||
)
|
||||
|
||||
private val mediumContrastDarkColorScheme = darkColorScheme(
|
||||
primary = primaryDarkMediumContrast,
|
||||
onPrimary = onPrimaryDarkMediumContrast,
|
||||
primaryContainer = primaryContainerDarkMediumContrast,
|
||||
onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
|
||||
secondary = secondaryDarkMediumContrast,
|
||||
onSecondary = onSecondaryDarkMediumContrast,
|
||||
secondaryContainer = secondaryContainerDarkMediumContrast,
|
||||
onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
|
||||
tertiary = tertiaryDarkMediumContrast,
|
||||
onTertiary = onTertiaryDarkMediumContrast,
|
||||
tertiaryContainer = tertiaryContainerDarkMediumContrast,
|
||||
onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
|
||||
error = errorDarkMediumContrast,
|
||||
onError = onErrorDarkMediumContrast,
|
||||
errorContainer = errorContainerDarkMediumContrast,
|
||||
onErrorContainer = onErrorContainerDarkMediumContrast,
|
||||
background = backgroundDarkMediumContrast,
|
||||
onBackground = onBackgroundDarkMediumContrast,
|
||||
surface = surfaceDarkMediumContrast,
|
||||
onSurface = onSurfaceDarkMediumContrast,
|
||||
surfaceVariant = surfaceVariantDarkMediumContrast,
|
||||
onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
|
||||
outline = outlineDarkMediumContrast,
|
||||
outlineVariant = outlineVariantDarkMediumContrast,
|
||||
scrim = scrimDarkMediumContrast,
|
||||
inverseSurface = inverseSurfaceDarkMediumContrast,
|
||||
inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
|
||||
inversePrimary = inversePrimaryDarkMediumContrast,
|
||||
surfaceDim = surfaceDimDarkMediumContrast,
|
||||
surfaceBright = surfaceBrightDarkMediumContrast,
|
||||
surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
|
||||
surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
|
||||
surfaceContainer = surfaceContainerDarkMediumContrast,
|
||||
surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
|
||||
surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,
|
||||
)
|
||||
|
||||
private val highContrastDarkColorScheme = darkColorScheme(
|
||||
primary = primaryDarkHighContrast,
|
||||
onPrimary = onPrimaryDarkHighContrast,
|
||||
primaryContainer = primaryContainerDarkHighContrast,
|
||||
onPrimaryContainer = onPrimaryContainerDarkHighContrast,
|
||||
secondary = secondaryDarkHighContrast,
|
||||
onSecondary = onSecondaryDarkHighContrast,
|
||||
secondaryContainer = secondaryContainerDarkHighContrast,
|
||||
onSecondaryContainer = onSecondaryContainerDarkHighContrast,
|
||||
tertiary = tertiaryDarkHighContrast,
|
||||
onTertiary = onTertiaryDarkHighContrast,
|
||||
tertiaryContainer = tertiaryContainerDarkHighContrast,
|
||||
onTertiaryContainer = onTertiaryContainerDarkHighContrast,
|
||||
error = errorDarkHighContrast,
|
||||
onError = onErrorDarkHighContrast,
|
||||
errorContainer = errorContainerDarkHighContrast,
|
||||
onErrorContainer = onErrorContainerDarkHighContrast,
|
||||
background = backgroundDarkHighContrast,
|
||||
onBackground = onBackgroundDarkHighContrast,
|
||||
surface = surfaceDarkHighContrast,
|
||||
onSurface = onSurfaceDarkHighContrast,
|
||||
surfaceVariant = surfaceVariantDarkHighContrast,
|
||||
onSurfaceVariant = onSurfaceVariantDarkHighContrast,
|
||||
outline = outlineDarkHighContrast,
|
||||
outlineVariant = outlineVariantDarkHighContrast,
|
||||
scrim = scrimDarkHighContrast,
|
||||
inverseSurface = inverseSurfaceDarkHighContrast,
|
||||
inverseOnSurface = inverseOnSurfaceDarkHighContrast,
|
||||
inversePrimary = inversePrimaryDarkHighContrast,
|
||||
surfaceDim = surfaceDimDarkHighContrast,
|
||||
surfaceBright = surfaceBrightDarkHighContrast,
|
||||
surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
|
||||
surfaceContainerLow = surfaceContainerLowDarkHighContrast,
|
||||
surfaceContainer = surfaceContainerDarkHighContrast,
|
||||
surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
|
||||
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class ColorFamily(
|
||||
val color: Color,
|
||||
val onColor: Color,
|
||||
val colorContainer: Color,
|
||||
val onColorContainer: Color
|
||||
)
|
||||
|
||||
val unspecified_scheme = ColorFamily(
|
||||
Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun AppTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable() () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> darkScheme
|
||||
else -> lightScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = AppTypography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package ru.n08i40k.polytechnic.next.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
|
||||
val AppTypography = Typography()
|
||||
@@ -0,0 +1,27 @@
|
||||
package ru.n08i40k.polytechnic.next.utils
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
open class EnumAsIntSerializer<T : Enum<*>>(
|
||||
serialName: String,
|
||||
val serialize: (v: T) -> Int,
|
||||
val deserialize: (v: Int) -> T
|
||||
) : KSerializer<T> {
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor(serialName, PrimitiveKind.INT)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: T) {
|
||||
encoder.encodeInt(serialize(value))
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): T {
|
||||
val v = decoder.decodeInt()
|
||||
return deserialize(v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package ru.n08i40k.polytechnic.next.utils
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
open class EnumAsStringSerializer<T : Enum<*>>(
|
||||
serialName: String,
|
||||
val serialize: (v: T) -> String,
|
||||
val deserialize: (v: String) -> T
|
||||
) : KSerializer<T> {
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor(serialName, PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: T) {
|
||||
encoder.encodeString(serialize(value))
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): T {
|
||||
val v = decoder.decodeString()
|
||||
return deserialize(v)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package ru.n08i40k.polytechnic.next.utils
|
||||
|
||||
data class ErrorMessage(val message: String)
|
||||
10
app/src/main/proto/settings.proto
Normal file
10
app/src/main/proto/settings.proto
Normal file
@@ -0,0 +1,10 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option java_package = "ru.n08i40k.polytechnic.next";
|
||||
option java_multiple_files = true;
|
||||
|
||||
message Settings {
|
||||
string user_id = 1;
|
||||
string access_token = 2;
|
||||
string group = 3;
|
||||
}
|
||||
16
app/src/main/res/drawable/ic_launcher.xml
Normal file
16
app/src/main/res/drawable/ic_launcher.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/logo"
|
||||
android:inset="20dp" />
|
||||
</foreground>
|
||||
|
||||
<background android:drawable="@color/white" />
|
||||
|
||||
<monochrome>
|
||||
<inset
|
||||
android:drawable="@drawable/logo"
|
||||
android:inset="20dp" />
|
||||
</monochrome>
|
||||
</adaptive-icon>
|
||||
16
app/src/main/res/drawable/ic_launcher_round.xml
Normal file
16
app/src/main/res/drawable/ic_launcher_round.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/logo"
|
||||
android:inset="20dp" />
|
||||
</foreground>
|
||||
|
||||
<background android:drawable="@color/white" />
|
||||
|
||||
<monochrome>
|
||||
<inset
|
||||
android:drawable="@drawable/logo"
|
||||
android:inset="20dp" />
|
||||
</monochrome>
|
||||
</adaptive-icon>
|
||||
12
app/src/main/res/drawable/logo.xml
Normal file
12
app/src/main/res/drawable/logo.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="81.5dp"
|
||||
android:height="81.5dp"
|
||||
android:viewportWidth="81.5"
|
||||
android:viewportHeight="81.5">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M51.89,64.09c0,1.22 -0.06,2.8 2.48,4.03 2.55,1.23 5.58,-2.19 7.04,-3.69 1.46,-1.5 5.46,-6.51 5.46,-7.91 0,-0.77 -1.56,-2.28 -2.77,-0.36l-2,3.14c-1.45,2.13 -8.07,9.83 -7.43,4.3 0.74,-6.42 6.07,-28.1 6.07,-28.76 0,-1.45 -2.03,-2.42 -2.88,-0.1l-4.98,24.31c-0.31,1.21 -0.49,2.44 -0.77,3.62 -0.14,0.61 -0.22,0.69 -0.22,1.43h0Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M5.33,39.39c0,1.71 2.35,1.81 3.01,-0.31 0.17,-0.53 0.17,-0.77 0.36,-1.28l1.6,-3.35c0.43,-0.76 0.78,-1.48 1.27,-2.22l4.21,-5.89c0.59,-0.79 1.09,-1.08 1.67,-1.82l2.21,-2.2c2.95,-2.54 5.98,-4.96 9.83,-6.18 11.36,-3.5 19.74,2.13 19.28,14.3 0,0.7 -1.94,1.18 -2.58,1.47 -4.35,1.9 -8.65,3.99 -13.11,5.69 -2.01,0.76 -3.26,1.1 -3.63,2.01 -0.37,0.91 0.13,1.85 1.01,1.98 0.88,0.12 2.75,-0.86 3.87,-1.33 1.08,-0.45 12.96,-6.07 14.07,-6.16 -0.24,1.97 -0.92,4.08 -1.58,5.93l-0.54,1.29c-2.27,5.24 -5.46,9.65 -9.08,14.04 -2.46,2.83 -5.74,6.02 -8.98,7.93l-5.24,2.5c-4.75,1.77 -10.16,0.07 -13.25,-3.7 -0.41,-0.5 -0.69,-1.31 -0.84,-1.91 -0.22,-0.93 0.07,-2.48 -0.6,-3.24 -0.85,-0.96 -2.41,-0.43 -2.41,0.92 0,5.07 2.79,7.94 7.15,10.11 3.8,1.89 8.59,1.37 12.37,-0.14l1.78,-0.79c5.11,-2.78 9.5,-6.62 13.14,-11.13 3.48,-4.27 6.64,-8.87 8.74,-14.02l1.11,-3.11c0.29,-0.82 0.43,-1.38 0.57,-2.17 0.98,-3.89 -0.32,-3.35 2.5,-4.66 7.13,-3.13 14.12,-6.43 21.3,-9.47 0.77,-0.33 1.43,-0.41 1.6,-1.34 0.1,-0.55 -0.21,-1.32 -0.97,-1.54 -0.75,-0.22 -1.36,0.26 -2.07,0.55 -7.55,3.07 -15.9,7.61 -19.81,8.78 -2.85,1.53 -1.9,-0.01 -2.42,-4.66 -1.17,-6.1 -5.82,-10.61 -12,-11.61 -6.59,-1.2 -12.17,1.22 -17.45,4.66l-0.4,0.34s-0.07,0.06 -0.1,0.08l-2.52,2.08c-3.35,2.69 -6,6.24 -8.39,9.8 -1.41,2.13 -4.7,7.55 -4.7,9.79Z"/>
|
||||
</vector>
|
||||
27
app/src/main/res/raw/ssl.pem
Normal file
27
app/src/main/res/raw/ssl.pem
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEpjCCAw6gAwIBAgIRAO5cnXTnxJXAIMgjHjt8VNAwDQYJKoZIhvcNAQELBQAw
|
||||
azEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSAwHgYDVQQLDBdIT01F
|
||||
LVBDXG4wOGk0MGtASE9NRS1QQzEnMCUGA1UEAwwebWtjZXJ0IEhPTUUtUENcbjA4
|
||||
aTQwa0BIT01FLVBDMB4XDTI0MDQyMTIyMjE1MVoXDTM0MDQyMTIyMjE1MVowazEe
|
||||
MBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSAwHgYDVQQLDBdIT01FLVBD
|
||||
XG4wOGk0MGtASE9NRS1QQzEnMCUGA1UEAwwebWtjZXJ0IEhPTUUtUENcbjA4aTQw
|
||||
a0BIT01FLVBDMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxdT1huby
|
||||
eoG+X/kJoh1tn6jVJB/zN1r0/O7PZSZXJ9qYHufl6IwEfPlKJPNfpYifiSZ1x/R+
|
||||
flzmfk8Q3L/njIh3WOURmKhPuHyUnruVQnM/66FqRAp0gSntuGnQ8y/JEkSRtzRI
|
||||
p75Rp/1NktDRqi8UOq1+aXCKwyh/jcu2gaJ9L0EKRqpxrGjm2hF3yYRQU/DRcWhc
|
||||
VJKpMW4TpelRxUTM5QSdqsoe+GoKgLQeHzLXRSmRdTMFql/yG3Cy3MfYNR/oA+Nu
|
||||
Nlt1ozPBTrtQ0LhTlToJkALB2cBxodRGJs871eDqzTzDHbJ6+OjlNy4xtZEgDTqt
|
||||
XIl0wexOTjVk+31ClLsaqtfWDsyFPpAG4G7+eAUNPXY1MSiui3SmF5G3tLKUMmci
|
||||
A6hV7MSBOhSF+Rl0pzZ6t8/aANmZerpALymRRzpp93z3evdQqRtki1oi4x1SIb5h
|
||||
cNHgvwTzb6vSxd17lTiB78bWGVsDO3wy7JMuWRdA/AClxufeA8d7RlkPAgMBAAGj
|
||||
RTBDMA4GA1UdDwEB/wQEAwICBDASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW
|
||||
BBRR8ZgtYNodDB7WWBFlx8lNwWlkZDANBgkqhkiG9w0BAQsFAAOCAYEAtMxjGIF0
|
||||
7I3UzM/2e/VWV8zaKRP4y7Nc3SbmygPi1kE+MJUtGyGqnOtVvTwVqmN1YMYe7Z3f
|
||||
/IsjuMJfmDGq71gNf/frGDykOojpGnzk7R0d4VDWGA3cP+urSumilcz5a/nCfrps
|
||||
zU5ZIM53t30Eg1FUdpJ84a9n2MF5rTBoFwez1JoeUUYdSI2SpbOHCAIzH3VLfQX0
|
||||
n/80aWxrykpPsvqVE171fpq7JHYXgftCR/aEOrMNpq0mlv3uQ8nJ0ejLcHG5FOCn
|
||||
0sjx3ol/5VbVQItfhUHzQab0ubNaxl3VhxJEuqlvqJn0A0VoisCARPp1k9IIwz78
|
||||
LuuPUMjzmazBWgtE4Sj95M159f+A7tHeTXRDElSVb1QOAiHiFwBR6Eiw4jfjzJMi
|
||||
RbZTh4aZ7YNtMFLROv3B8CP9TfcdyX/SeKRFwBp9mCotg9pxNuDwrwQ36+zyxv4N
|
||||
W98lNodK590KS7F7P1P1oFS+cguzxiGJ3du4O5TTOY7VdgS+gz8DlBE+
|
||||
-----END CERTIFICATE-----
|
||||
1
app/src/main/res/resources.properties
Normal file
1
app/src/main/res/resources.properties
Normal file
@@ -0,0 +1 @@
|
||||
unqualifiedResLocale=ru-RU
|
||||
33
app/src/main/res/values-ru/strings.xml
Normal file
33
app/src/main/res/values-ru/strings.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Политехникум</string>
|
||||
<string name="username">Имя пользователя</string>
|
||||
<string name="password">Пароль</string>
|
||||
<string name="register">Зарегистрироваться</string>
|
||||
<string name="login">Авторизоваться</string>
|
||||
<string name="login_title">Авторизация</string>
|
||||
<string name="register_title">Регистрация</string>
|
||||
<string name="not_registered">Не зарегистрированы?</string>
|
||||
<string name="already_registered">Уже зарегистрированы?</string>
|
||||
<string name="reload">Перезагрузить</string>
|
||||
<string name="lesson_teacher">Преподаватель</string>
|
||||
<string name="lesson_teachers">Преподаватели</string>
|
||||
<string name="lesson_duration">Длительность</string>
|
||||
<string name="hours">ч.</string>
|
||||
<string name="minutes">мин.</string>
|
||||
<string name="lesson_break">Перемена</string>
|
||||
<string name="schedule">Расписание</string>
|
||||
<string name="profile">Профиль</string>
|
||||
<string name="role_student">Студент</string>
|
||||
<string name="role_teacher">Преподаватель</string>
|
||||
<string name="role_admin">Администратор</string>
|
||||
<string name="group">Группа</string>
|
||||
<string name="role">Роль</string>
|
||||
<string name="day_null">Расписание ещё не обновилось.</string>
|
||||
<string name="old_password">Старый пароль</string>
|
||||
<string name="new_password">Новый пароль</string>
|
||||
<string name="loading">Загрузка…</string>
|
||||
<string name="change_password">Сменить пароль</string>
|
||||
<string name="change_username">Сменить имя пользователя</string>
|
||||
<string name="change_group">Сменить группу</string>
|
||||
</resources>
|
||||
10
app/src/main/res/values/colors.xml
Normal file
10
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
33
app/src/main/res/values/strings.xml
Normal file
33
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<resources>
|
||||
<string name="app_name">Polytechnic</string>
|
||||
|
||||
<string name="username">Username</string>
|
||||
<string name="password">Password</string>
|
||||
<string name="register">Register</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="login_title">Login</string>
|
||||
<string name="register_title">Registration</string>
|
||||
<string name="not_registered">Not registered?</string>
|
||||
<string name="already_registered">Already registered?</string>
|
||||
<string name="reload">Reload</string>
|
||||
<string name="lesson_teacher">Teacher</string>
|
||||
<string name="lesson_teachers">Teachers</string>
|
||||
<string name="lesson_duration">Duration</string>
|
||||
<string name="hours">h.</string>
|
||||
<string name="minutes">min.</string>
|
||||
<string name="lesson_break">Break</string>
|
||||
<string name="schedule">Schedule</string>
|
||||
<string name="profile">Profile</string>
|
||||
<string name="role_student">Student</string>
|
||||
<string name="role_teacher">Teacher</string>
|
||||
<string name="role_admin">Administrator</string>
|
||||
<string name="group">Group</string>
|
||||
<string name="role">Role</string>
|
||||
<string name="day_null">Schedule not updated yet.</string>
|
||||
<string name="old_password">Old password</string>
|
||||
<string name="new_password">New password</string>
|
||||
<string name="change_password">Change password</string>
|
||||
<string name="change_username">Change username</string>
|
||||
<string name="loading">Loading…</string>
|
||||
<string name="change_group">Change group</string>
|
||||
</resources>
|
||||
5
app/src/main/res/values/themes.xml
Normal file
5
app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.PolytechnicNext" parent="android:Theme.Material.NoActionBar" />
|
||||
</resources>
|
||||
13
app/src/main/res/xml/backup_rules.xml
Normal file
13
app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older that API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
19
app/src/main/res/xml/data_extraction_rules.xml
Normal file
19
app/src/main/res/xml/data_extraction_rules.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
@@ -0,0 +1,17 @@
|
||||
package ru.n08i40k.polytechnic.next
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user