This commit is contained in:
2024-09-18 09:06:19 +04:00
parent ec43b774de
commit 0d872c1c5f
34 changed files with 1318 additions and 615 deletions

View File

@@ -4,7 +4,7 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-09-09T18:25:06.130054Z">
<DropdownSelection timestamp="2024-09-10T09:44:45.172790900Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\StepanovPlaton\.android\avd\Medium_Phone_API_30.avd" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -0,0 +1,6 @@
package com.example.ssau_schedule.data.unsaved
import kotlinx.serialization.Serializable
@Serializable
data class User(val name: String)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,4 +4,6 @@ plugins {
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.devtools.ksp) apply false
alias(libs.plugins.androidx.room) apply false
}

View File

@@ -12,6 +12,8 @@ okhttp = "4.12.0"
datastore = "1.1.1"
media3Common = "1.4.1"
kotlinSerializationJson = "1.7.1"
room = "2.6.1"
workRuntimeKtx = "2.9.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -32,9 +34,15 @@ squareup-okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref
androidx-datastore = { group = "androidx.datastore", name="datastore-preferences", version.ref = "datastore" }
androidx-media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3Common" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinSerializationJson" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workRuntimeKtx" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
devtools-ksp = { id = "com.google.devtools.ksp", version = "2.0.20-1.0.24" }
androidx-room = { id = "androidx.room", version.ref = "room" }