Первый коммит

This commit is contained in:
2024-09-15 14:39:30 +04:00
commit 4c75b87656
86 changed files with 4446 additions and 0 deletions

View 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>

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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>
}

View File

@@ -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!!)
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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>
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}
}

View 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?>
)

View File

@@ -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
)

View File

@@ -0,0 +1,5 @@
package ru.n08i40k.polytechnic.next.network
object NetworkValues {
const val API_HOST = "https://192.168.0.103:5050/api/v1/"
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,7 @@
package ru.n08i40k.polytechnic.next.network
class ResponseBase {
fun handleResponse(response: String?) {
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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)

View File

@@ -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()
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()
}
}

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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()
}
}

View File

@@ -0,0 +1,6 @@
package ru.n08i40k.polytechnic.next.network.data.profile
import kotlinx.serialization.Serializable
@Serializable
data class ChangeGroupRequestData(val group: String)

View File

@@ -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()
}
}

View File

@@ -0,0 +1,6 @@
package ru.n08i40k.polytechnic.next.network.data.profile
import kotlinx.serialization.Serializable
@Serializable
data class ChangeUsernameRequestData(val username: String)

View File

@@ -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
)

View File

@@ -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()
}
}

View File

@@ -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
)

View File

@@ -0,0 +1,8 @@
package ru.n08i40k.polytechnic.next.network.data.schedule
import kotlinx.serialization.Serializable
@Serializable
data class ScheduleGetGroupNamesResponseData(
val names: ArrayList<String>,
)

View File

@@ -0,0 +1,6 @@
package ru.n08i40k.polytechnic.next.network.data.schedule
import kotlinx.serialization.Serializable
@Serializable
data class ScheduleGetRequestData(val name: String)

View File

@@ -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>
)

View File

@@ -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
)

View File

@@ -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()
}
}

View File

@@ -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()
}
}
}

View File

@@ -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)
}
}
})
}

View File

@@ -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
)
}
}
})
}

View File

@@ -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")
)
}

View File

@@ -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
)
}
}

View File

@@ -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))
}
}
}
}
}

View File

@@ -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))
}
}
}
}
}

View File

@@ -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))
}
}
}
}
}

View File

@@ -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 }
}
}
}
}
}
}

View File

@@ -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)
}
}
}
}
}

View File

@@ -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
)
}
}
}
}
}

View File

@@ -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
)
}
}

View File

@@ -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
)
}

View File

@@ -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)
}
}
}
}
}

View File

@@ -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)

View File

@@ -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)
}
}
}
}
}

View 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)

View 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
)
}

View File

@@ -0,0 +1,5 @@
package ru.n08i40k.polytechnic.next.ui.theme
import androidx.compose.material3.Typography
val AppTypography = Typography()

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,3 @@
package ru.n08i40k.polytechnic.next.utils
data class ErrorMessage(val message: String)

View 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;
}

View 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>

View 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>

View 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>

View 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-----

View File

@@ -0,0 +1 @@
unqualifiedResLocale=ru-RU

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.PolytechnicNext" parent="android:Theme.Material.NoActionBar" />
</resources>

View 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>

View 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>