mirror of
https://github.com/StepanovPlaton/SSAU_Schedule.git
synced 2026-04-03 12:20:39 +04:00
09-09 Auth page complete
This commit is contained in:
2
.idea/deploymentTargetSelector.xml
generated
2
.idea/deploymentTargetSelector.xml
generated
@@ -4,7 +4,7 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2024-09-08T10:52:57.628174800Z">
|
||||
<DropdownSelection timestamp="2024-09-09T18:25:06.130054Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\StepanovPlaton\.android\avd\Medium_Phone_API_30.avd" />
|
||||
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -3,6 +3,7 @@ import com.android.build.api.variant.BuildConfigField
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
}
|
||||
|
||||
@@ -35,8 +36,21 @@ android {
|
||||
androidComponents {
|
||||
onVariants {
|
||||
it.buildConfigFields.putAll(mapOf(
|
||||
Pair("BASE_URL", BuildConfigField("String", "\"https://lk.ssau.ru/\"", null)),
|
||||
Pair("SIGN_IN_URL", BuildConfigField("String", "\"account/login\"", null))
|
||||
Pair("BASE_URL",
|
||||
BuildConfigField("String",
|
||||
"\"https://lk.ssau.ru/\"", null)),
|
||||
Pair("SIGN_IN_URL",
|
||||
BuildConfigField("String",
|
||||
"\"account/login\"", null)),
|
||||
Pair("USER_DETAILS_URL",
|
||||
BuildConfigField("String",
|
||||
"\"api/proxy/current-user-details\"", null)),
|
||||
Pair("USER_GROUPS_URL",
|
||||
BuildConfigField("String",
|
||||
"\"api/proxy/personal/groups\"", null)),
|
||||
Pair("YEARS_URL",
|
||||
BuildConfigField("String",
|
||||
"\"api/proxy/dictionaries?slug=unified_years\"", null))
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -62,7 +76,6 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
@@ -82,4 +95,5 @@ dependencies {
|
||||
|
||||
implementation(libs.squareup.okhttp)
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
}
|
||||
@@ -25,5 +25,11 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.SSAU_Schedule"
|
||||
android:screenOrientation="portrait">
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.ssau_schedule
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
@@ -8,10 +9,9 @@ import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -32,40 +32,58 @@ import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Snackbar
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import com.example.ssau_schedule.ui.theme.SSAU_ScheduleTheme
|
||||
import kotlinx.coroutines.delay
|
||||
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.BiasAlignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.example.ssau_schedule.api.ApiErrorMessage
|
||||
import com.example.ssau_schedule.api.AuthErrorMessage
|
||||
import com.example.ssau_schedule.api.AuthorizationAPI
|
||||
import com.example.ssau_schedule.api.Http
|
||||
import com.example.ssau_schedule.api.GeneralApi
|
||||
import com.example.ssau_schedule.data.store.AuthStore
|
||||
import com.example.ssau_schedule.data.store.GeneralStore
|
||||
import com.example.ssau_schedule.data.store.Group
|
||||
import com.example.ssau_schedule.data.store.User
|
||||
import com.example.ssau_schedule.data.store.Year
|
||||
import com.example.ssau_schedule.ui.theme.ApplicationColors
|
||||
import com.example.ssau_schedule.ui.theme.SSAU_ScheduleTheme
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import kotlin.math.min
|
||||
|
||||
class AuthActivity : ComponentActivity() {
|
||||
|
||||
private val http = Http()
|
||||
private var auth: AuthorizationAPI? = null
|
||||
private var authApi: AuthorizationAPI? = null
|
||||
private var userApi: GeneralApi? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
auth = AuthorizationAPI.getInstance(http, applicationContext)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
setContent {
|
||||
@@ -77,129 +95,256 @@ class AuthActivity : ComponentActivity() {
|
||||
|
||||
@Composable
|
||||
fun AuthPage() {
|
||||
var loginOpen by remember { mutableStateOf(false) }
|
||||
val authScope = rememberCoroutineScope()
|
||||
authApi = remember { AuthorizationAPI(http, applicationContext, authScope) }
|
||||
userApi = remember { GeneralApi(http, applicationContext, authScope) }
|
||||
|
||||
var user by remember { mutableStateOf<User?>(null) }
|
||||
var group by remember { mutableStateOf<Group?>(null) }
|
||||
var year by remember { mutableStateOf<Year?>(null) }
|
||||
|
||||
|
||||
var needAuth by remember { mutableStateOf(false) }
|
||||
var entered by remember { mutableStateOf(false) }
|
||||
|
||||
val keyboardOpen by Utils.keyboardState()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val logoHeight by animateFloatAsState(
|
||||
if (keyboardOpen) 0f else min(LocalConfiguration.current.screenWidthDp, 500) / 101f * 48f,
|
||||
if (keyboardOpen) 0f else min(
|
||||
LocalConfiguration.current.screenWidthDp,
|
||||
500
|
||||
) / 101f * 48f,
|
||||
label = "alpha",
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
LaunchedEffect(false) {
|
||||
delay(1000)
|
||||
loginOpen = true
|
||||
LaunchedEffect(user, group, year) {
|
||||
if(user != null && group != null && year != null) {
|
||||
delay(1500)
|
||||
startActivity(Intent(applicationContext, MainActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
Box(Modifier.background(MaterialTheme.colorScheme.primary)
|
||||
.fillMaxSize().statusBarsPadding().navigationBarsPadding().imePadding(),
|
||||
LaunchedEffect(entered) {
|
||||
delay(1000)
|
||||
AuthStore.getAuthToken(applicationContext, authScope) { authToken ->
|
||||
if(authToken != null) {
|
||||
userApi?.getUserDetails(authToken, { u -> user = u }, { needAuth = true })
|
||||
userApi?.getUserGroups(authToken,
|
||||
{ groups ->
|
||||
GeneralStore.getCurrentGroup(applicationContext, authScope) { g ->
|
||||
if(g != null && groups.contains(g)) group = g
|
||||
else {
|
||||
GeneralStore.setCurrentGroup(groups[0],
|
||||
applicationContext, authScope)
|
||||
group = groups[0]
|
||||
}
|
||||
}
|
||||
}, { error ->
|
||||
if(error != ApiErrorMessage.USER_NOT_AUTHORIZED) {
|
||||
authScope.launch {
|
||||
val message = error.getMessage(applicationContext)
|
||||
if(message != null) snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
} else needAuth = true
|
||||
})
|
||||
GeneralStore.getCurrentYear(applicationContext, authScope) { y ->
|
||||
if(y != null && y.hasDate(Date())) year = y
|
||||
else {
|
||||
userApi?.getYears(authToken,
|
||||
{ rawYears ->
|
||||
val currentRawYear = rawYears.find { y -> y.isCurrent }
|
||||
if(currentRawYear != null) {
|
||||
year = currentRawYear.toYear()
|
||||
GeneralStore.setCurrentYear(year!!,
|
||||
applicationContext, authScope)
|
||||
} else {
|
||||
authScope.launch {
|
||||
val message = ApiErrorMessage.FAILED_GET_YEARS
|
||||
.getMessage(applicationContext)
|
||||
if(message != null)
|
||||
snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
}
|
||||
}, { error ->
|
||||
if(error != ApiErrorMessage.USER_NOT_AUTHORIZED) {
|
||||
authScope.launch {
|
||||
val message = error.getMessage(applicationContext)
|
||||
if(message != null)
|
||||
snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
} else needAuth = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
else needAuth = true
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState) {
|
||||
Snackbar(
|
||||
snackbarData = it,
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Box(
|
||||
Modifier
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding()
|
||||
.imePadding(),
|
||||
contentAlignment = BiasAlignment(0f, -0.25f),
|
||||
) {
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(Modifier.widthIn(0.dp, 500.dp)
|
||||
.height(logoHeight.dp).padding(20.dp, 0.dp)
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.widthIn(0.dp, 500.dp)
|
||||
.height(logoHeight.dp)
|
||||
.padding(20.dp, 0.dp)) {
|
||||
Image(painterResource(R.drawable.ssau_logo_01),
|
||||
contentDescription = stringResource(R.string.samara_university),
|
||||
modifier = Modifier.fillMaxSize().padding(10.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(10.dp),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
alignment = Alignment.TopCenter)
|
||||
}
|
||||
Box(Modifier.padding(20.dp, 0.dp).widthIn(0.dp, 400.dp)) {
|
||||
Card(Modifier.fillMaxWidth().animateContentSize(animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = 50f
|
||||
))
|
||||
.height(if(loginOpen) 280.dp else 0.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp)
|
||||
) {
|
||||
AuthForm()
|
||||
Box(
|
||||
Modifier
|
||||
.padding(20.dp, 0.dp)
|
||||
.widthIn(0.dp, 400.dp)) {
|
||||
Column {
|
||||
WelcomeMessage(user, group, year)
|
||||
AuthForm(open = needAuth) {
|
||||
needAuth = false
|
||||
entered = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AuthForm() {
|
||||
val authScope = rememberCoroutineScope()
|
||||
fun AuthForm(open: Boolean, callback: () -> Unit) {
|
||||
var login by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var error by remember { mutableStateOf<AuthErrorMessage?>(null) }
|
||||
|
||||
Column(Modifier.fillMaxWidth().padding(30.dp, 20.dp),
|
||||
val height by animateDpAsState(if (open) 290.dp else 0.dp, label = "Auth form height",
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
)
|
||||
|
||||
Card(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(height)
|
||||
.padding(0.dp, 10.dp)
|
||||
.shadow(10.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(30.dp, 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.sign_in),
|
||||
Text(stringResource(R.string.sign_in),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.displaySmall
|
||||
)
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
style = MaterialTheme.typography.displaySmall)
|
||||
OutlinedTextField(modifier = Modifier.fillMaxWidth(),
|
||||
value = login,
|
||||
onValueChange = { login = it; error = null },
|
||||
label = { Text(stringResource(R.string.login)) },
|
||||
placeholder =
|
||||
{ Text(stringResource(R.string.enter_your_login)) },
|
||||
)
|
||||
placeholder = { Text(stringResource(R.string.enter_your_login)) })
|
||||
Spacer(Modifier.height(2.dp))
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
OutlinedTextField(modifier = Modifier.fillMaxWidth(),
|
||||
value = password,
|
||||
onValueChange = { password = it; error = null },
|
||||
label = { Text(stringResource(R.string.password)) },
|
||||
placeholder =
|
||||
{ Text(stringResource(R.string.enter_your_password)) },
|
||||
)
|
||||
placeholder = { Text(stringResource(R.string.enter_your_password)) })
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Box(Modifier.fillMaxWidth().height(14.dp)) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(14.dp)) {
|
||||
this@Column.AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
visible = error !== null,
|
||||
enter = fadeIn(animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessVeryLow
|
||||
)),
|
||||
exit = fadeOut(animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessVeryLow
|
||||
))
|
||||
visible = error !== null
|
||||
) {
|
||||
Text(error?.getMessage(applicationContext) ?: "",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
FilledTonalButton(onClick = {
|
||||
if(login.length < 5) error = AuthErrorMessage.LOGIN_IS_TOO_SHORT
|
||||
else if(password.length < 5) error = AuthErrorMessage.PASSWORD_IS_TOO_SHORT
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
if (login.length < 5)
|
||||
error = AuthErrorMessage.LOGIN_IS_TOO_SHORT
|
||||
else if (password.length < 5)
|
||||
error = AuthErrorMessage.PASSWORD_IS_TOO_SHORT
|
||||
else {
|
||||
auth?.signIn(login, password, authScope,
|
||||
{ startActivity(Intent(applicationContext, AuthActivity::class.java)) },
|
||||
{ _, _ ->
|
||||
error = AuthErrorMessage.INCORRECT_LOGIN_OR_PASSWORD
|
||||
}
|
||||
authApi?.signIn(login, password,
|
||||
{ callback() },
|
||||
{ error = AuthErrorMessage.INCORRECT_LOGIN_OR_PASSWORD }
|
||||
)
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(50),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary)
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Text(stringResource(R.string.sign_in))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
@Composable
|
||||
fun WelcomeMessage(user: User?, group: Group?, year: Year?) {
|
||||
val currentDate = remember { SimpleDateFormat("d MMMM").format(Date()) }
|
||||
val currentYear = remember { Calendar.getInstance().get(Calendar.YEAR); }
|
||||
Column(Modifier.fillMaxWidth().animateContentSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if(user !== null && group != null && year != null) {
|
||||
Text("Здравствуйте ${user.name}!",
|
||||
color = ApplicationColors.White,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center)
|
||||
Text("Расписание для группы ${group.name}",
|
||||
color = ApplicationColors.White,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
textAlign = TextAlign.Center)
|
||||
Text("$currentDate, ${year.getWeekOfDate(Date())} "+
|
||||
"учебная неделя, ${currentYear}-${currentYear+1} учебный год",
|
||||
color = ApplicationColors.White,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
50
app/src/main/java/com/example/ssau_schedule/MainActivity.kt
Normal file
50
app/src/main/java/com/example/ssau_schedule/MainActivity.kt
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.example.ssau_schedule
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.example.ssau_schedule.api.Http
|
||||
import com.example.ssau_schedule.ui.theme.SSAU_ScheduleTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val http = Http()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
setContent {
|
||||
SSAU_ScheduleTheme {
|
||||
MainPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainPage() {
|
||||
Box(
|
||||
Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding()
|
||||
.imePadding(),
|
||||
) {
|
||||
Text("main page")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,14 @@
|
||||
package com.example.ssau_schedule.api
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import com.example.ssau_schedule.BuildConfig
|
||||
import com.example.ssau_schedule.R
|
||||
import com.example.ssau_schedule.data.source.AuthStoreKeys
|
||||
import com.example.ssau_schedule.data.source.authStore
|
||||
import com.example.ssau_schedule.data.store.AuthStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import okio.IOException
|
||||
import org.json.JSONArray
|
||||
|
||||
enum class AuthErrorMessage(private val resource: Int) {
|
||||
@@ -25,63 +20,48 @@ enum class AuthErrorMessage(private val resource: Int) {
|
||||
context.getString(resource)
|
||||
}
|
||||
|
||||
class AuthorizationAPI(private var http: Http, private var context: Context) {
|
||||
companion object {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Volatile
|
||||
private var INSTANCE: AuthorizationAPI? = null
|
||||
fun getInstance(http: Http, context: Context) =
|
||||
INSTANCE ?: synchronized(this) {
|
||||
INSTANCE ?: AuthorizationAPI(http, context).also {
|
||||
INSTANCE = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AuthorizationAPI(
|
||||
private var http: Http,
|
||||
private var context: Context,
|
||||
private var scope: CoroutineScope
|
||||
) {
|
||||
private val responseHasAuthToken =
|
||||
{ response: Response? -> response?.headers?.toMap()?.containsKey("set-cookie") == true }
|
||||
|
||||
private fun safeAuthToken(response: Response,
|
||||
authScope: CoroutineScope,
|
||||
callback: HttpResponseCallback) {
|
||||
authScope.launch {
|
||||
context.authStore.edit { authStore ->
|
||||
authStore[AuthStoreKeys.AUTH_TOKEN] =
|
||||
response.headers("set-cookie").joinToString(", ")
|
||||
}.run {
|
||||
callback(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun signIn(login: String, password: String, authScope: CoroutineScope,
|
||||
callback: HttpResponseCallback,
|
||||
exceptionCallback: HttpExceptionCallback? = null) {
|
||||
fun signIn(
|
||||
login: String, password: String,
|
||||
callback: (token: String) -> Unit,
|
||||
exceptionCallback: ((error: HttpRequestException) -> Unit)? = null
|
||||
) {
|
||||
http.request(
|
||||
Method.POST,
|
||||
BuildConfig.SIGN_IN_URL,
|
||||
JSONArray(arrayOf(mapOf(
|
||||
JSONArray(
|
||||
arrayOf(
|
||||
mapOf(
|
||||
Pair("login", login),
|
||||
Pair("password", password)
|
||||
))).toString().toRequestBody("application/json".toMediaType()),
|
||||
)
|
||||
)
|
||||
).toString().toRequestBody("application/json".toMediaType()),
|
||||
mapOf(
|
||||
Pair("Next-Action", "b395d17834d8b7df06372cbf1f241170a272d540")
|
||||
).toHeaders(),
|
||||
fun(response) {
|
||||
if(responseHasAuthToken(response))
|
||||
safeAuthToken(response, authScope, callback)
|
||||
else if(exceptionCallback != null)
|
||||
exceptionCallback(IOException("Authorization token not found"), response)
|
||||
if (responseHasAuthToken(response)) {
|
||||
val token = response.headers("set-cookie").joinToString(", ")
|
||||
AuthStore.setAuthToken(token, context, scope) { callback(token) }
|
||||
} else
|
||||
exceptionCallback?.invoke(
|
||||
HttpRequestException("Authorization token not found"))
|
||||
},
|
||||
fun(error, response): Boolean {
|
||||
if (responseHasAuthToken(response)) return true
|
||||
if(exceptionCallback !== null) exceptionCallback(error, response)
|
||||
exceptionCallback?.invoke(error)
|
||||
return false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
189
app/src/main/java/com/example/ssau_schedule/api/General.kt
Normal file
189
app/src/main/java/com/example/ssau_schedule/api/General.kt
Normal file
@@ -0,0 +1,189 @@
|
||||
package com.example.ssau_schedule.api
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.example.ssau_schedule.BuildConfig
|
||||
import com.example.ssau_schedule.R
|
||||
import com.example.ssau_schedule.data.store.AuthStore
|
||||
import com.example.ssau_schedule.data.store.Group
|
||||
import com.example.ssau_schedule.data.store.RawYear
|
||||
import com.example.ssau_schedule.data.store.User
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
|
||||
|
||||
enum class ApiErrorMessage(private val resource: Int?) {
|
||||
FAILED_GET_USER_DETAILS(R.string.failder_get_user_details),
|
||||
NOT_MEMBER_OF_ANY_GROUP(R.string.not_member_of_any_group),
|
||||
FAILED_GET_USER_GROUPS(R.string.failed_get_user_groups),
|
||||
FAILED_GET_YEARS(R.string.failed_get_years),
|
||||
|
||||
USER_NOT_AUTHORIZED(null);
|
||||
|
||||
fun getMessage(context: Context) =
|
||||
if(resource != null) context.getString(resource) else null
|
||||
}
|
||||
|
||||
class GeneralApi(
|
||||
private var http: Http,
|
||||
private var context: Context,
|
||||
private var scope: CoroutineScope
|
||||
) {
|
||||
fun getUserDetails(
|
||||
token: String,
|
||||
callback: (user: User) -> Unit,
|
||||
exceptionCallback: ((error: ApiErrorMessage) -> Unit)
|
||||
) {
|
||||
http.request(
|
||||
Method.GET,
|
||||
BuildConfig.USER_DETAILS_URL,
|
||||
mapOf(
|
||||
Pair("Cookie", token)
|
||||
).toHeaders(),
|
||||
{ response ->
|
||||
try {
|
||||
if(response.body != null) {
|
||||
val serializer = Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
callback(serializer
|
||||
.decodeFromString<User>(response.body!!.string()))
|
||||
}
|
||||
else exceptionCallback(ApiErrorMessage.FAILED_GET_USER_DETAILS)
|
||||
}
|
||||
catch(e: SerializationException) {
|
||||
Log.e("Groups Deserialization exception", e.message ?: "")
|
||||
exceptionCallback(ApiErrorMessage.FAILED_GET_USER_DETAILS)
|
||||
}
|
||||
catch (e: IllegalArgumentException) {
|
||||
Log.e("Groups argument exception", e.message ?: "")
|
||||
exceptionCallback(ApiErrorMessage.FAILED_GET_USER_DETAILS)
|
||||
}
|
||||
},
|
||||
fun(_, r): Boolean {
|
||||
if(r?.code == 401) exceptionCallback(ApiErrorMessage.USER_NOT_AUTHORIZED)
|
||||
else exceptionCallback(ApiErrorMessage.FAILED_GET_USER_DETAILS)
|
||||
return false
|
||||
}
|
||||
)
|
||||
}
|
||||
fun getUserDetails(
|
||||
callback: (user: User) -> Unit,
|
||||
exceptionCallback: ((error: ApiErrorMessage) -> Unit)
|
||||
) {
|
||||
AuthStore.getAuthToken(context, scope) { authToken ->
|
||||
if(authToken != null)
|
||||
getUserDetails(authToken, callback, exceptionCallback)
|
||||
else exceptionCallback(ApiErrorMessage.USER_NOT_AUTHORIZED)
|
||||
}
|
||||
}
|
||||
|
||||
fun getUserGroups(
|
||||
token: String,
|
||||
callback: (groups: List<Group>) -> Unit,
|
||||
exceptionCallback: (error: ApiErrorMessage) -> Unit
|
||||
) {
|
||||
http.request(
|
||||
Method.GET,
|
||||
BuildConfig.USER_GROUPS_URL,
|
||||
mapOf(
|
||||
Pair("Cookie", token)
|
||||
).toHeaders(),
|
||||
{ response ->
|
||||
try {
|
||||
if(response.body != null) {
|
||||
val serializer = Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
val groups = serializer
|
||||
.decodeFromString<List<Group>>(response.body!!.string())
|
||||
if(groups.isNotEmpty()) callback(groups)
|
||||
else { exceptionCallback(ApiErrorMessage.NOT_MEMBER_OF_ANY_GROUP) }
|
||||
}
|
||||
else exceptionCallback(ApiErrorMessage.FAILED_GET_USER_GROUPS)
|
||||
}
|
||||
catch(e: SerializationException) {
|
||||
Log.e("Groups Deserialization exception", e.message ?: "")
|
||||
exceptionCallback(ApiErrorMessage.FAILED_GET_USER_GROUPS)
|
||||
}
|
||||
catch (e: IllegalArgumentException) {
|
||||
Log.e("Groups argument exception", e.message ?: "")
|
||||
exceptionCallback(ApiErrorMessage.FAILED_GET_USER_GROUPS)
|
||||
}
|
||||
},
|
||||
fun(_, r): Boolean {
|
||||
if(r?.code == 401) exceptionCallback(ApiErrorMessage.USER_NOT_AUTHORIZED)
|
||||
else exceptionCallback(ApiErrorMessage.FAILED_GET_USER_GROUPS)
|
||||
return false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun getUserGroups(
|
||||
callback: (groups: List<Group>) -> Unit,
|
||||
exceptionCallback: (error: ApiErrorMessage) -> Unit
|
||||
) {
|
||||
AuthStore.getAuthToken(context, scope) { authToken ->
|
||||
if(authToken != null)
|
||||
getUserGroups(authToken, callback, exceptionCallback)
|
||||
else exceptionCallback(ApiErrorMessage.USER_NOT_AUTHORIZED)
|
||||
}
|
||||
}
|
||||
|
||||
fun getYears(
|
||||
token: String,
|
||||
callback: (rawYears: List<RawYear>) -> Unit,
|
||||
exceptionCallback: (error: ApiErrorMessage) -> Unit
|
||||
) {
|
||||
http.request(
|
||||
Method.GET,
|
||||
BuildConfig.YEARS_URL,
|
||||
mapOf(
|
||||
Pair("Cookie", token)
|
||||
).toHeaders(),
|
||||
{ response ->
|
||||
try {
|
||||
if(response.body != null) {
|
||||
val serializer = Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
val rawYears = serializer
|
||||
.decodeFromString<List<RawYear>>(response.body!!.string())
|
||||
if(rawYears.isNotEmpty()) callback(rawYears)
|
||||
else { exceptionCallback(ApiErrorMessage.FAILED_GET_YEARS) }
|
||||
}
|
||||
else exceptionCallback(ApiErrorMessage.FAILED_GET_YEARS)
|
||||
}
|
||||
catch(e: SerializationException) {
|
||||
Log.e("Groups Deserialization exception", e.message ?: "")
|
||||
exceptionCallback(ApiErrorMessage.FAILED_GET_YEARS)
|
||||
}
|
||||
catch (e: IllegalArgumentException) {
|
||||
Log.e("Groups argument exception", e.message ?: "")
|
||||
exceptionCallback(ApiErrorMessage.FAILED_GET_YEARS)
|
||||
}
|
||||
},
|
||||
fun(_, r): Boolean {
|
||||
if(r?.code == 401) exceptionCallback(ApiErrorMessage.USER_NOT_AUTHORIZED)
|
||||
else exceptionCallback(ApiErrorMessage.FAILED_GET_YEARS)
|
||||
return false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun getYears(
|
||||
callback: (rawYears: List<RawYear>) -> Unit,
|
||||
exceptionCallback: (error: ApiErrorMessage) -> Unit
|
||||
) {
|
||||
AuthStore.getAuthToken(context, scope) { authToken ->
|
||||
if(authToken != null)
|
||||
getYears(authToken, callback, exceptionCallback)
|
||||
else exceptionCallback(ApiErrorMessage.USER_NOT_AUTHORIZED)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import okio.IOException
|
||||
|
||||
enum class Method {
|
||||
GET,
|
||||
@@ -18,9 +18,14 @@ enum class Method {
|
||||
DELETE
|
||||
}
|
||||
|
||||
typealias HttpResponseCallback = (response: Response) -> Unit
|
||||
typealias HttpExceptionVerifyCallback = (exception: IOException, response: Response?) -> Boolean
|
||||
typealias HttpExceptionCallback = (exception: IOException, response: Response?) -> Unit
|
||||
typealias HttpRequestException = IOException
|
||||
|
||||
typealias HttpResponseCallback =
|
||||
(response: Response) -> Unit
|
||||
typealias HttpExceptionVerifyCallback =
|
||||
(exception: HttpRequestException, response: Response?) -> Boolean
|
||||
typealias HttpExceptionCallback =
|
||||
(exception: HttpRequestException, response: Response?) -> Unit
|
||||
|
||||
class Http {
|
||||
val http = OkHttpClient()
|
||||
@@ -33,22 +38,38 @@ class Http {
|
||||
callback: HttpResponseCallback,
|
||||
exceptionCallback: HttpExceptionVerifyCallback? = null
|
||||
) {
|
||||
val request = Request.Builder().url(BuildConfig.BASE_URL+url).
|
||||
method(method.toString(), body)
|
||||
val request =
|
||||
Request.Builder().url(BuildConfig.BASE_URL + url).method(method.toString(), body)
|
||||
if (headers !== null) request.headers(headers)
|
||||
http.newCall(request.build()).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
override fun onFailure(call: Call, e: HttpRequestException) {
|
||||
Log.e("Http request failed", e.toString())
|
||||
if(exceptionCallback !== null) exceptionCallback(e, null)
|
||||
exceptionCallback?.invoke(e, null)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
var runCallback = false
|
||||
if (!response.isSuccessful && exceptionCallback !== null)
|
||||
runCallback = exceptionCallback(
|
||||
IOException("Http response is not successful"), response)
|
||||
HttpRequestException("Http response is not successful"), response
|
||||
)
|
||||
if (runCallback || response.isSuccessful) callback(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun request(
|
||||
method: Method,
|
||||
url: String,
|
||||
headers: Headers? = null,
|
||||
callback: HttpResponseCallback,
|
||||
exceptionCallback: HttpExceptionVerifyCallback? = null
|
||||
) = request(method, url, null, headers, callback, exceptionCallback)
|
||||
|
||||
fun request(
|
||||
method: Method,
|
||||
url: String,
|
||||
callback: HttpResponseCallback,
|
||||
exceptionCallback: HttpExceptionVerifyCallback? = null
|
||||
) = request(method, url, null, null, callback, exceptionCallback)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.example.ssau_schedule.data.source
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
|
||||
val Context.authStore by preferencesDataStore(name = "auth")
|
||||
|
||||
object AuthStoreKeys {
|
||||
val AUTH_TOKEN = stringPreferencesKey("auth_token")
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.example.ssau_schedule.data.store
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
val Context.authStore by preferencesDataStore(name = "auth")
|
||||
|
||||
class AuthStore {
|
||||
class Keys {
|
||||
companion object {
|
||||
val AUTH_TOKEN = stringPreferencesKey("auth_token")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun setAuthToken(
|
||||
token: String,
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
callback: (() -> Unit)? = null
|
||||
) {
|
||||
scope.launch {
|
||||
context.authStore.edit { authStore ->
|
||||
authStore[Keys.AUTH_TOKEN] = token
|
||||
}.run { callback?.invoke() }
|
||||
}
|
||||
}
|
||||
|
||||
fun getAuthToken(
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
callback: (token: String?) -> Unit
|
||||
) {
|
||||
scope.launch {
|
||||
val authTokenFlow = context.authStore.data
|
||||
.map { authStore -> authStore[Keys.AUTH_TOKEN] }
|
||||
callback(authTokenFlow.first())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.example.ssau_schedule.data.store
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
|
||||
val Context.generalStore by preferencesDataStore(name = "user")
|
||||
|
||||
@Serializable
|
||||
data class User(val name: String)
|
||||
|
||||
@Serializable
|
||||
data class Group(val id: Int, val name: String)
|
||||
|
||||
data class Year(
|
||||
val id: Int,
|
||||
val startDate: Date,
|
||||
val endDate: Date,
|
||||
) {
|
||||
companion object {
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
val DateFormat = SimpleDateFormat("yyyy-MM-dd")
|
||||
|
||||
fun parseDate(dateString: String): Date = DateFormat.parse(dateString)!!
|
||||
fun dateFormat(date: Date): String = DateFormat.format(date)
|
||||
}
|
||||
|
||||
fun hasDate(date: Date) = date.after(startDate) && endDate.after(date)
|
||||
fun getWeekOfDate(date: Date): Int {
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.minimalDaysInFirstWeek = 6
|
||||
calendar.firstDayOfWeek = Calendar.MONDAY
|
||||
calendar.time = startDate
|
||||
val firstWeek = calendar.get(Calendar.WEEK_OF_YEAR)
|
||||
calendar.time = date
|
||||
return (calendar.get(Calendar.WEEK_OF_YEAR) - firstWeek)+1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class RawYear(
|
||||
val id: Int,
|
||||
val startDate: String,
|
||||
val endDate: String,
|
||||
val isCurrent: Boolean,
|
||||
) {
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
fun toYear() = Year(
|
||||
id = id,
|
||||
startDate = Year.parseDate(startDate),
|
||||
endDate = Year.parseDate(endDate),
|
||||
)
|
||||
}
|
||||
|
||||
class GeneralStore {
|
||||
class Keys {
|
||||
companion object {
|
||||
val CURRENT_GROUP_ID = intPreferencesKey("group_id")
|
||||
val CURRENT_GROUP_NAME = stringPreferencesKey("group_name")
|
||||
|
||||
val CURRENT_YEAR_ID = intPreferencesKey("year_id")
|
||||
val CURRENT_YEAR_START = stringPreferencesKey("year_start")
|
||||
val CURRENT_YEAR_END = stringPreferencesKey("year_end")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun setCurrentGroup(
|
||||
group: Group,
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
callback: (() -> Unit)? = null
|
||||
) {
|
||||
scope.launch {
|
||||
context.generalStore.edit { generalStore ->
|
||||
generalStore[Keys.CURRENT_GROUP_ID] = group.id
|
||||
generalStore[Keys.CURRENT_GROUP_NAME] = group.name
|
||||
}.run { callback?.invoke() }
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentGroup(
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
callback: (group: Group?) -> Unit
|
||||
) {
|
||||
scope.launch {
|
||||
val currentGroupId = context.generalStore.data
|
||||
.map { generalStore -> generalStore[Keys.CURRENT_GROUP_ID] }.first()
|
||||
val currentGroupName = context.generalStore.data
|
||||
.map { generalStore -> generalStore[Keys.CURRENT_GROUP_NAME] }.first()
|
||||
callback(
|
||||
if(currentGroupId != null && currentGroupName != null)
|
||||
Group(id = currentGroupId,
|
||||
name = currentGroupName)
|
||||
else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setCurrentYear(
|
||||
year: Year,
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
callback: (() -> Unit)? = null
|
||||
) {
|
||||
scope.launch {
|
||||
context.generalStore.edit { generalStore ->
|
||||
generalStore[Keys.CURRENT_YEAR_ID] = year.id
|
||||
generalStore[Keys.CURRENT_YEAR_START] = Year.dateFormat(year.startDate)
|
||||
generalStore[Keys.CURRENT_YEAR_END] = Year.dateFormat(year.endDate)
|
||||
}.run { callback?.invoke() }
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentYear(
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
callback: (year: Year?) -> Unit
|
||||
) {
|
||||
scope.launch {
|
||||
val currentYearId = context.generalStore.data
|
||||
.map { generalStore -> generalStore[Keys.CURRENT_YEAR_ID] }.first()
|
||||
val currentYearStartDate = context.generalStore.data
|
||||
.map { generalStore -> generalStore[Keys.CURRENT_YEAR_START]
|
||||
}.first()
|
||||
val currentYearEndDate = context.generalStore.data
|
||||
.map { generalStore -> generalStore[Keys.CURRENT_YEAR_END]
|
||||
}.first()
|
||||
callback(
|
||||
if(currentYearId != null &&
|
||||
currentYearStartDate != null &&
|
||||
currentYearEndDate != null)
|
||||
Year(id = currentYearId,
|
||||
startDate = Year.parseDate(currentYearStartDate),
|
||||
endDate = Year.parseDate(currentYearEndDate))
|
||||
else null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package com.example.ssau_schedule.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
class ApplicationColors {
|
||||
companion object {
|
||||
val Primary01 = Color(0xFF0D47A1)
|
||||
val Primary02 = Color(0xFF134BC5)
|
||||
val Primary03 = Color(0xFF1D5DEB)
|
||||
@@ -15,4 +17,9 @@ val Gray01 = Color(0xFF2C2C2C)
|
||||
val Gray02 = Color(0xFF383838)
|
||||
val Gray03 = Color(0xFF66727F)
|
||||
|
||||
val Red = Color(0xFFEE3F58)
|
||||
val Red01 = Color(0xFFEE3F58)
|
||||
val Red02 = Color(0xFF7E212E)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,21 +12,23 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Primary01,
|
||||
secondary = Primary03,
|
||||
tertiary = Primary04,
|
||||
background = Gray01,
|
||||
surface = Gray02,
|
||||
error = Red,
|
||||
primary = ApplicationColors.Primary01,
|
||||
secondary = ApplicationColors.Primary03,
|
||||
tertiary = ApplicationColors.Primary04,
|
||||
background = ApplicationColors.Gray01,
|
||||
surface = ApplicationColors.Gray02,
|
||||
error = ApplicationColors.Red01,
|
||||
errorContainer = ApplicationColors.Red02
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Primary01,
|
||||
secondary = Primary03,
|
||||
tertiary = Primary04,
|
||||
background = White,
|
||||
surface = Primary06,
|
||||
error = Red,
|
||||
primary = ApplicationColors.Primary01,
|
||||
secondary = ApplicationColors.Primary03,
|
||||
tertiary = ApplicationColors.Primary04,
|
||||
background = ApplicationColors.White,
|
||||
surface = ApplicationColors.Primary06,
|
||||
error = ApplicationColors.Red01,
|
||||
errorContainer = ApplicationColors.Red02
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
|
||||
@@ -10,4 +10,8 @@
|
||||
<string name="incorrect_login_or_password">Incorrect login or password</string>
|
||||
<string name="login_is_too_short">Login is too short</string>
|
||||
<string name="password_is_too_short">Password is too short</string>
|
||||
<string name="not_member_of_any_group">The user is not a member of any study group. You can\'t get a schedule for him</string>
|
||||
<string name="failed_get_user_groups">Не получилось получить список групп, в которых состоит пользователь. Расписание нельзя получить без учебной группы</string>
|
||||
<string name="failed_get_years">It was not possible to obtain a list of academic years. The timetable cannot be obtained without the academic year</string>
|
||||
<string name="failder_get_user_details">Failed to retrieve user information. The schedule cannot be obtained without user data</string>
|
||||
</resources>
|
||||
@@ -9,4 +9,8 @@
|
||||
<string name="incorrect_login_or_password">Неверный логин или пароль</string>
|
||||
<string name="login_is_too_short">Логин слишком короткий</string>
|
||||
<string name="password_is_too_short">Пароль слишком короткий</string>
|
||||
<string name="not_member_of_any_group">Пользователь не состоит ни в одной учебной группе. Для него нельзя получить расписание</string>
|
||||
<string name="failed_get_user_groups">Не получилось получить список групп, в которых состоит пользователь. Расписание нельзя получить без учебной группы</string>
|
||||
<string name="failed_get_years">Не получилось получить список учебных годов. Расписание нельзя получить без учебного года</string>
|
||||
<string name="failder_get_user_details">Не получилось получить информацию о пользователе. Расписание нельзя получить без пользовательских данных</string>
|
||||
</resources>
|
||||
@@ -2,5 +2,6 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.serialization) apply false
|
||||
alias(libs.plugins.compose.compiler) apply false
|
||||
}
|
||||
@@ -11,6 +11,7 @@ composeBom = "2024.09.00"
|
||||
okhttp = "4.12.0"
|
||||
datastore = "1.1.1"
|
||||
media3Common = "1.4.1"
|
||||
kotlinSerializationJson = "1.7.1"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -30,8 +31,10 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3"
|
||||
squareup-okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
androidx-datastore = { group = "androidx.datastore", name="datastore-preferences", version.ref = "datastore" }
|
||||
androidx-media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3Common" }
|
||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinSerializationJson" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
|
||||
Reference in New Issue
Block a user