mirror of
https://github.com/StepanovPlaton/SSAU_Schedule.git
synced 2026-04-03 20:30:40 +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>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<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">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\StepanovPlaton\.android\avd\Medium_Phone_API_30.avd" />
|
<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 {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
alias(libs.plugins.compose.compiler)
|
alias(libs.plugins.compose.compiler)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,8 +36,21 @@ android {
|
|||||||
androidComponents {
|
androidComponents {
|
||||||
onVariants {
|
onVariants {
|
||||||
it.buildConfigFields.putAll(mapOf(
|
it.buildConfigFields.putAll(mapOf(
|
||||||
Pair("BASE_URL", BuildConfigField("String", "\"https://lk.ssau.ru/\"", null)),
|
Pair("BASE_URL",
|
||||||
Pair("SIGN_IN_URL", BuildConfigField("String", "\"account/login\"", null))
|
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 {
|
dependencies {
|
||||||
|
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
@@ -82,4 +95,5 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.squareup.okhttp)
|
implementation(libs.squareup.okhttp)
|
||||||
implementation(libs.androidx.datastore)
|
implementation(libs.androidx.datastore)
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
}
|
}
|
||||||
@@ -25,5 +25,11 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/Theme.SSAU_Schedule"
|
||||||
|
android:screenOrientation="portrait">
|
||||||
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.ssau_schedule
|
package com.example.ssau_schedule
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
@@ -8,10 +9,9 @@ import androidx.activity.enableEdgeToEdge
|
|||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.animation.core.Spring
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.spring
|
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.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -32,40 +32,58 @@ import androidx.compose.material3.CardDefaults
|
|||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
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.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
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.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.BiasAlignment
|
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.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.view.WindowCompat
|
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.AuthErrorMessage
|
||||||
import com.example.ssau_schedule.api.AuthorizationAPI
|
import com.example.ssau_schedule.api.AuthorizationAPI
|
||||||
import com.example.ssau_schedule.api.Http
|
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
|
import kotlin.math.min
|
||||||
|
|
||||||
class AuthActivity : ComponentActivity() {
|
class AuthActivity : ComponentActivity() {
|
||||||
|
|
||||||
private val http = Http()
|
private val http = Http()
|
||||||
private var auth: AuthorizationAPI? = null
|
private var authApi: AuthorizationAPI? = null
|
||||||
|
private var userApi: GeneralApi? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
auth = AuthorizationAPI.getInstance(http, applicationContext)
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
@@ -77,129 +95,256 @@ class AuthActivity : ComponentActivity() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthPage() {
|
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 keyboardOpen by Utils.keyboardState()
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val logoHeight by animateFloatAsState(
|
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",
|
label = "alpha",
|
||||||
animationSpec = spring(
|
animationSpec = spring(
|
||||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
stiffness = Spring.StiffnessLow
|
stiffness = Spring.StiffnessLow
|
||||||
))
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(user, group, year) {
|
||||||
LaunchedEffect(false) {
|
if(user != null && group != null && year != null) {
|
||||||
delay(1000)
|
delay(1500)
|
||||||
loginOpen = true
|
startActivity(Intent(applicationContext, MainActivity::class.java))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(Modifier.background(MaterialTheme.colorScheme.primary)
|
LaunchedEffect(entered) {
|
||||||
.fillMaxSize().statusBarsPadding().navigationBarsPadding().imePadding(),
|
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),
|
contentAlignment = BiasAlignment(0f, -0.25f),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Box(Modifier.widthIn(0.dp, 500.dp)
|
Box(
|
||||||
.height(logoHeight.dp).padding(20.dp, 0.dp)
|
Modifier
|
||||||
) {
|
.widthIn(0.dp, 500.dp)
|
||||||
|
.height(logoHeight.dp)
|
||||||
|
.padding(20.dp, 0.dp)) {
|
||||||
Image(painterResource(R.drawable.ssau_logo_01),
|
Image(painterResource(R.drawable.ssau_logo_01),
|
||||||
contentDescription = stringResource(R.string.samara_university),
|
contentDescription = stringResource(R.string.samara_university),
|
||||||
modifier = Modifier.fillMaxSize().padding(10.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(10.dp),
|
||||||
contentScale = ContentScale.FillWidth,
|
contentScale = ContentScale.FillWidth,
|
||||||
alignment = Alignment.TopCenter)
|
alignment = Alignment.TopCenter)
|
||||||
}
|
}
|
||||||
Box(Modifier.padding(20.dp, 0.dp).widthIn(0.dp, 400.dp)) {
|
Box(
|
||||||
Card(Modifier.fillMaxWidth().animateContentSize(animationSpec = spring(
|
Modifier
|
||||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
.padding(20.dp, 0.dp)
|
||||||
stiffness = 50f
|
.widthIn(0.dp, 400.dp)) {
|
||||||
))
|
Column {
|
||||||
.height(if(loginOpen) 280.dp else 0.dp),
|
WelcomeMessage(user, group, year)
|
||||||
colors = CardDefaults.cardColors(
|
AuthForm(open = needAuth) {
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
needAuth = false
|
||||||
),
|
entered = true
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 10.dp)
|
|
||||||
) {
|
|
||||||
AuthForm()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthForm() {
|
fun AuthForm(open: Boolean, callback: () -> Unit) {
|
||||||
val authScope = rememberCoroutineScope()
|
|
||||||
var login by remember { mutableStateOf("") }
|
var login by remember { mutableStateOf("") }
|
||||||
var password by remember { mutableStateOf("") }
|
var password by remember { mutableStateOf("") }
|
||||||
var error by remember { mutableStateOf<AuthErrorMessage?>(null) }
|
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
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(stringResource(R.string.sign_in),
|
||||||
stringResource(R.string.sign_in),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = MaterialTheme.typography.displaySmall
|
style = MaterialTheme.typography.displaySmall)
|
||||||
)
|
OutlinedTextField(modifier = Modifier.fillMaxWidth(),
|
||||||
OutlinedTextField(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = login,
|
value = login,
|
||||||
onValueChange = { login = it; error = null },
|
onValueChange = { login = it; error = null },
|
||||||
label = { Text(stringResource(R.string.login)) },
|
label = { Text(stringResource(R.string.login)) },
|
||||||
placeholder =
|
placeholder = { Text(stringResource(R.string.enter_your_login)) })
|
||||||
{ Text(stringResource(R.string.enter_your_login)) },
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(2.dp))
|
Spacer(Modifier.height(2.dp))
|
||||||
OutlinedTextField(
|
OutlinedTextField(modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
value = password,
|
value = password,
|
||||||
onValueChange = { password = it; error = null },
|
onValueChange = { password = it; error = null },
|
||||||
label = { Text(stringResource(R.string.password)) },
|
label = { Text(stringResource(R.string.password)) },
|
||||||
placeholder =
|
placeholder = { Text(stringResource(R.string.enter_your_password)) })
|
||||||
{ Text(stringResource(R.string.enter_your_password)) },
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(2.dp))
|
Spacer(Modifier.height(2.dp))
|
||||||
Box(Modifier.fillMaxWidth().height(14.dp)) {
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(14.dp)) {
|
||||||
this@Column.AnimatedVisibility(
|
this@Column.AnimatedVisibility(
|
||||||
modifier = Modifier.align(Alignment.Center),
|
modifier = Modifier.align(Alignment.Center),
|
||||||
visible = error !== null,
|
visible = error !== null
|
||||||
enter = fadeIn(animationSpec = spring(
|
|
||||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
||||||
stiffness = Spring.StiffnessVeryLow
|
|
||||||
)),
|
|
||||||
exit = fadeOut(animationSpec = spring(
|
|
||||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
|
||||||
stiffness = Spring.StiffnessVeryLow
|
|
||||||
))
|
|
||||||
) {
|
) {
|
||||||
Text(error?.getMessage(applicationContext) ?: "",
|
Text(error?.getMessage(applicationContext) ?: "",
|
||||||
color = MaterialTheme.colorScheme.error,
|
color = MaterialTheme.colorScheme.error,
|
||||||
style = MaterialTheme.typography.labelSmall
|
style = MaterialTheme.typography.labelSmall)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
FilledTonalButton(onClick = {
|
FilledTonalButton(
|
||||||
if(login.length < 5) error = AuthErrorMessage.LOGIN_IS_TOO_SHORT
|
onClick = {
|
||||||
else if(password.length < 5) error = AuthErrorMessage.PASSWORD_IS_TOO_SHORT
|
if (login.length < 5)
|
||||||
|
error = AuthErrorMessage.LOGIN_IS_TOO_SHORT
|
||||||
|
else if (password.length < 5)
|
||||||
|
error = AuthErrorMessage.PASSWORD_IS_TOO_SHORT
|
||||||
else {
|
else {
|
||||||
auth?.signIn(login, password, authScope,
|
authApi?.signIn(login, password,
|
||||||
{ startActivity(Intent(applicationContext, AuthActivity::class.java)) },
|
{ callback() },
|
||||||
{ _, _ ->
|
{ error = AuthErrorMessage.INCORRECT_LOGIN_OR_PASSWORD }
|
||||||
error = AuthErrorMessage.INCORRECT_LOGIN_OR_PASSWORD
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
shape = RoundedCornerShape(50),
|
shape = RoundedCornerShape(50),
|
||||||
colors = ButtonDefaults.buttonColors(
|
colors = ButtonDefaults.buttonColors(
|
||||||
containerColor = MaterialTheme.colorScheme.primary)
|
containerColor = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.sign_in))
|
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
|
package com.example.ssau_schedule.api
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.datastore.preferences.core.edit
|
|
||||||
import com.example.ssau_schedule.BuildConfig
|
import com.example.ssau_schedule.BuildConfig
|
||||||
import com.example.ssau_schedule.R
|
import com.example.ssau_schedule.R
|
||||||
import com.example.ssau_schedule.data.source.AuthStoreKeys
|
import com.example.ssau_schedule.data.store.AuthStore
|
||||||
import com.example.ssau_schedule.data.source.authStore
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import okhttp3.Headers.Companion.toHeaders
|
import okhttp3.Headers.Companion.toHeaders
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.IOException
|
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
|
|
||||||
enum class AuthErrorMessage(private val resource: Int) {
|
enum class AuthErrorMessage(private val resource: Int) {
|
||||||
@@ -25,63 +20,48 @@ enum class AuthErrorMessage(private val resource: Int) {
|
|||||||
context.getString(resource)
|
context.getString(resource)
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthorizationAPI(private var http: Http, private var context: Context) {
|
class AuthorizationAPI(
|
||||||
companion object {
|
private var http: Http,
|
||||||
@SuppressLint("StaticFieldLeak")
|
private var context: Context,
|
||||||
@Volatile
|
private var scope: CoroutineScope
|
||||||
private var INSTANCE: AuthorizationAPI? = null
|
) {
|
||||||
fun getInstance(http: Http, context: Context) =
|
|
||||||
INSTANCE ?: synchronized(this) {
|
|
||||||
INSTANCE ?: AuthorizationAPI(http, context).also {
|
|
||||||
INSTANCE = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val responseHasAuthToken =
|
private val responseHasAuthToken =
|
||||||
{response: Response? -> response?.headers?.toMap()?.containsKey("set-cookie") == true}
|
{ response: Response? -> response?.headers?.toMap()?.containsKey("set-cookie") == true }
|
||||||
|
|
||||||
private fun safeAuthToken(response: Response,
|
fun signIn(
|
||||||
authScope: CoroutineScope,
|
login: String, password: String,
|
||||||
callback: HttpResponseCallback) {
|
callback: (token: String) -> Unit,
|
||||||
authScope.launch {
|
exceptionCallback: ((error: HttpRequestException) -> Unit)? = null
|
||||||
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) {
|
|
||||||
http.request(
|
http.request(
|
||||||
Method.POST,
|
Method.POST,
|
||||||
BuildConfig.SIGN_IN_URL,
|
BuildConfig.SIGN_IN_URL,
|
||||||
JSONArray(arrayOf(mapOf(
|
JSONArray(
|
||||||
|
arrayOf(
|
||||||
|
mapOf(
|
||||||
Pair("login", login),
|
Pair("login", login),
|
||||||
Pair("password", password)
|
Pair("password", password)
|
||||||
))).toString().toRequestBody("application/json".toMediaType()),
|
)
|
||||||
|
)
|
||||||
|
).toString().toRequestBody("application/json".toMediaType()),
|
||||||
mapOf(
|
mapOf(
|
||||||
Pair("Next-Action", "b395d17834d8b7df06372cbf1f241170a272d540")
|
Pair("Next-Action", "b395d17834d8b7df06372cbf1f241170a272d540")
|
||||||
).toHeaders(),
|
).toHeaders(),
|
||||||
fun(response) {
|
fun(response) {
|
||||||
if(responseHasAuthToken(response))
|
if (responseHasAuthToken(response)) {
|
||||||
safeAuthToken(response, authScope, callback)
|
val token = response.headers("set-cookie").joinToString(", ")
|
||||||
else if(exceptionCallback != null)
|
AuthStore.setAuthToken(token, context, scope) { callback(token) }
|
||||||
exceptionCallback(IOException("Authorization token not found"), response)
|
} else
|
||||||
|
exceptionCallback?.invoke(
|
||||||
|
HttpRequestException("Authorization token not found"))
|
||||||
},
|
},
|
||||||
fun (error, response): Boolean {
|
fun(error, response): Boolean {
|
||||||
if(responseHasAuthToken(response)) return true
|
if (responseHasAuthToken(response)) return true
|
||||||
if(exceptionCallback !== null) exceptionCallback(error, response)
|
exceptionCallback?.invoke(error)
|
||||||
return false
|
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.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import java.io.IOException
|
import okio.IOException
|
||||||
|
|
||||||
enum class Method {
|
enum class Method {
|
||||||
GET,
|
GET,
|
||||||
@@ -18,9 +18,14 @@ enum class Method {
|
|||||||
DELETE
|
DELETE
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias HttpResponseCallback = (response: Response) -> Unit
|
typealias HttpRequestException = IOException
|
||||||
typealias HttpExceptionVerifyCallback = (exception: IOException, response: Response?) -> Boolean
|
|
||||||
typealias HttpExceptionCallback = (exception: IOException, response: Response?) -> Unit
|
typealias HttpResponseCallback =
|
||||||
|
(response: Response) -> Unit
|
||||||
|
typealias HttpExceptionVerifyCallback =
|
||||||
|
(exception: HttpRequestException, response: Response?) -> Boolean
|
||||||
|
typealias HttpExceptionCallback =
|
||||||
|
(exception: HttpRequestException, response: Response?) -> Unit
|
||||||
|
|
||||||
class Http {
|
class Http {
|
||||||
val http = OkHttpClient()
|
val http = OkHttpClient()
|
||||||
@@ -33,22 +38,38 @@ class Http {
|
|||||||
callback: HttpResponseCallback,
|
callback: HttpResponseCallback,
|
||||||
exceptionCallback: HttpExceptionVerifyCallback? = null
|
exceptionCallback: HttpExceptionVerifyCallback? = null
|
||||||
) {
|
) {
|
||||||
val request = Request.Builder().url(BuildConfig.BASE_URL+url).
|
val request =
|
||||||
method(method.toString(), body)
|
Request.Builder().url(BuildConfig.BASE_URL + url).method(method.toString(), body)
|
||||||
if(headers !== null) request.headers(headers)
|
if (headers !== null) request.headers(headers)
|
||||||
http.newCall(request.build()).enqueue(object : Callback {
|
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())
|
Log.e("Http request failed", e.toString())
|
||||||
if(exceptionCallback !== null) exceptionCallback(e, null)
|
exceptionCallback?.invoke(e, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
override fun onResponse(call: Call, response: Response) {
|
||||||
var runCallback = false
|
var runCallback = false
|
||||||
if (!response.isSuccessful && exceptionCallback !== null)
|
if (!response.isSuccessful && exceptionCallback !== null)
|
||||||
runCallback = exceptionCallback(
|
runCallback = exceptionCallback(
|
||||||
IOException("Http response is not successful"), response)
|
HttpRequestException("Http response is not successful"), response
|
||||||
if(runCallback || response.isSuccessful) callback(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,17 +2,24 @@ package com.example.ssau_schedule.ui.theme
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
val Primary01 = Color(0xFF0D47A1)
|
class ApplicationColors {
|
||||||
val Primary02 = Color(0xFF134BC5)
|
companion object {
|
||||||
val Primary03 = Color(0xFF1D5DEB)
|
val Primary01 = Color(0xFF0D47A1)
|
||||||
val Primary04 = Color(0xFF2780E3)
|
val Primary02 = Color(0xFF134BC5)
|
||||||
val Primary05 = Color(0xFF6B92E5)
|
val Primary03 = Color(0xFF1D5DEB)
|
||||||
val Primary06 = Color(0xFFA8C4EC)
|
val Primary04 = Color(0xFF2780E3)
|
||||||
|
val Primary05 = Color(0xFF6B92E5)
|
||||||
|
val Primary06 = Color(0xFFA8C4EC)
|
||||||
|
|
||||||
val White = Color(0xFFFFFFFF)
|
val White = Color(0xFFFFFFFF)
|
||||||
|
|
||||||
|
val Gray01 = Color(0xFF2C2C2C)
|
||||||
|
val Gray02 = Color(0xFF383838)
|
||||||
|
val Gray03 = Color(0xFF66727F)
|
||||||
|
|
||||||
|
val Red01 = Color(0xFFEE3F58)
|
||||||
|
val Red02 = Color(0xFF7E212E)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val Gray01 = Color(0xFF2C2C2C)
|
|
||||||
val Gray02 = Color(0xFF383838)
|
|
||||||
val Gray03 = Color(0xFF66727F)
|
|
||||||
|
|
||||||
val Red = Color(0xFFEE3F58)
|
|
||||||
@@ -12,21 +12,23 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
private val DarkColorScheme = darkColorScheme(
|
||||||
primary = Primary01,
|
primary = ApplicationColors.Primary01,
|
||||||
secondary = Primary03,
|
secondary = ApplicationColors.Primary03,
|
||||||
tertiary = Primary04,
|
tertiary = ApplicationColors.Primary04,
|
||||||
background = Gray01,
|
background = ApplicationColors.Gray01,
|
||||||
surface = Gray02,
|
surface = ApplicationColors.Gray02,
|
||||||
error = Red,
|
error = ApplicationColors.Red01,
|
||||||
|
errorContainer = ApplicationColors.Red02
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorScheme = lightColorScheme(
|
private val LightColorScheme = lightColorScheme(
|
||||||
primary = Primary01,
|
primary = ApplicationColors.Primary01,
|
||||||
secondary = Primary03,
|
secondary = ApplicationColors.Primary03,
|
||||||
tertiary = Primary04,
|
tertiary = ApplicationColors.Primary04,
|
||||||
background = White,
|
background = ApplicationColors.White,
|
||||||
surface = Primary06,
|
surface = ApplicationColors.Primary06,
|
||||||
error = Red,
|
error = ApplicationColors.Red01,
|
||||||
|
errorContainer = ApplicationColors.Red02
|
||||||
|
|
||||||
/* Other default colors to override
|
/* Other default colors to override
|
||||||
background = Color(0xFFFFFBFE),
|
background = Color(0xFFFFFBFE),
|
||||||
|
|||||||
@@ -10,4 +10,8 @@
|
|||||||
<string name="incorrect_login_or_password">Incorrect login or password</string>
|
<string name="incorrect_login_or_password">Incorrect login or password</string>
|
||||||
<string name="login_is_too_short">Login is too short</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="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>
|
</resources>
|
||||||
@@ -9,4 +9,8 @@
|
|||||||
<string name="incorrect_login_or_password">Неверный логин или пароль</string>
|
<string name="incorrect_login_or_password">Неверный логин или пароль</string>
|
||||||
<string name="login_is_too_short">Логин слишком короткий</string>
|
<string name="login_is_too_short">Логин слишком короткий</string>
|
||||||
<string name="password_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>
|
</resources>
|
||||||
@@ -2,5 +2,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
|
alias(libs.plugins.kotlin.serialization) apply false
|
||||||
alias(libs.plugins.compose.compiler) apply false
|
alias(libs.plugins.compose.compiler) apply false
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ composeBom = "2024.09.00"
|
|||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
datastore = "1.1.1"
|
datastore = "1.1.1"
|
||||||
media3Common = "1.4.1"
|
media3Common = "1.4.1"
|
||||||
|
kotlinSerializationJson = "1.7.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
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" }
|
squareup-okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||||
androidx-datastore = { group = "androidx.datastore", name="datastore-preferences", version.ref = "datastore" }
|
androidx-datastore = { group = "androidx.datastore", name="datastore-preferences", version.ref = "datastore" }
|
||||||
androidx-media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3Common" }
|
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]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", 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