mirror of
https://github.com/StepanovPlaton/SSAU_Schedule.git
synced 2026-04-03 20:30:40 +04:00
18.09
This commit is contained in:
@@ -5,6 +5,8 @@ plugins {
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.devtools.ksp)
|
||||
alias(libs.plugins.androidx.room)
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -50,7 +52,10 @@ android {
|
||||
"\"api/proxy/personal/groups\"", null)),
|
||||
Pair("YEARS_URL",
|
||||
BuildConfigField("String",
|
||||
"\"api/proxy/dictionaries?slug=unified_years\"", null))
|
||||
"\"api/proxy/dictionaries?slug=unified_years\"", null)),
|
||||
Pair("LESSONS_URL",
|
||||
BuildConfigField("String",
|
||||
"\"/api/proxy/timetable/get-timetable\"", null))
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -73,6 +78,10 @@ android {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
|
||||
room {
|
||||
schemaDirectory("$projectDir/schemas")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -85,6 +94,7 @@ dependencies {
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.media3.common)
|
||||
implementation(libs.androidx.work.runtime.ktx)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
@@ -94,6 +104,14 @@ dependencies {
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
implementation(libs.squareup.okhttp)
|
||||
|
||||
implementation(libs.androidx.datastore)
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
}
|
||||
|
||||
implementation(libs.androidx.room.runtime)
|
||||
annotationProcessor(libs.androidx.room.compiler)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
|
||||
implementation(libs.androidx.room.ktx)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "d3086e45becf402ec027d32056b910a8",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "lessons",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` TEXT, `discipline` TEXT NOT NULL, `week` INTEGER NOT NULL, `day_of_week` INTEGER NOT NULL, `teacher` TEXT NOT NULL, `begin_time` TEXT NOT NULL, `end_time` TEXT NOT NULL, `conference_url` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "discipline",
|
||||
"columnName": "discipline",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "week",
|
||||
"columnName": "week",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "dayOfWeek",
|
||||
"columnName": "day_of_week",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "teacher",
|
||||
"columnName": "teacher",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "beginTime",
|
||||
"columnName": "begin_time",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "endTime",
|
||||
"columnName": "end_time",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "conferenceUrl",
|
||||
"columnName": "conference_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd3086e45becf402ec027d32056b910a8')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -15,7 +16,7 @@
|
||||
android:theme="@style/Theme.SSAU_Schedule"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".AuthActivity"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.SSAU_Schedule"
|
||||
android:screenOrientation="portrait">
|
||||
@@ -26,7 +27,7 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:name=".AuthActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.SSAU_Schedule"
|
||||
android:screenOrientation="portrait">
|
||||
|
||||
@@ -55,18 +55,23 @@ 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.GroupAPI
|
||||
import com.example.ssau_schedule.api.GroupAPIErrorMessage
|
||||
import com.example.ssau_schedule.api.Http
|
||||
import com.example.ssau_schedule.api.GeneralApi
|
||||
import com.example.ssau_schedule.api.UserAPI
|
||||
import com.example.ssau_schedule.api.YearAPI
|
||||
import com.example.ssau_schedule.api.YearAPIErrorMessage
|
||||
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.GroupStore
|
||||
import com.example.ssau_schedule.data.store.Year
|
||||
import com.example.ssau_schedule.data.store.YearStore
|
||||
import com.example.ssau_schedule.data.unsaved.User
|
||||
import com.example.ssau_schedule.ui.theme.ApplicationColors
|
||||
import com.example.ssau_schedule.ui.theme.SSAU_ScheduleTheme
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -75,10 +80,11 @@ import java.util.Date
|
||||
import kotlin.math.min
|
||||
|
||||
class AuthActivity : ComponentActivity() {
|
||||
|
||||
private val http = Http()
|
||||
private var authApi: AuthorizationAPI? = null
|
||||
private var userApi: GeneralApi? = null
|
||||
private val authAPI = AuthorizationAPI(http)
|
||||
private val userAPI = UserAPI(http)
|
||||
private val groupAPI = GroupAPI(http)
|
||||
private val yearAPI = YearAPI(http)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -96,8 +102,6 @@ class AuthActivity : ComponentActivity() {
|
||||
@Composable
|
||||
fun AuthPage() {
|
||||
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) }
|
||||
@@ -110,7 +114,7 @@ class AuthActivity : ComponentActivity() {
|
||||
val keyboardOpen by Utils.keyboardState()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val logoHeight by animateFloatAsState(
|
||||
if (keyboardOpen) 0f else min(
|
||||
if (keyboardOpen && needAuth) 0f else min(
|
||||
LocalConfiguration.current.screenWidthDp,
|
||||
500
|
||||
) / 101f * 48f,
|
||||
@@ -123,65 +127,54 @@ class AuthActivity : ComponentActivity() {
|
||||
|
||||
LaunchedEffect(user, group, year) {
|
||||
if(user != null && group != null && year != null) {
|
||||
delay(1500)
|
||||
delay(2500)
|
||||
startActivity(Intent(applicationContext, MainActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
delay(3000)
|
||||
|
||||
val token = AuthStore.getAuthToken(applicationContext)
|
||||
if(token == null) { needAuth = true; return@LaunchedEffect }
|
||||
|
||||
val (userDetails) = userAPI.getUserDetails(token)
|
||||
if(userDetails == null) { needAuth = true; return@LaunchedEffect }
|
||||
else { user = userDetails }
|
||||
|
||||
val (groups, groupsError) = groupAPI.getUserGroups(token)
|
||||
if(groups == null) {
|
||||
if(groupsError != null && groupsError !=
|
||||
GroupAPIErrorMessage.USER_NOT_AUTHORIZED) {
|
||||
val message = groupsError.getMessage(applicationContext)
|
||||
if(message != null) snackbarHostState.showSnackbar(message)
|
||||
} else { needAuth = true; return@LaunchedEffect }
|
||||
} else {
|
||||
val currentGroup = GroupStore.getCurrentGroup(applicationContext)
|
||||
if(currentGroup != null && groups.contains(currentGroup)) group = currentGroup
|
||||
else {
|
||||
GroupStore.setCurrentGroup(groups[0], applicationContext)
|
||||
group = groups[0]
|
||||
}
|
||||
}
|
||||
|
||||
val (years, yearsError) = yearAPI.getYears(token)
|
||||
if(years == null) {
|
||||
if(yearsError != null && yearsError !=
|
||||
YearAPIErrorMessage.USER_NOT_AUTHORIZED) {
|
||||
val message = yearsError.getMessage(applicationContext)
|
||||
if(message != null) snackbarHostState.showSnackbar(message)
|
||||
} else { needAuth = true; return@LaunchedEffect }
|
||||
} else {
|
||||
val currentRawYear = years.find { y -> y.isCurrent }
|
||||
if(currentRawYear != null) {
|
||||
year = currentRawYear.toYear()
|
||||
YearStore.setCurrentYear(year!!, applicationContext, authScope)
|
||||
} else {
|
||||
val message = YearAPIErrorMessage.FAILED_GET_YEARS
|
||||
.getMessage(applicationContext)
|
||||
if(message != null) snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
else needAuth = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,18 +184,13 @@ class AuthActivity : ComponentActivity() {
|
||||
Snackbar(
|
||||
snackbarData = it,
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Box(
|
||||
Modifier
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding()
|
||||
.imePadding(),
|
||||
Box(Modifier.background(MaterialTheme.colorScheme.primary)
|
||||
.fillMaxSize().padding(padding).imePadding(),
|
||||
contentAlignment = BiasAlignment(0f, -0.25f),
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
@@ -225,7 +213,7 @@ class AuthActivity : ComponentActivity() {
|
||||
.widthIn(0.dp, 400.dp)) {
|
||||
Column {
|
||||
WelcomeMessage(user, group, year)
|
||||
AuthForm(open = needAuth) {
|
||||
AuthForm(open = needAuth, authScope) {
|
||||
needAuth = false
|
||||
entered = true
|
||||
}
|
||||
@@ -239,7 +227,7 @@ class AuthActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AuthForm(open: Boolean, callback: () -> Unit) {
|
||||
fun AuthForm(open: Boolean, scope: CoroutineScope, callback: () -> Unit) {
|
||||
var login by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var error by remember { mutableStateOf<AuthErrorMessage?>(null) }
|
||||
@@ -251,20 +239,12 @@ class AuthActivity : ComponentActivity() {
|
||||
)
|
||||
)
|
||||
|
||||
Card(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(height)
|
||||
.padding(0.dp, 10.dp)
|
||||
.shadow(10.dp),
|
||||
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),
|
||||
Column(Modifier.fillMaxWidth().padding(30.dp, 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(stringResource(R.string.sign_in),
|
||||
@@ -284,10 +264,7 @@ class AuthActivity : ComponentActivity() {
|
||||
label = { Text(stringResource(R.string.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
|
||||
@@ -300,24 +277,22 @@ class AuthActivity : ComponentActivity() {
|
||||
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
|
||||
else {
|
||||
authApi?.signIn(login, password,
|
||||
{ callback() },
|
||||
{ error = AuthErrorMessage.INCORRECT_LOGIN_OR_PASSWORD }
|
||||
)
|
||||
if (login.length < 5) error = AuthErrorMessage.LOGIN_IS_TOO_SHORT
|
||||
else if (password.length < 5) error = AuthErrorMessage.PASSWORD_IS_TOO_SHORT
|
||||
else scope.launch {
|
||||
val (token) = authAPI.signIn(login, password)
|
||||
if(token != null) {
|
||||
AuthStore.setAuthToken(token, applicationContext)
|
||||
callback()
|
||||
}
|
||||
else error = AuthErrorMessage.INCORRECT_LOGIN_OR_PASSWORD
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(50),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Text(stringResource(R.string.sign_in))
|
||||
}
|
||||
) { Text(stringResource(R.string.sign_in)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,16 +306,17 @@ class AuthActivity : ComponentActivity() {
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if(user !== null && group != null && year != null) {
|
||||
Text("Здравствуйте ${user.name}!",
|
||||
Text("${stringResource(R.string.hello)} ${user.name}!",
|
||||
color = ApplicationColors.White,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center)
|
||||
Text("Расписание для группы ${group.name}",
|
||||
Text("${stringResource(R.string.schedule_for_group)} ${group.name}",
|
||||
color = ApplicationColors.White,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
textAlign = TextAlign.Center)
|
||||
Text("$currentDate, ${year.getWeekOfDate(Date())} "+
|
||||
"учебная неделя, ${currentYear}-${currentYear+1} учебный год",
|
||||
"${stringResource(R.string.education_week)}, ${currentYear}-"+
|
||||
"${currentYear+1} ${stringResource(R.string.education_year)}",
|
||||
color = ApplicationColors.White,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = TextAlign.Center)
|
||||
|
||||
@@ -1,25 +1,73 @@
|
||||
package com.example.ssau_schedule
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.DateRange
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.example.ssau_schedule.api.Http
|
||||
import com.example.ssau_schedule.api.LessonAPI
|
||||
import com.example.ssau_schedule.api.LessonAPIErrorMessage
|
||||
import com.example.ssau_schedule.components.LessonCards
|
||||
import com.example.ssau_schedule.data.base.Database
|
||||
import com.example.ssau_schedule.data.base.entity.lesson.Lesson
|
||||
import com.example.ssau_schedule.data.store.StoreUtils
|
||||
import com.example.ssau_schedule.ui.theme.SSAU_ScheduleTheme
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.Date
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val http = Http()
|
||||
private val lessonAPI = LessonAPI(http)
|
||||
private lateinit var database: Database
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -36,15 +84,115 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
@Composable
|
||||
fun MainPage() {
|
||||
Box(
|
||||
Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
.statusBarsPadding()
|
||||
.navigationBarsPadding()
|
||||
.imePadding(),
|
||||
) {
|
||||
Text("main page")
|
||||
database = remember { Database.getInstance(applicationContext) }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val lessons = remember { mutableStateOf<List<Lesson>>(listOf()) }
|
||||
|
||||
val currentDate = remember { mutableStateOf(Date()) }
|
||||
val currentDayOfWeek = remember {
|
||||
mutableIntStateOf(Utils.Date.getDateOfWeek(currentDate.value))
|
||||
}
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = currentDayOfWeek.intValue-1, pageCount = {Int.MAX_VALUE})
|
||||
|
||||
LaunchedEffect(false) {
|
||||
lessons.value = database.lessonDao().getAll()
|
||||
}
|
||||
|
||||
// LaunchedEffect(false) {
|
||||
// val generalData = StoreUtils.getGeneralData(applicationContext)
|
||||
// if(generalData == null)
|
||||
// startActivity(Intent(applicationContext, AuthActivity::class.java))
|
||||
// else {
|
||||
// val week = generalData.year.getWeekOfDate(Date())
|
||||
// val (apiLessons, apiError) = lessonAPI.getLessons(
|
||||
// generalData.token, generalData.group, generalData.year, week)
|
||||
// if(apiLessons != null && apiError == null) {
|
||||
// val (databaseLessons, converterErrors) = apiLessons.toLessons(week)
|
||||
// Log.i("Lessons", Json.encodeToString(apiLessons))
|
||||
// database.lessonDao().insert(*databaseLessons.toTypedArray())
|
||||
// converterErrors.forEach { error ->
|
||||
// val message = error.getMessage(applicationContext)
|
||||
// if(message != null) snackbarHostState.showSnackbar(message)
|
||||
// }
|
||||
// lessons.value = databaseLessons
|
||||
// } else {
|
||||
// if(apiError == LessonAPIErrorMessage.USER_NOT_AUTHORIZED) {
|
||||
// startActivity(Intent(applicationContext, AuthActivity::class.java))
|
||||
// } else {
|
||||
// val message = apiError?.getMessage(applicationContext)
|
||||
// if(message != null) snackbarHostState.showSnackbar(message)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState) {
|
||||
Snackbar(
|
||||
snackbarData = it,
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Box(
|
||||
Modifier
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.imePadding(),
|
||||
) {
|
||||
Column {
|
||||
Box(Modifier.fillMaxWidth().height(60.dp)) {
|
||||
Row(Modifier.fillMaxSize().padding(10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Box(Modifier.height(40.dp).width(40.dp)
|
||||
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
|
||||
.background(MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
||||
contentDescription = "Forward icon",
|
||||
Modifier.fillMaxSize(),
|
||||
tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
Box(Modifier.height(40.dp)
|
||||
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
|
||||
.background(MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
Row(Modifier.fillMaxHeight().padding(10.dp, 0.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.DateRange,
|
||||
contentDescription = "Date icon",
|
||||
Modifier.height(40.dp).padding(0.dp, 0.dp, 10.dp, 0.dp),
|
||||
tint = MaterialTheme.colorScheme.primary)
|
||||
Text(Utils.Date.format(currentDate.value),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
style = MaterialTheme.typography.bodyLarge)
|
||||
}
|
||||
}
|
||||
Box(Modifier.height(40.dp).width(40.dp)
|
||||
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
|
||||
.background(MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||
contentDescription = "Forward icon",
|
||||
Modifier.fillMaxSize(),
|
||||
tint = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalDivider(Modifier.padding(20.dp, 0.dp))
|
||||
HorizontalPager(state = pagerState) { _ ->
|
||||
LessonCards(lessons.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.ssau_schedule
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Rect
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -8,9 +9,16 @@ import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
|
||||
class Utils {
|
||||
companion object {
|
||||
val Serializer = Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun keyboardState(): State<Boolean> {
|
||||
@@ -31,4 +39,24 @@ class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
class Date {
|
||||
companion object {
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
val StoreDateFormat = SimpleDateFormat("yyyy-MM-dd")
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
val DateFormat = SimpleDateFormat("dd MMMM")
|
||||
|
||||
fun parse(dateString: String): java.util.Date = StoreDateFormat.parse(dateString)!!
|
||||
fun storeFormat(date: java.util.Date): String = StoreDateFormat.format(date)
|
||||
fun format(date: java.util.Date): String = DateFormat.format(date)
|
||||
|
||||
fun getDateOfWeek(data: java.util.Date): Int {
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.minimalDaysInFirstWeek = 6
|
||||
calendar.firstDayOfWeek = Calendar.MONDAY
|
||||
calendar.time = data
|
||||
return calendar.get(Calendar.DAY_OF_WEEK)-1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,37 +3,30 @@ package com.example.ssau_schedule.api
|
||||
import android.content.Context
|
||||
import com.example.ssau_schedule.BuildConfig
|
||||
import com.example.ssau_schedule.R
|
||||
import com.example.ssau_schedule.data.store.AuthStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import org.json.JSONArray
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
enum class AuthErrorMessage(private val resource: Int) {
|
||||
enum class AuthErrorMessage(private val resource: Int?) {
|
||||
LOGIN_IS_TOO_SHORT(R.string.login_is_too_short),
|
||||
PASSWORD_IS_TOO_SHORT(R.string.password_is_too_short),
|
||||
INCORRECT_LOGIN_OR_PASSWORD(R.string.incorrect_login_or_password);
|
||||
|
||||
fun getMessage(context: Context) =
|
||||
context.getString(resource)
|
||||
if(resource != null) context.getString(resource) else null
|
||||
}
|
||||
|
||||
class AuthorizationAPI(
|
||||
private var http: Http,
|
||||
private var context: Context,
|
||||
private var scope: CoroutineScope
|
||||
) {
|
||||
private val responseHasAuthToken =
|
||||
class AuthorizationAPI(private var http: Http) {
|
||||
private val getAuthToken =
|
||||
{ response: Response? -> response?.headers?.toMap()?.containsKey("set-cookie") == true }
|
||||
|
||||
fun signIn(
|
||||
suspend fun signIn(
|
||||
login: String, password: String,
|
||||
callback: (token: String) -> Unit,
|
||||
exceptionCallback: ((error: HttpRequestException) -> Unit)? = null
|
||||
) {
|
||||
http.request(
|
||||
): Pair<String?, HttpRequestException?> {
|
||||
val (response, exception) = http.request(
|
||||
Method.POST,
|
||||
BuildConfig.SIGN_IN_URL,
|
||||
JSONArray(
|
||||
@@ -46,22 +39,9 @@ class AuthorizationAPI(
|
||||
).toString().toRequestBody("application/json".toMediaType()),
|
||||
mapOf(
|
||||
Pair("Next-Action", "b395d17834d8b7df06372cbf1f241170a272d540")
|
||||
).toHeaders(),
|
||||
fun(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
|
||||
exceptionCallback?.invoke(error)
|
||||
return false
|
||||
}
|
||||
)
|
||||
).toHeaders())
|
||||
val token = if(response?.headers?.toMap()?.containsKey("set-cookie") == true)
|
||||
response.headers("set-cookie").joinToString(", ") else null
|
||||
return Pair(token, exception)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
44
app/src/main/java/com/example/ssau_schedule/api/Group.kt
Normal file
44
app/src/main/java/com/example/ssau_schedule/api/Group.kt
Normal file
@@ -0,0 +1,44 @@
|
||||
package com.example.ssau_schedule.api
|
||||
|
||||
import android.content.Context
|
||||
import com.example.ssau_schedule.BuildConfig
|
||||
import com.example.ssau_schedule.R
|
||||
import com.example.ssau_schedule.Utils
|
||||
import com.example.ssau_schedule.data.store.Group
|
||||
import kotlinx.serialization.SerializationException
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
|
||||
enum class GroupAPIErrorMessage(private val resource: Int?) {
|
||||
NOT_MEMBER_OF_ANY_GROUP(R.string.not_member_of_any_group),
|
||||
FAILED_GET_USER_GROUPS(R.string.failed_get_user_groups),
|
||||
|
||||
USER_NOT_AUTHORIZED(null);
|
||||
|
||||
fun getMessage(context: Context) =
|
||||
if(resource != null) context.getString(resource) else null
|
||||
}
|
||||
|
||||
class GroupAPI(private var http: Http) {
|
||||
suspend fun getUserGroups(token: String): Pair<List<Group>?, GroupAPIErrorMessage?> {
|
||||
val (response) = http.request(
|
||||
Method.GET,
|
||||
BuildConfig.USER_GROUPS_URL,
|
||||
mapOf(
|
||||
Pair("Cookie", token)
|
||||
).toHeaders())
|
||||
if(response?.code == 401) return Pair(null, GroupAPIErrorMessage.USER_NOT_AUTHORIZED)
|
||||
if(response?.body == null) return Pair(null, GroupAPIErrorMessage.FAILED_GET_USER_GROUPS)
|
||||
else {
|
||||
try {
|
||||
val groups = Utils.Serializer
|
||||
.decodeFromString<List<Group>>(response.body!!.string())
|
||||
return if (groups.isNotEmpty()) Pair(groups, null)
|
||||
else Pair(null, GroupAPIErrorMessage.NOT_MEMBER_OF_ANY_GROUP)
|
||||
} catch (e: SerializationException) {
|
||||
return Pair(null, GroupAPIErrorMessage.FAILED_GET_USER_GROUPS)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return Pair(null, GroupAPIErrorMessage.FAILED_GET_USER_GROUPS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.Response
|
||||
import okio.IOException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
enum class Method {
|
||||
GET,
|
||||
@@ -20,56 +22,44 @@ enum class Method {
|
||||
|
||||
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()
|
||||
|
||||
fun request(
|
||||
suspend fun request(
|
||||
method: Method,
|
||||
url: String,
|
||||
body: RequestBody? = null,
|
||||
headers: Headers? = null,
|
||||
callback: HttpResponseCallback,
|
||||
exceptionCallback: HttpExceptionVerifyCallback? = null
|
||||
) {
|
||||
): Pair<Response?, HttpRequestException?> {
|
||||
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: HttpRequestException) {
|
||||
Log.e("Http request failed", e.toString())
|
||||
exceptionCallback?.invoke(e, null)
|
||||
}
|
||||
return suspendCoroutine { coroutine ->
|
||||
http.newCall(request.build()).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: HttpRequestException) {
|
||||
Log.e("Http request failed", e.toString())
|
||||
coroutine.resume(Pair(null, e))
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (!response.isSuccessful)
|
||||
coroutine.resume(Pair(response,
|
||||
HttpRequestException("Http response is not successful")))
|
||||
else coroutine.resume(Pair(response, null))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
var runCallback = false
|
||||
if (!response.isSuccessful && exceptionCallback !== null)
|
||||
runCallback = exceptionCallback(
|
||||
HttpRequestException("Http response is not successful"), response
|
||||
)
|
||||
if (runCallback || response.isSuccessful) callback(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun request(
|
||||
suspend fun request(
|
||||
method: Method,
|
||||
url: String,
|
||||
headers: Headers? = null,
|
||||
callback: HttpResponseCallback,
|
||||
exceptionCallback: HttpExceptionVerifyCallback? = null
|
||||
) = request(method, url, null, headers, callback, exceptionCallback)
|
||||
) = request(method, url, null, headers)
|
||||
|
||||
fun request(
|
||||
suspend fun request(
|
||||
method: Method,
|
||||
url: String,
|
||||
callback: HttpResponseCallback,
|
||||
exceptionCallback: HttpExceptionVerifyCallback? = null
|
||||
) = request(method, url, null, null, callback, exceptionCallback)
|
||||
) = request(method, url, null, null)
|
||||
}
|
||||
49
app/src/main/java/com/example/ssau_schedule/api/Lesson.kt
Normal file
49
app/src/main/java/com/example/ssau_schedule/api/Lesson.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
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.Utils
|
||||
import com.example.ssau_schedule.data.unsaved.APILessons
|
||||
import com.example.ssau_schedule.data.store.Group
|
||||
import com.example.ssau_schedule.data.store.Year
|
||||
import kotlinx.serialization.SerializationException
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
|
||||
enum class LessonAPIErrorMessage(private val resource: Int?) {
|
||||
FAILED_GET_LESSONS(R.string.failed_get_lessons),
|
||||
|
||||
USER_NOT_AUTHORIZED(null);
|
||||
|
||||
fun getMessage(context: Context) =
|
||||
if(resource != null) context.getString(resource) else null
|
||||
}
|
||||
|
||||
class LessonAPI(private var http: Http) {
|
||||
suspend fun getLessons(
|
||||
token: String,
|
||||
group: Group,
|
||||
year: Year,
|
||||
week: Int,
|
||||
): Pair<APILessons?, LessonAPIErrorMessage?> {
|
||||
val (response) = http.request(
|
||||
Method.GET,
|
||||
"${BuildConfig.LESSONS_URL}?yearId=${year.id}"+
|
||||
"&week=$week&userType=student&groupId=${group.id}",
|
||||
mapOf(
|
||||
Pair("Cookie", token)
|
||||
).toHeaders())
|
||||
if(response?.code == 401) return Pair(null, LessonAPIErrorMessage.USER_NOT_AUTHORIZED)
|
||||
if(response?.body == null) return Pair(null, LessonAPIErrorMessage.FAILED_GET_LESSONS)
|
||||
try { return Pair(Utils.Serializer
|
||||
.decodeFromString<APILessons>(response.body!!.string()), null)
|
||||
} catch(e: SerializationException) {
|
||||
Log.e("Serialization error", e.message.toString())
|
||||
return Pair(null, LessonAPIErrorMessage.FAILED_GET_LESSONS)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e("Serialization error", e.message.toString())
|
||||
return Pair(null, LessonAPIErrorMessage.FAILED_GET_LESSONS)
|
||||
}
|
||||
}
|
||||
}
|
||||
40
app/src/main/java/com/example/ssau_schedule/api/User.kt
Normal file
40
app/src/main/java/com/example/ssau_schedule/api/User.kt
Normal file
@@ -0,0 +1,40 @@
|
||||
package com.example.ssau_schedule.api
|
||||
|
||||
import android.content.Context
|
||||
import com.example.ssau_schedule.BuildConfig
|
||||
import com.example.ssau_schedule.R
|
||||
import com.example.ssau_schedule.Utils
|
||||
import com.example.ssau_schedule.data.unsaved.User
|
||||
import kotlinx.serialization.SerializationException
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
|
||||
enum class UserAPIErrorMessage(private val resource: Int?) {
|
||||
FAILED_GET_USER_DETAILS(R.string.failed_get_user_details),
|
||||
|
||||
USER_NOT_AUTHORIZED(null);
|
||||
|
||||
fun getMessage(context: Context) =
|
||||
if(resource != null) context.getString(resource) else null
|
||||
}
|
||||
|
||||
class UserAPI(private var http: Http) {
|
||||
suspend fun getUserDetails(
|
||||
token: String,
|
||||
): Pair<User?, UserAPIErrorMessage?> {
|
||||
val (response) = http.request(
|
||||
Method.GET,
|
||||
BuildConfig.USER_DETAILS_URL,
|
||||
mapOf(
|
||||
Pair("Cookie", token)
|
||||
).toHeaders())
|
||||
if(response?.code == 401) return Pair(null, UserAPIErrorMessage.USER_NOT_AUTHORIZED)
|
||||
if(response?.body == null) return Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS)
|
||||
return try {
|
||||
Pair(Utils.Serializer.decodeFromString<User>(response.body!!.string()), null)
|
||||
} catch(e: SerializationException) {
|
||||
Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS)
|
||||
}
|
||||
}
|
||||
}
|
||||
45
app/src/main/java/com/example/ssau_schedule/api/Year.kt
Normal file
45
app/src/main/java/com/example/ssau_schedule/api/Year.kt
Normal file
@@ -0,0 +1,45 @@
|
||||
package com.example.ssau_schedule.api
|
||||
|
||||
import android.content.Context
|
||||
import com.example.ssau_schedule.BuildConfig
|
||||
import com.example.ssau_schedule.R
|
||||
import com.example.ssau_schedule.Utils
|
||||
import com.example.ssau_schedule.data.store.RawYear
|
||||
import kotlinx.serialization.SerializationException
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
|
||||
enum class YearAPIErrorMessage(private val resource: Int?) {
|
||||
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 YearAPI(private var http: Http) {
|
||||
suspend fun getYears(
|
||||
token: String,
|
||||
): Pair<List<RawYear>?, YearAPIErrorMessage?> {
|
||||
val (response) = http.request(
|
||||
Method.GET,
|
||||
BuildConfig.YEARS_URL,
|
||||
mapOf(
|
||||
Pair("Cookie", token)
|
||||
).toHeaders())
|
||||
if(response?.code == 401) return Pair(null, YearAPIErrorMessage.USER_NOT_AUTHORIZED)
|
||||
if(response?.body == null) return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
|
||||
try {
|
||||
val rawYears = Utils.Serializer
|
||||
.decodeFromString<List<RawYear>>(response.body!!.string())
|
||||
return if(rawYears.isNotEmpty()) Pair(rawYears, null)
|
||||
else Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
|
||||
}
|
||||
catch(e: SerializationException) {
|
||||
return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
|
||||
}
|
||||
catch (e: IllegalArgumentException) {
|
||||
return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.example.ssau_schedule.components
|
||||
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun AutoResizeText(
|
||||
text: String,
|
||||
fontSizeRange: FontSizeRange,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
fontStyle: FontStyle? = null,
|
||||
fontWeight: FontWeight? = null,
|
||||
fontFamily: FontFamily? = null,
|
||||
letterSpacing: TextUnit = TextUnit.Unspecified,
|
||||
textDecoration: TextDecoration? = null,
|
||||
textAlign: TextAlign? = null,
|
||||
lineHeight: TextUnit = TextUnit.Unspecified,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
softWrap: Boolean = true,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
) {
|
||||
val fontSizeValue = remember { mutableFloatStateOf(fontSizeRange.max.value) }
|
||||
val readyToDraw = remember { mutableStateOf(false) }
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
color = color,
|
||||
maxLines = maxLines,
|
||||
fontStyle = fontStyle,
|
||||
fontWeight = fontWeight,
|
||||
fontFamily = fontFamily,
|
||||
letterSpacing = letterSpacing,
|
||||
textDecoration = textDecoration,
|
||||
textAlign = textAlign,
|
||||
lineHeight = lineHeight,
|
||||
overflow = overflow,
|
||||
softWrap = softWrap,
|
||||
style = style,
|
||||
fontSize = fontSizeValue.floatValue.sp,
|
||||
onTextLayout = {
|
||||
if (it.didOverflowHeight && !readyToDraw.value) {
|
||||
val nextFontSizeValue = fontSizeValue.floatValue - fontSizeRange.step.value
|
||||
if (nextFontSizeValue <= fontSizeRange.min.value) {
|
||||
fontSizeValue.floatValue = fontSizeRange.min.value
|
||||
readyToDraw.value = true
|
||||
} else fontSizeValue.floatValue = nextFontSizeValue
|
||||
} else readyToDraw.value = true
|
||||
},
|
||||
modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() }
|
||||
)
|
||||
}
|
||||
|
||||
data class FontSizeRange(
|
||||
val min: TextUnit,
|
||||
val max: TextUnit,
|
||||
val step: TextUnit = DEFAULT_TEXT_STEP,
|
||||
) {
|
||||
init {
|
||||
require(min < max) { "min should be less than max, $this" }
|
||||
require(step.value > 0) { "step should be greater than 0, $this" }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_TEXT_STEP = 1.sp
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.example.ssau_schedule.components
|
||||
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.ssau_schedule.data.base.entity.lesson.Lesson
|
||||
import com.example.ssau_schedule.ui.theme.LessonColors
|
||||
|
||||
@Composable
|
||||
fun LessonCard(modifier: Modifier, lesson: Lesson) {
|
||||
Row(modifier.fillMaxWidth()
|
||||
.height(130.dp).padding(14.dp, 8.dp)
|
||||
.shadow(elevation = 6.dp, shape = RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
if(isSystemInDarkTheme())
|
||||
lesson.type?.darkBackground ?: LessonColors.Background.Dark.Unknown
|
||||
else lesson.type?.lightBackground ?: LessonColors.Background.Light.Unknown
|
||||
)
|
||||
) {
|
||||
Box(modifier.fillMaxHeight().width(16.dp).shadow(4.dp)
|
||||
.background(lesson.type?.foreground ?: LessonColors.Foreground.Unknown))
|
||||
Column(modifier.fillMaxHeight().padding(10.dp, 10.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween) {
|
||||
Row(modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text("${lesson.beginTime} - ${lesson.endTime}",
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
style = MaterialTheme.typography.bodyMedium)
|
||||
Text("512 - 5",
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
AutoResizeText(lesson.discipline,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
fontSizeRange = FontSizeRange(10.sp, 24.sp),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = MaterialTheme.typography.titleLarge)
|
||||
Text(lesson.teacher,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = MaterialTheme.typography.titleSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LessonCards(lessons: List<Lesson>) {
|
||||
Column(Modifier.verticalScroll(ScrollState(0))) {
|
||||
lessons.forEach { lesson ->
|
||||
LessonCard(Modifier, lesson)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.example.ssau_schedule.data.base
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverter
|
||||
import androidx.room.TypeConverters
|
||||
import com.example.ssau_schedule.data.base.dao.LessonDao
|
||||
import com.example.ssau_schedule.data.base.entity.lesson.Lesson
|
||||
import com.example.ssau_schedule.data.base.entity.lesson.LessonType
|
||||
|
||||
class Converters {
|
||||
@TypeConverter fun toLessonType(value: String) = LessonType.getTypeFromName(value)
|
||||
@TypeConverter fun fromLessonType(value: LessonType) = value.displayName
|
||||
}
|
||||
|
||||
@androidx.room.Database(
|
||||
entities = [Lesson::class],
|
||||
version = 1,
|
||||
autoMigrations = [])
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class Database : RoomDatabase() {
|
||||
abstract fun lessonDao(): LessonDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var database: Database? = null
|
||||
fun getInstance(context: Context): Database =
|
||||
database
|
||||
?: synchronized(this) {
|
||||
database
|
||||
?: Room.databaseBuilder(
|
||||
context,
|
||||
Database::class.java, "database"
|
||||
).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.example.ssau_schedule.data.base.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.example.ssau_schedule.data.base.entity.lesson.Lesson
|
||||
|
||||
@Dao
|
||||
interface LessonDao {
|
||||
@Query("SELECT * FROM lessons")
|
||||
suspend fun getAll(): List<Lesson>
|
||||
|
||||
|
||||
@Query("SELECT * FROM lessons WHERE id IN (:ids)")
|
||||
suspend fun getById(ids: IntArray): List<Lesson>
|
||||
|
||||
@Query("SELECT * FROM lessons WHERE id = (:id) LIMIT 1")
|
||||
suspend fun getById(id: Int): Lesson
|
||||
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(lesson: Lesson)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg lessons: Lesson)
|
||||
|
||||
|
||||
@Delete
|
||||
suspend fun delete(lesson: Lesson)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.example.ssau_schedule.data.base.entity.lesson
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.example.ssau_schedule.ui.theme.LessonColors
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
enum class LessonType(
|
||||
val displayName: String,
|
||||
val foreground: Color,
|
||||
val darkBackground: Color,
|
||||
val lightBackground: Color,
|
||||
) {
|
||||
LECTURE("Лекция",
|
||||
LessonColors.Foreground.Lecture,
|
||||
LessonColors.Background.Dark.Lecture,
|
||||
LessonColors.Background.Light.Lecture),
|
||||
PRACTICE("Практика",
|
||||
LessonColors.Foreground.Practice,
|
||||
LessonColors.Background.Dark.Practice,
|
||||
LessonColors.Background.Light.Practice),
|
||||
LABORATORY("Лабораторная",
|
||||
LessonColors.Foreground.Laboratory,
|
||||
LessonColors.Background.Dark.Laboratory,
|
||||
LessonColors.Background.Light.Laboratory),
|
||||
OTHER("Другое",
|
||||
LessonColors.Foreground.Other,
|
||||
LessonColors.Background.Dark.Other,
|
||||
LessonColors.Background.Light.Other),
|
||||
EXAMINATION("Экзамен",
|
||||
LessonColors.Foreground.Examination,
|
||||
LessonColors.Background.Dark.Examination,
|
||||
LessonColors.Background.Light.Examination),
|
||||
TEST("Зачёт",
|
||||
LessonColors.Foreground.Test,
|
||||
LessonColors.Background.Dark.Test,
|
||||
LessonColors.Background.Light.Test),
|
||||
CONSULTATION("Консультация",
|
||||
LessonColors.Foreground.Consultation,
|
||||
LessonColors.Background.Dark.Consultation,
|
||||
LessonColors.Background.Light.Consultation);
|
||||
|
||||
companion object {
|
||||
fun getTypeFromName(name: String) =
|
||||
entries.firstOrNull() { it.displayName == name }
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@Entity(tableName = "lessons")
|
||||
data class Lesson(
|
||||
@PrimaryKey val id: Int,
|
||||
@ColumnInfo(name = "type") val type: LessonType?,
|
||||
@ColumnInfo(name = "discipline") val discipline: String,
|
||||
@ColumnInfo(name = "week") val week: Int,
|
||||
@ColumnInfo(name = "day_of_week") val dayOfWeek: Int,
|
||||
@ColumnInfo(name = "teacher") val teacher: String,
|
||||
@ColumnInfo(name = "begin_time") val beginTime: String,
|
||||
@ColumnInfo(name = "end_time") val endTime: String,
|
||||
@ColumnInfo(name = "conference_url") val conferenceUrl: String?,
|
||||
)
|
||||
@@ -19,31 +19,25 @@ class AuthStore {
|
||||
}
|
||||
|
||||
companion object {
|
||||
suspend fun setAuthToken(
|
||||
token: String,
|
||||
context: Context,
|
||||
) { context.authStore.edit { authStore -> authStore[Keys.AUTH_TOKEN] = token } }
|
||||
|
||||
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() }
|
||||
}
|
||||
}
|
||||
) = scope.launch { setAuthToken(token, context) }.run { callback?.invoke() }
|
||||
|
||||
suspend fun getAuthToken(context: Context) =
|
||||
context.authStore.data.map { authStore -> authStore[Keys.AUTH_TOKEN] }.first()
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
) = scope.launch { callback(getAuthToken(context)) }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.example.ssau_schedule.data.store
|
||||
|
||||
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
|
||||
|
||||
val Context.groupStore by preferencesDataStore(name = "group")
|
||||
|
||||
@Serializable
|
||||
data class Group(val id: Int, val name: String)
|
||||
|
||||
class GroupStore {
|
||||
class Keys {
|
||||
companion object {
|
||||
val CURRENT_GROUP_ID = intPreferencesKey("group_id")
|
||||
val CURRENT_GROUP_NAME = stringPreferencesKey("group_name")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
suspend fun setCurrentGroup(
|
||||
group: Group,
|
||||
context: Context,
|
||||
) {
|
||||
context.groupStore.edit { groupStore ->
|
||||
groupStore[Keys.CURRENT_GROUP_ID] = group.id
|
||||
groupStore[Keys.CURRENT_GROUP_NAME] = group.name
|
||||
}
|
||||
}
|
||||
|
||||
fun setCurrentGroup(
|
||||
group: Group,
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
callback: (() -> Unit)? = null
|
||||
) = scope.launch { setCurrentGroup(group, context) }.run { callback?.invoke() }
|
||||
|
||||
suspend fun getCurrentGroup(context: Context): Group? {
|
||||
val currentGroupId = context.groupStore.data
|
||||
.map { groupStore -> groupStore[Keys.CURRENT_GROUP_ID] }.first()
|
||||
val currentGroupName = context.groupStore.data
|
||||
.map { groupStore -> groupStore[Keys.CURRENT_GROUP_NAME] }.first()
|
||||
return if(currentGroupId != null && currentGroupName != null)
|
||||
Group(id = currentGroupId,
|
||||
name = currentGroupName)
|
||||
else null
|
||||
}
|
||||
|
||||
fun getCurrentGroup(
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
callback: (group: Group?) -> Unit
|
||||
) = scope.launch { callback(getCurrentGroup(context)) }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.example.ssau_schedule.data.store
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
data class GeneralData(
|
||||
val token: String,
|
||||
val group: Group,
|
||||
val year: Year
|
||||
)
|
||||
|
||||
class StoreUtils {
|
||||
companion object {
|
||||
suspend fun getGeneralData(
|
||||
context: Context,
|
||||
): GeneralData? {
|
||||
val token = AuthStore.getAuthToken(context)
|
||||
val group = GroupStore.getCurrentGroup(context)
|
||||
val year = YearStore.getCurrentYear(context)
|
||||
return if (token != null && group != null && year != null)
|
||||
GeneralData(token, group, year)
|
||||
else null
|
||||
}
|
||||
|
||||
fun getGeneralData(
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
callback: (data: GeneralData?) -> Unit,
|
||||
) = scope.launch { callback(getGeneralData(context)) }
|
||||
}
|
||||
}
|
||||
103
app/src/main/java/com/example/ssau_schedule/data/store/Year.kt
Normal file
103
app/src/main/java/com/example/ssau_schedule/data/store/Year.kt
Normal file
@@ -0,0 +1,103 @@
|
||||
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 com.example.ssau_schedule.Utils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
|
||||
val Context.yearStore by preferencesDataStore(name = "year")
|
||||
|
||||
data class Year(
|
||||
val id: Int,
|
||||
val startDate: Date,
|
||||
val endDate: 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 = Utils.Date.parse(startDate),
|
||||
endDate = Utils.Date.parse(endDate),
|
||||
)
|
||||
}
|
||||
|
||||
class YearStore {
|
||||
class Keys {
|
||||
companion object {
|
||||
val CURRENT_YEAR_ID = intPreferencesKey("year_id")
|
||||
val CURRENT_YEAR_START = stringPreferencesKey("year_start")
|
||||
val CURRENT_YEAR_END = stringPreferencesKey("year_end")
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
suspend fun setCurrentYear(
|
||||
year: Year,
|
||||
context: Context,
|
||||
) {
|
||||
context.yearStore.edit { yearStore ->
|
||||
yearStore[Keys.CURRENT_YEAR_ID] = year.id
|
||||
yearStore[Keys.CURRENT_YEAR_START] = Utils.Date.storeFormat(year.startDate)
|
||||
yearStore[Keys.CURRENT_YEAR_END] = Utils.Date.storeFormat(year.endDate)
|
||||
}
|
||||
}
|
||||
|
||||
fun setCurrentYear(
|
||||
year: Year,
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
callback: (() -> Unit)? = null
|
||||
) = scope.launch { setCurrentYear(year, context) }.run { callback?.invoke() }
|
||||
|
||||
suspend fun getCurrentYear(context: Context): Year? {
|
||||
val currentYearId = context.yearStore.data
|
||||
.map { yearStore -> yearStore[Keys.CURRENT_YEAR_ID] }.first()
|
||||
val currentYearStartDate = context.yearStore.data
|
||||
.map { yearStore -> yearStore[Keys.CURRENT_YEAR_START]
|
||||
}.first()
|
||||
val currentYearEndDate = context.yearStore.data
|
||||
.map { yearStore -> yearStore[Keys.CURRENT_YEAR_END]
|
||||
}.first()
|
||||
return if(currentYearId != null &&
|
||||
currentYearStartDate != null &&
|
||||
currentYearEndDate != null)
|
||||
Year(id = currentYearId,
|
||||
startDate = Utils.Date.parse(currentYearStartDate),
|
||||
endDate = Utils.Date.parse(currentYearEndDate))
|
||||
else null
|
||||
}
|
||||
|
||||
fun getCurrentYear(
|
||||
context: Context,
|
||||
scope: CoroutineScope,
|
||||
callback: (year: Year?) -> Unit
|
||||
) = scope.launch { callback(getCurrentYear(context)) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.example.ssau_schedule.data.unsaved
|
||||
|
||||
import android.content.Context
|
||||
import com.example.ssau_schedule.R
|
||||
import com.example.ssau_schedule.data.base.entity.lesson.Lesson
|
||||
import com.example.ssau_schedule.data.base.entity.lesson.LessonType
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
enum class LessonConverterErrorMessage(private val resource: Int?) {
|
||||
NO_TEACHER_FOR_LESSON(R.string.failed_get_lessons),
|
||||
NO_DISCIPLINE_FOR_IET_LESSON(R.string.failed_get_lessons);
|
||||
|
||||
fun getMessage(context: Context) =
|
||||
if(resource != null) context.getString(resource) else null
|
||||
}
|
||||
|
||||
@Serializable data class APILessonType(val name: String)
|
||||
@Serializable data class APILessonDiscipline(val name: String)
|
||||
@Serializable data class APILessonTeacher(val name: String)
|
||||
@Serializable data class APILessonTime(val beginTime: String, val endTime: String)
|
||||
@Serializable data class APILessonConference(val url: String)
|
||||
@Serializable data class APILessonFlow(val discipline: APILessonDiscipline)
|
||||
@Serializable data class APILessonWeekDay(val id: Int)
|
||||
|
||||
@Serializable
|
||||
data class APILesson(
|
||||
val id: Int,
|
||||
val type: APILessonType,
|
||||
val discipline: APILessonDiscipline,
|
||||
val teachers: List<APILessonTeacher>,
|
||||
val time: APILessonTime,
|
||||
val conference: APILessonConference?,
|
||||
val weekday: APILessonWeekDay
|
||||
) {
|
||||
fun toLesson(week: Int): Pair<Lesson?, LessonConverterErrorMessage?> {
|
||||
return if(teachers.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_TEACHER_FOR_LESSON)
|
||||
else Pair(Lesson(
|
||||
id = id,
|
||||
type = LessonType.getTypeFromName(type.name),
|
||||
discipline = discipline.name,
|
||||
teacher = teachers[0].name,
|
||||
beginTime = time.beginTime,
|
||||
endTime = time.endTime,
|
||||
conferenceUrl = conference?.url,
|
||||
dayOfWeek = weekday.id,
|
||||
week = week
|
||||
), null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class APIIETLesson(
|
||||
val id: Int,
|
||||
val type: APILessonType,
|
||||
val flows: List<APILessonFlow>,
|
||||
val teachers: List<APILessonTeacher>,
|
||||
val time: APILessonTime,
|
||||
val conference: APILessonConference?,
|
||||
val weekday: APILessonWeekDay
|
||||
) {
|
||||
fun toLesson(week: Int): Pair<Lesson?, LessonConverterErrorMessage?> {
|
||||
return if(teachers.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_TEACHER_FOR_LESSON)
|
||||
else if(flows.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_DISCIPLINE_FOR_IET_LESSON)
|
||||
else Pair(Lesson(
|
||||
id = id,
|
||||
type = LessonType.getTypeFromName(type.name),
|
||||
discipline = flows[0].discipline.name,
|
||||
teacher = teachers[0].name,
|
||||
beginTime = time.beginTime,
|
||||
endTime = time.endTime,
|
||||
conferenceUrl = conference?.url,
|
||||
dayOfWeek = weekday.id,
|
||||
week = week
|
||||
), null)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class APILessons(
|
||||
val lessons: List<APILesson>,
|
||||
val ietLessons: List<APIIETLesson>
|
||||
) {
|
||||
fun toLessons(week: Int): Pair<List<Lesson>, List<LessonConverterErrorMessage>> {
|
||||
val databaseLessons = mutableListOf<Lesson>()
|
||||
val exceptions = mutableListOf<LessonConverterErrorMessage>()
|
||||
lessons.forEach { lesson ->
|
||||
val (databaseLesson, exception) = lesson.toLesson(week)
|
||||
if(databaseLesson != null) databaseLessons.add(databaseLesson)
|
||||
if(exception != null) exceptions.add(exception)
|
||||
}
|
||||
ietLessons.forEach { ietLesson ->
|
||||
val (databaseIetLesson, exception) = ietLesson.toLesson(week)
|
||||
if(databaseIetLesson != null) databaseLessons.add(databaseIetLesson)
|
||||
if(exception != null) exceptions.add(exception)
|
||||
}
|
||||
return Pair(databaseLessons, exceptions)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.example.ssau_schedule.data.unsaved
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class User(val name: String)
|
||||
@@ -16,10 +16,55 @@ class ApplicationColors {
|
||||
val Gray01 = Color(0xFF2C2C2C)
|
||||
val Gray02 = Color(0xFF383838)
|
||||
val Gray03 = Color(0xFF66727F)
|
||||
val Gray04 = Color(0xFFCDCDCD)
|
||||
|
||||
val Red01 = Color(0xFFEE3F58)
|
||||
val Red02 = Color(0xFF7E212E)
|
||||
}
|
||||
}
|
||||
|
||||
class LessonColors {
|
||||
class Background {
|
||||
class Light {
|
||||
companion object {
|
||||
val Lecture = Color(0xFFEAF9F0)
|
||||
val Practice = Color(0xFFDFEEFF)
|
||||
val Laboratory = Color(0xFFFFE2FE)
|
||||
val Other = Color(0xFFFFF0DD)
|
||||
val Examination = Color(0xFFDAE2F4)
|
||||
val Test = Color(0xFFEAEEF2)
|
||||
val Consultation = Color(0xFFD6FAFE)
|
||||
|
||||
val Unknown = Color(0xFFE2E2E2)
|
||||
}
|
||||
}
|
||||
class Dark {
|
||||
companion object {
|
||||
val Lecture = Color(0xFF444946)
|
||||
val Practice = Color(0xFF41464B)
|
||||
val Laboratory = Color(0xFF4B424A)
|
||||
val Other = Color(0xFF4B4641)
|
||||
val Examination = Color(0xFF404247)
|
||||
val Test = Color(0xFF404247)
|
||||
val Consultation = Color(0xFF3E494A)
|
||||
|
||||
val Unknown = Color(0xFF444444)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Foreground {
|
||||
companion object {
|
||||
val Lecture = Color(0xFF16A086)
|
||||
val Practice = Color(0xFF64B5FF)
|
||||
val Laboratory = Color(0xFFDF5FFF)
|
||||
val Other = Color(0xFFF19236)
|
||||
val Examination = Color(0xFF0B40B3)
|
||||
val Test = Color(0xFF5E7EA1)
|
||||
val Consultation = Color(0xFF0BB4BF)
|
||||
|
||||
val Unknown = ApplicationColors.Gray02
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
package com.example.ssau_schedule.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = ApplicationColors.Primary01,
|
||||
secondary = ApplicationColors.Primary03,
|
||||
tertiary = ApplicationColors.Primary04,
|
||||
secondary = ApplicationColors.White,
|
||||
tertiary = ApplicationColors.Gray04,
|
||||
background = ApplicationColors.Gray01,
|
||||
surface = ApplicationColors.Gray02,
|
||||
error = ApplicationColors.Red01,
|
||||
@@ -23,44 +18,20 @@ private val DarkColorScheme = darkColorScheme(
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = ApplicationColors.Primary01,
|
||||
secondary = ApplicationColors.Primary03,
|
||||
tertiary = ApplicationColors.Primary04,
|
||||
secondary = ApplicationColors.Gray01,
|
||||
tertiary = ApplicationColors.Gray03,
|
||||
background = ApplicationColors.White,
|
||||
surface = ApplicationColors.Primary06,
|
||||
error = ApplicationColors.Red01,
|
||||
errorContainer = ApplicationColors.Red02
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
errorContainer = ApplicationColors.Red02,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SSAU_ScheduleTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
//dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
// dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
// val context = LocalContext.current
|
||||
// if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
// }
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
) = MaterialTheme(
|
||||
colorScheme = if(darkTheme) DarkColorScheme else LightColorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
|
||||
@@ -1,34 +1,5 @@
|
||||
package com.example.ssau_schedule.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.SansSerif,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
),
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
val Typography = Typography()
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.example.ssau_schedule.work
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import com.example.ssau_schedule.api.Http
|
||||
import com.example.ssau_schedule.api.LessonAPI
|
||||
import com.example.ssau_schedule.data.base.Database
|
||||
import com.example.ssau_schedule.data.store.StoreUtils
|
||||
|
||||
//class RequestLessonsWorker(
|
||||
// private val context: Context,
|
||||
// private val workerParams: WorkerParameters
|
||||
//): CoroutineWorker(context, workerParams) {
|
||||
// private val notificationManager =
|
||||
// context.getSystemService(Context.NOTIFICATION_SERVICE) as
|
||||
// NotificationManager
|
||||
//
|
||||
// override suspend fun doWork(): Result {
|
||||
// val http = Http()
|
||||
// val lessonAPI = LessonAPI(http)
|
||||
// val database = Database.getInstance(context)
|
||||
//
|
||||
// val generalData = StoreUtils.getGeneralData(context) ?: return Result.failure()
|
||||
// val week = inputData.getInt("week", -1)
|
||||
// if(week == -1) return Result.failure()
|
||||
//
|
||||
//
|
||||
// val (apiLessons, apiErrors) = lessonAPI.getLessons(
|
||||
// generalData.token,
|
||||
// generalData.group,
|
||||
// generalData.year,
|
||||
// week,
|
||||
// )
|
||||
// if(apiErrors != null || apiLessons == null) return Result.failure()
|
||||
//
|
||||
// val (lessons, convertErrors) = apiLessons.toLessons(week)
|
||||
// if(convertErrors.isNotEmpty()) {
|
||||
// var builder = NotificationCompat.Builder(context, "1")
|
||||
// .setContentTitle("Title")
|
||||
// .setContentText("Content")
|
||||
// .setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
// }
|
||||
// database.lessonDao().insert(*lessons.to)
|
||||
// }
|
||||
//}
|
||||
@@ -13,5 +13,10 @@
|
||||
<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>
|
||||
<string name="failed_get_user_details">Failed to retrieve user information. The schedule cannot be obtained without user data</string>
|
||||
<string name="failed_get_lessons">Failed to get schedule</string>
|
||||
<string name="hello">Hello</string>
|
||||
<string name="education_week">education week</string>
|
||||
<string name="education_year">education year</string>
|
||||
<string name="schedule_for_group">Schedule for the group</string>
|
||||
</resources>
|
||||
@@ -12,5 +12,10 @@
|
||||
<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>
|
||||
<string name="failed_get_user_details">Не получилось получить информацию о пользователе. Расписание нельзя получить без пользовательских данных</string>
|
||||
<string name="failed_get_lessons">Не удалось получить расписание</string>
|
||||
<string name="hello">Здравствуйте</string>
|
||||
<string name="education_week">учебная неделя</string>
|
||||
<string name="education_year">учебный год</string>
|
||||
<string name="schedule_for_group">Расписание для группы</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user