Complete work

This commit is contained in:
2024-12-15 20:39:36 +04:00
parent 5b73de7830
commit 31b9d4f631
31 changed files with 767 additions and 390 deletions

3
.idea/misc.xml generated
View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@@ -73,6 +73,9 @@ android {
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
}
kotlinOptions {
jvmTarget = "1.8"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
@@ -114,4 +117,7 @@ dependencies {
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.glance)
implementation(libs.androidx.glance.material)
}

View File

@@ -1,82 +0,0 @@
{
"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

@@ -6,7 +6,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application
android:allowBackup="true"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
@@ -32,5 +32,16 @@
android:theme="@style/Theme.SSAU_Schedule"
android:screenOrientation="portrait">
</activity>
<receiver
android:name=".widget.WidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
</application>
</manifest>

View File

@@ -110,7 +110,7 @@ class AuthActivity : ComponentActivity() {
var entered by remember { mutableStateOf(false) }
val keyboardOpen by Utils.keyboardState()
val snackbarHostState = remember { SnackbarHostState() }
val notificationState = remember { SnackbarHostState() }
val logoHeight by animateFloatAsState(
if (keyboardOpen && needAuth) 0f else min(
LocalConfiguration.current.screenWidthDp,
@@ -124,7 +124,7 @@ class AuthActivity : ComponentActivity() {
)
LaunchedEffect(user, group, year) {
if(user != null && group != null && year != null) {
if (user != null && group != null && year != null) {
delay(2500)
startActivity(Intent(applicationContext, MainActivity::class.java))
}
@@ -134,22 +134,30 @@ class AuthActivity : ComponentActivity() {
delay(3000)
val token = AuthStore.getAuthToken(applicationContext)
if(token == null) { needAuth = true; return@LaunchedEffect }
if (token == null) {
needAuth = true; return@LaunchedEffect
}
val (userDetails) = userAPI.getUserDetails(token)
if(userDetails == null) { needAuth = true; return@LaunchedEffect }
else { user = userDetails }
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) {
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 }
if (message != null) notificationState.showSnackbar(message)
} else {
needAuth = true; return@LaunchedEffect
}
} else {
val currentGroup = GroupStore.getCurrentGroup(applicationContext)
if(currentGroup != null && groups.contains(currentGroup)) group = currentGroup
if (currentGroup != null && groups.contains(currentGroup)) group = currentGroup
else {
GroupStore.setCurrentGroup(groups[0], applicationContext)
group = groups[0]
@@ -157,28 +165,31 @@ class AuthActivity : ComponentActivity() {
}
val (years, yearsError) = yearAPI.getYears(token)
if(years == null) {
if(yearsError != null && yearsError !=
YearAPIErrorMessage.USER_NOT_AUTHORIZED) {
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 }
if (message != null) notificationState.showSnackbar(message)
} else {
needAuth = true; return@LaunchedEffect
}
} else {
val currentRawYear = years.find { y -> y.isCurrent }
if(currentRawYear != null) {
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)
if (message != null) notificationState.showSnackbar(message)
}
}
}
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState) {
SnackbarHost(hostState = notificationState) {
Snackbar(
snackbarData = it,
containerColor = MaterialTheme.colorScheme.errorContainer,
@@ -187,8 +198,12 @@ class AuthActivity : ComponentActivity() {
}
}
) { padding ->
Box(Modifier.background(MaterialTheme.colorScheme.primary)
.fillMaxSize().padding(padding).imePadding(),
Box(
Modifier
.background(MaterialTheme.colorScheme.primary)
.fillMaxSize()
.padding(padding)
.imePadding(),
contentAlignment = BiasAlignment(0f, -0.25f),
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
@@ -196,19 +211,23 @@ class AuthActivity : ComponentActivity() {
Modifier
.widthIn(0.dp, 500.dp)
.height(logoHeight.dp)
.padding(20.dp, 0.dp)) {
Image(painterResource(R.drawable.ssau_logo_01),
.padding(20.dp, 0.dp)
) {
Image(
painterResource(R.drawable.ssau_logo_01),
contentDescription = stringResource(R.string.samara_university),
modifier = Modifier
.fillMaxSize()
.padding(10.dp),
contentScale = ContentScale.FillWidth,
alignment = Alignment.TopCenter)
alignment = Alignment.TopCenter
)
}
Box(
Modifier
.padding(20.dp, 0.dp)
.widthIn(0.dp, 400.dp)) {
.widthIn(0.dp, 400.dp)
) {
Column {
WelcomeMessage(user, group, year)
AuthForm(open = needAuth, authScope) {
@@ -230,26 +249,37 @@ class AuthActivity : ComponentActivity() {
var password by remember { mutableStateOf("") }
var error by remember { mutableStateOf<AuthErrorMessage?>(null) }
val height by animateDpAsState(if (open) 290.dp else 0.dp, label = "Auth form height",
val height by animateDpAsState(
if (open) 290.dp else 0.dp, label = "Auth form height",
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow
)
)
Card(Modifier.fillMaxWidth().height(height).padding(0.dp, 10.dp).shadow(10.dp),
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),
Text(
stringResource(R.string.sign_in),
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.primary,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.displaySmall)
style = MaterialTheme.typography.displaySmall
)
OutlinedTextField(modifier = Modifier.fillMaxWidth(),
value = login,
onValueChange = { login = it; error = null },
@@ -262,14 +292,19 @@ 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
) {
Text(error?.getMessage(applicationContext) ?: "",
Text(
error?.getMessage(applicationContext) ?: "",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.labelSmall)
style = MaterialTheme.typography.labelSmall
)
}
}
Spacer(Modifier.height(4.dp))
@@ -279,11 +314,10 @@ class AuthActivity : ComponentActivity() {
else if (password.length < 5) error = AuthErrorMessage.PASSWORD_IS_TOO_SHORT
else scope.launch {
val (token) = authAPI.signIn(login, password)
if(token != null) {
if (token != null) {
AuthStore.setAuthToken(token, applicationContext)
callback()
}
else error = AuthErrorMessage.INCORRECT_LOGIN_OR_PASSWORD
} else error = AuthErrorMessage.INCORRECT_LOGIN_OR_PASSWORD
}
},
shape = RoundedCornerShape(50),
@@ -298,26 +332,35 @@ class AuthActivity : ComponentActivity() {
@SuppressLint("SimpleDateFormat")
@Composable
fun WelcomeMessage(user: User?, group: Group?, year: Year?) {
val currentDate = remember { SimpleDateFormat("d MMMM").format(Date()) }
val currentDate = remember { SimpleDateFormat("d MMMM").format(Date()) }
val currentYear = remember { Calendar.getInstance().get(Calendar.YEAR); }
Column(Modifier.fillMaxWidth().animateContentSize(),
Column(
Modifier
.fillMaxWidth()
.animateContentSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
if(user !== null && group != null && year != null) {
Text("${stringResource(R.string.hello)} ${user.name}!",
if (user !== null && group != null && year != null) {
Text(
"${stringResource(R.string.hello)} ${user.name}!",
color = ApplicationColors.White,
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center)
Text("${stringResource(R.string.schedule_for_group)} ${group.name}",
textAlign = TextAlign.Center
)
Text(
"${stringResource(R.string.schedule_for_group)} ${group.name}",
color = ApplicationColors.White,
style = MaterialTheme.typography.titleSmall,
textAlign = TextAlign.Center)
Text("$currentDate, ${year.getWeekOfDate(Date())} "+
"${stringResource(R.string.education_week)}, ${currentYear}-"+
"${currentYear+1} ${stringResource(R.string.education_year)}",
textAlign = TextAlign.Center
)
Text(
"$currentDate, ${year.getWeekOfDate(Date())} " +
"${stringResource(R.string.education_week)}, ${currentYear}-" +
"${currentYear + 1} ${stringResource(R.string.education_year)}",
color = ApplicationColors.White,
style = MaterialTheme.typography.labelSmall,
textAlign = TextAlign.Center)
textAlign = TextAlign.Center
)
}
}
}

View File

@@ -2,7 +2,6 @@ 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
@@ -36,18 +35,18 @@ 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.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
import androidx.glance.appwidget.updateAll
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.example.ssau_schedule.api.Http
import com.example.ssau_schedule.api.LessonAPI
import com.example.ssau_schedule.api.LessonAPIErrorMessage
@@ -55,18 +54,24 @@ import com.example.ssau_schedule.components.EmptyDay
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.GeneralData
import com.example.ssau_schedule.data.store.StoreUtils
import com.example.ssau_schedule.data.store.Year
import com.example.ssau_schedule.ui.theme.SSAU_ScheduleTheme
import com.example.ssau_schedule.widget.ScheduleWidget
import com.example.ssau_schedule.work.RequestLessonsWorker
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.util.Date
import java.util.concurrent.TimeUnit
class MainActivity : ComponentActivity() {
private val http = Http()
private val lessonAPI = LessonAPI(http)
private lateinit var database: Database
private lateinit var workManager: WorkManager
private val workName = "SSAUSchedule"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
@@ -81,51 +86,73 @@ class MainActivity : ComponentActivity() {
@Composable
fun MainPage() {
database = remember { Database.getInstance(applicationContext) }
val snackbarHostState = remember { SnackbarHostState() }
workManager = remember { WorkManager.getInstance(applicationContext) }
val notificationState = remember { SnackbarHostState() }
val lessons = remember { mutableStateOf<List<Lesson>>(listOf()) }
val animationScope = rememberCoroutineScope()
val currentDate = remember { mutableStateOf(Date()) }
val currentDayOfWeek = remember {
mutableIntStateOf(Utils.Date.getDayOfWeek(currentDate.value))
}
val pagerState = rememberPagerState(
initialPage = currentDayOfWeek.intValue-1, pageCount = {Int.MAX_VALUE})
initialPage = Utils.Date.getDayOfWeek(Date()) - 1, pageCount = { Int.MAX_VALUE })
val studyYear = remember { mutableStateOf<Year?>(null) }
val loadedWeeks = remember { mutableStateOf<List<Int>>(listOf()) }
val workStarted = remember { mutableStateOf(false) }
// LaunchedEffect(false) {
// lessons.value = database.lessonDao().getAll()
// }
suspend fun getLessons(generalData: GeneralData, week: Int) {
val (apiLessons, apiError) = lessonAPI.getLessons(
generalData.token, generalData.group, generalData.year, week
)
if (apiLessons != null && apiError == null) {
val (databaseLessons, converterErrors) = apiLessons.toLessons(week)
database.lessonDao().insert(*databaseLessons.toTypedArray())
converterErrors.forEach { error ->
val message = error.getMessage(applicationContext)
if (message != null) notificationState.showSnackbar(message)
}
lessons.value = lessons.value.plus(databaseLessons)
loadedWeeks.value = loadedWeeks.value.plus(week)
ScheduleWidget().updateAll(applicationContext)
} else {
if (apiError == LessonAPIErrorMessage.USER_NOT_AUTHORIZED) {
startActivity(Intent(applicationContext, AuthActivity::class.java))
} else {
val message = apiError?.getMessage(applicationContext)
if (message != null) notificationState.showSnackbar(message)
}
}
}
LaunchedEffect(false) {
LaunchedEffect(pagerState.currentPage) {
val generalData = StoreUtils.getGeneralData(applicationContext)
if(generalData == null)
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)
}
if (!workStarted.value) {
val workRequest = PeriodicWorkRequestBuilder<RequestLessonsWorker>(
repeatInterval = 3,
TimeUnit.HOURS
).setInitialDelay(1, TimeUnit.HOURS).build()
workManager.enqueueUniquePeriodicWork(
workName,
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
workRequest
)
workStarted.value = true
}
studyYear.value = generalData.year
val day = Utils.Date.addDays(
Date(),
pagerState.currentPage - Utils.Date.getDayOfWeek(Date())+1
)
var week = generalData.year.getWeekOfDate(day)
if (!loadedWeeks.value.contains(week)) getLessons(generalData, week)
if (Utils.Date.getDayOfWeek(day) == 6) week++
if (!loadedWeeks.value.contains(week)) getLessons(generalData, week)
}
}
Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState) {
SnackbarHost(hostState = notificationState) {
Snackbar(
snackbarData = it,
containerColor = MaterialTheme.colorScheme.errorContainer,
@@ -142,68 +169,102 @@ class MainActivity : ComponentActivity() {
.imePadding(),
) {
Column(Modifier.fillMaxHeight()) {
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)
.clickable {
animationScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage-1)
}
},
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)
.clickable {
animationScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage - 1)
}
},
) {
Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft,
Icon(
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
contentDescription = "Forward icon",
Modifier.fillMaxSize(),
tint = MaterialTheme.colorScheme.primary)
tint = MaterialTheme.colorScheme.primary
)
}
Box(Modifier.height(40.dp)
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
.background(MaterialTheme.colorScheme.surface),
Box(
Modifier
.height(40.dp)
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
.background(MaterialTheme.colorScheme.surface),
) {
Row(Modifier.fillMaxHeight().padding(10.dp, 0.dp),
Row(
Modifier
.fillMaxHeight()
.padding(10.dp, 0.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.DateRange,
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(
Utils.Date.addDays(currentDate.value,
pagerState.currentPage-currentDayOfWeek.intValue)
Modifier
.height(40.dp)
.padding(0.dp, 0.dp, 10.dp, 0.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
Utils.Date.format(
Utils.Date.addDays(
Date(),
pagerState.currentPage - Utils.Date.getDayOfWeek(
Date()
)+1
)
),
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.bodyLarge)
style = MaterialTheme.typography.bodyLarge
)
}
}
Box(Modifier.height(40.dp).width(40.dp)
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
.background(MaterialTheme.colorScheme.surface)
.clickable {
animationScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage+1)
}
},
Box(
Modifier
.height(40.dp)
.width(40.dp)
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
.background(MaterialTheme.colorScheme.surface)
.clickable {
animationScope.launch {
pagerState.animateScrollToPage(pagerState.currentPage + 1)
}
},
) {
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight,
Icon(
Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "Forward icon",
Modifier.fillMaxSize(),
tint = MaterialTheme.colorScheme.primary)
tint = MaterialTheme.colorScheme.primary
)
}
}
}
HorizontalDivider(Modifier.padding(20.dp, 0.dp))
HorizontalPager(state = pagerState) { page ->
val todayLessons = lessons.value.filter { lesson ->
lesson.dayOfWeek-1 == Utils.Date.getDayOfWeek(
Utils.Date.addDays(currentDate.value, page-currentDayOfWeek.intValue)) &&
lesson.dayOfWeek - 1 == Utils.Date.getDayOfWeek(
Utils.Date.addDays(Date(), page - Utils.Date.getDayOfWeek(Date())+1)
) &&
lesson.week == Utils.Date.getWeekOfStudyYear(
Utils.Date.addDays(currentDate.value, page-currentDayOfWeek.intValue))
Utils.Date.addDays(Date(), page - Utils.Date.getDayOfWeek(Date())+1)
)
}.sortedBy { lesson -> lesson.beginTime }
if(todayLessons.isEmpty())
if (todayLessons.isEmpty())
EmptyDay(Modifier)
else
LessonCards(todayLessons)

View File

@@ -2,7 +2,6 @@ package com.example.ssau_schedule
import android.annotation.SuppressLint
import android.graphics.Rect
import android.util.Log
import android.view.ViewTreeObserver
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -12,7 +11,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalView
import kotlinx.serialization.json.Json
import java.text.SimpleDateFormat
import java.time.LocalDate
import java.util.Calendar
class Utils {
@@ -45,6 +43,7 @@ class Utils {
companion object {
@SuppressLint("SimpleDateFormat")
val StoreDateFormat = SimpleDateFormat("yyyy-MM-dd")
@SuppressLint("SimpleDateFormat")
val DateFormat = SimpleDateFormat("dd MMMM")
@@ -55,21 +54,23 @@ class Utils {
fun getDayOfWeek(date: java.util.Date): Int {
val calendar = Calendar.getInstance()
calendar.time = date
return (calendar.get(Calendar.DAY_OF_WEEK)+5)%7
return (calendar.get(Calendar.DAY_OF_WEEK) + 5) % 7
}
private fun getWeekOfYear(date: java.util.Date): Int {
val calendar = Calendar.getInstance()
calendar.time = date
return calendar.get(Calendar.WEEK_OF_YEAR) -
(if(calendar.get(Calendar.DAY_OF_WEEK) == 1) 1 else 0)
(if (calendar.get(Calendar.DAY_OF_WEEK) == 0) 1 else 0)
}
fun getWeekOfStudyYear(date: java.util.Date): Int {
val calendar = Calendar.getInstance()
calendar.time = java.util.Date()
val year = calendar.get(Calendar.YEAR)
calendar.time = parse("${year}-09-01")
return getWeekOfYear(date) - (calendar.get(Calendar.WEEK_OF_YEAR) -
(if(calendar.get(Calendar.DAY_OF_WEEK) == 1) 1 else 0))
(if (calendar.get(Calendar.DAY_OF_WEEK) == 0) 1 else 0))
}
fun addDays(date: java.util.Date, days: Int): java.util.Date {

View File

@@ -8,7 +8,6 @@ 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?) {
LOGIN_IS_TOO_SHORT(R.string.login_is_too_short),
@@ -16,7 +15,7 @@ enum class AuthErrorMessage(private val resource: Int?) {
INCORRECT_LOGIN_OR_PASSWORD(R.string.incorrect_login_or_password);
fun getMessage(context: Context) =
if(resource != null) context.getString(resource) else null
if (resource != null) context.getString(resource) else null
}
class AuthorizationAPI(private var http: Http) {
@@ -39,8 +38,9 @@ class AuthorizationAPI(private var http: Http) {
).toString().toRequestBody("application/json".toMediaType()),
mapOf(
Pair("Next-Action", "b395d17834d8b7df06372cbf1f241170a272d540")
).toHeaders())
val token = if(response?.headers?.toMap()?.containsKey("set-cookie") == true)
).toHeaders()
)
val token = if (response?.headers?.toMap()?.containsKey("set-cookie") == true)
response.headers("set-cookie").joinToString(", ") else null
return Pair(token, exception)
}

View File

@@ -15,7 +15,7 @@ enum class GroupAPIErrorMessage(private val resource: Int?) {
USER_NOT_AUTHORIZED(null);
fun getMessage(context: Context) =
if(resource != null) context.getString(resource) else null
if (resource != null) context.getString(resource) else null
}
class GroupAPI(private var http: Http) {
@@ -25,9 +25,10 @@ class GroupAPI(private var http: Http) {
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)
).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

View File

@@ -43,8 +43,12 @@ class Http {
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful)
coroutine.resume(Pair(response,
HttpRequestException("Http response is not successful")))
coroutine.resume(
Pair(
response,
HttpRequestException("Http response is not successful")
)
)
else coroutine.resume(Pair(response, null))
}
})

View File

@@ -5,9 +5,9 @@ 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 com.example.ssau_schedule.data.unsaved.APILessons
import kotlinx.serialization.SerializationException
import okhttp3.Headers.Companion.toHeaders
@@ -17,7 +17,7 @@ enum class LessonAPIErrorMessage(private val resource: Int?) {
USER_NOT_AUTHORIZED(null);
fun getMessage(context: Context) =
if(resource != null) context.getString(resource) else null
if (resource != null) context.getString(resource) else null
}
class LessonAPI(private var http: Http) {
@@ -29,16 +29,20 @@ class LessonAPI(private var http: Http) {
): Pair<APILessons?, LessonAPIErrorMessage?> {
val (response) = http.request(
Method.GET,
"${BuildConfig.LESSONS_URL}?yearId=${year.id}"+
"${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) {
).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) {

View File

@@ -14,7 +14,7 @@ enum class UserAPIErrorMessage(private val resource: Int?) {
USER_NOT_AUTHORIZED(null);
fun getMessage(context: Context) =
if(resource != null) context.getString(resource) else null
if (resource != null) context.getString(resource) else null
}
class UserAPI(private var http: Http) {
@@ -26,12 +26,13 @@ class UserAPI(private var http: Http) {
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)
).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) {
} catch (e: SerializationException) {
Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS)
} catch (e: IllegalArgumentException) {
Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS)

View File

@@ -14,7 +14,7 @@ enum class YearAPIErrorMessage(private val resource: Int?) {
USER_NOT_AUTHORIZED(null);
fun getMessage(context: Context) =
if(resource != null) context.getString(resource) else null
if (resource != null) context.getString(resource) else null
}
class YearAPI(private var http: Http) {
@@ -26,19 +26,18 @@ class YearAPI(private var http: Http) {
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)
).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)
return if (rawYears.isNotEmpty()) Pair(rawYears, null)
else Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
}
catch(e: SerializationException) {
} catch (e: SerializationException) {
return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
}
catch (e: IllegalArgumentException) {
} catch (e: IllegalArgumentException) {
return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
}
}

View File

@@ -1,41 +1,50 @@
package com.example.ssau_schedule.components
import androidx.compose.foundation.border
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
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.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.ssau_schedule.R
import com.example.ssau_schedule.ui.theme.LessonColors
@Composable
fun EmptyDay(modifier: Modifier) {
Box(Modifier.fillMaxHeight().fillMaxWidth()) {
Row(modifier.fillMaxWidth()
.padding(14.dp, 8.dp)
.shadow(elevation = 6.dp, shape = RoundedCornerShape(12.dp))
.background(
if(isSystemInDarkTheme()) LessonColors.Background.Dark.Unknown
else LessonColors.Background.Light.Unknown
),
Box(
Modifier
.fillMaxHeight()
.fillMaxWidth()) {
Row(
modifier
.fillMaxWidth()
.padding(14.dp, 8.dp)
.shadow(elevation = 6.dp, shape = RoundedCornerShape(12.dp))
.background(
if (isSystemInDarkTheme()) LessonColors.Background.Dark.Unknown
else LessonColors.Background.Light.Unknown
),
) {
AutoResizeText("Сегодня нет занятий",
modifier = modifier.fillMaxWidth().padding(14.dp),
AutoResizeText(
stringResource(R.string.no_classes_today),
modifier = modifier
.fillMaxWidth()
.padding(14.dp),
fontSizeRange = FontSizeRange(10.sp, 24.sp),
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center)
textAlign = TextAlign.Center
)
}
}
}

View File

@@ -25,44 +25,69 @@ 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
)
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}",
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",
style = MaterialTheme.typography.bodyMedium
)
Text(
"${lesson.room ?: "???"} - ${lesson.building ?: "?"}",
color = MaterialTheme.colorScheme.tertiary,
style = MaterialTheme.typography.bodyMedium)
style = MaterialTheme.typography.bodyMedium
)
}
AutoResizeText(lesson.discipline,
AutoResizeText(
lesson.discipline,
modifier = modifier.fillMaxWidth(),
fontSizeRange = FontSizeRange(10.sp, 24.sp),
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleLarge)
Text(lesson.teacher,
style = MaterialTheme.typography.titleLarge
)
Text(
lesson.teacher,
modifier = modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleSmall)
style = MaterialTheme.typography.titleSmall
)
}
}
}
@Composable
fun LessonCards(lessons: List<Lesson>) {
Box(Modifier.fillMaxHeight().fillMaxWidth()) {
Box(
Modifier
.fillMaxHeight()
.fillMaxWidth()) {
LazyColumn {
items(lessons.count()) {
LessonCard(Modifier, lessons[it])

View File

@@ -1,7 +1,6 @@
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
@@ -11,14 +10,17 @@ 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
@TypeConverter
fun toLessonType(value: String) = LessonType.getTypeFromName(value)
@TypeConverter
fun fromLessonType(value: LessonType) = value.displayName
}
@androidx.room.Database(
entities = [Lesson::class],
version = 1,
autoMigrations = [])
autoMigrations = []
)
@TypeConverters(Converters::class)
abstract class Database : RoomDatabase() {
abstract fun lessonDao(): LessonDao

View File

@@ -13,34 +13,48 @@ enum class LessonType(
val darkBackground: Color,
val lightBackground: Color,
) {
LECTURE("Лекция",
LECTURE(
"Лекция",
LessonColors.Foreground.Lecture,
LessonColors.Background.Dark.Lecture,
LessonColors.Background.Light.Lecture),
PRACTICE("Практика",
LessonColors.Background.Light.Lecture
),
PRACTICE(
"Практика",
LessonColors.Foreground.Practice,
LessonColors.Background.Dark.Practice,
LessonColors.Background.Light.Practice),
LABORATORY("Лабораторная",
LessonColors.Background.Light.Practice
),
LABORATORY(
"Лабораторная",
LessonColors.Foreground.Laboratory,
LessonColors.Background.Dark.Laboratory,
LessonColors.Background.Light.Laboratory),
OTHER("Другое",
LessonColors.Background.Light.Laboratory
),
OTHER(
"Другое",
LessonColors.Foreground.Other,
LessonColors.Background.Dark.Other,
LessonColors.Background.Light.Other),
EXAMINATION("Экзамен",
LessonColors.Background.Light.Other
),
EXAMINATION(
"Экзамен",
LessonColors.Foreground.Examination,
LessonColors.Background.Dark.Examination,
LessonColors.Background.Light.Examination),
TEST("Зачёт",
LessonColors.Background.Light.Examination
),
TEST(
"Зачёт",
LessonColors.Foreground.Test,
LessonColors.Background.Dark.Test,
LessonColors.Background.Light.Test),
CONSULTATION("Консультация",
LessonColors.Background.Light.Test
),
CONSULTATION(
"Консультация",
LessonColors.Foreground.Consultation,
LessonColors.Background.Dark.Consultation,
LessonColors.Background.Light.Consultation);
LessonColors.Background.Light.Consultation
);
companion object {
fun getTypeFromName(name: String) =
@@ -60,4 +74,6 @@ data class Lesson(
@ColumnInfo(name = "begin_time") val beginTime: String,
@ColumnInfo(name = "end_time") val endTime: String,
@ColumnInfo(name = "conference_url") val conferenceUrl: String?,
@ColumnInfo(name = "building") val building: String?,
@ColumnInfo(name = "room") val room: String?,
)

View File

@@ -22,7 +22,9 @@ class AuthStore {
suspend fun setAuthToken(
token: String,
context: Context,
) { context.authStore.edit { authStore -> authStore[Keys.AUTH_TOKEN] = token } }
) {
context.authStore.edit { authStore -> authStore[Keys.AUTH_TOKEN] = token }
}
fun setAuthToken(
token: String,

View File

@@ -47,9 +47,11 @@ class GroupStore {
.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)
return if (currentGroupId != null && currentGroupName != null)
Group(
id = currentGroupId,
name = currentGroupName
)
else null
}

View File

@@ -3,8 +3,6 @@ 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,

View File

@@ -30,7 +30,7 @@ data class Year(
calendar.time = startDate
val firstWeek = calendar.get(Calendar.WEEK_OF_YEAR)
calendar.time = date
return (calendar.get(Calendar.WEEK_OF_YEAR) - firstWeek)+1
return (calendar.get(Calendar.WEEK_OF_YEAR) - firstWeek) + 1
}
}
@@ -57,6 +57,7 @@ class YearStore {
val CURRENT_YEAR_END = stringPreferencesKey("year_end")
}
}
companion object {
suspend fun setCurrentYear(
year: Year,
@@ -80,17 +81,22 @@ class YearStore {
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]
.map { yearStore ->
yearStore[Keys.CURRENT_YEAR_START]
}.first()
val currentYearEndDate = context.yearStore.data
.map { yearStore -> yearStore[Keys.CURRENT_YEAR_END]
.map { yearStore ->
yearStore[Keys.CURRENT_YEAR_END]
}.first()
return if(currentYearId != null &&
return if (currentYearId != null &&
currentYearStartDate != null &&
currentYearEndDate != null)
Year(id = currentYearId,
currentYearEndDate != null
)
Year(
id = currentYearId,
startDate = Utils.Date.parse(currentYearStartDate),
endDate = Utils.Date.parse(currentYearEndDate))
endDate = Utils.Date.parse(currentYearEndDate)
)
else null
}

View File

@@ -11,16 +11,34 @@ enum class LessonConverterErrorMessage(private val resource: Int?) {
NO_DISCIPLINE_FOR_IET_LESSON(R.string.failed_get_lessons);
fun getMessage(context: Context) =
if(resource != null) context.getString(resource) else null
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 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 APILessonBuilding(val name: String)
@Serializable
data class APILessonRoom(val name: String)
@Serializable
data class APILessonWeeks(
val building: APILessonBuilding,
val room: APILessonRoom,
val week: Int
)
@Serializable
data class APILesson(
@@ -30,21 +48,27 @@ data class APILesson(
val teachers: List<APILessonTeacher>,
val time: APILessonTime,
val conference: APILessonConference?,
val weekday: APILessonWeekDay
val weekday: APILessonWeekDay,
val weeks: List<APILessonWeeks>
) {
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)
val weekInfo = weeks.find { w -> w.week == week }
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,
building = weekInfo?.building?.name,
room = weekInfo?.room?.name
), null
)
}
}
@@ -57,22 +81,31 @@ data class APIIETLesson(
val teachers: List<APILessonTeacher>,
val time: APILessonTime,
val conference: APILessonConference?,
val weekday: APILessonWeekDay
val weekday: APILessonWeekDay,
val weeks: List<APILessonWeeks>
) {
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)
val weekInfo = weeks.find { w -> w.week == week }
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,
building = weekInfo?.building?.name,
room = weekInfo?.room?.name
), null
)
}
}
@@ -86,13 +119,13 @@ data class APILessons(
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)
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)
if (databaseIetLesson != null) databaseLessons.add(databaseIetLesson)
if (exception != null) exceptions.add(exception)
}
return Pair(databaseLessons, exceptions)
}

View File

@@ -38,6 +38,7 @@ class LessonColors {
val Unknown = Color(0xFFE2E2E2)
}
}
class Dark {
companion object {
val Lecture = Color(0xFF444946)

View File

@@ -5,6 +5,9 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.glance.GlanceTheme
import androidx.glance.color.ColorProviders
import androidx.glance.material3.ColorProviders
private val DarkColorScheme = darkColorScheme(
primary = ApplicationColors.Primary01,
@@ -31,7 +34,16 @@ fun SSAU_ScheduleTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) = MaterialTheme(
colorScheme = if(darkTheme) DarkColorScheme else LightColorScheme,
colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme,
typography = Typography,
content = content
)
@Composable
fun SSAU_ScheduleWidgetTheme(
colors: ColorProviders = ColorProviders(light = LightColorScheme, dark = DarkColorScheme),
content: @Composable () -> Unit
) = GlanceTheme(
colors = colors,
content = content
)

View File

@@ -0,0 +1,65 @@
package com.example.ssau_schedule.widget
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.appwidget.cornerRadius
import androidx.glance.background
import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxHeight
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.height
import androidx.glance.layout.padding
import androidx.glance.layout.width
import androidx.glance.text.Text
import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle
import com.example.ssau_schedule.data.base.entity.lesson.Lesson
import com.example.ssau_schedule.ui.theme.LessonColors
@Composable
fun LessonCard(lesson: Lesson) {
Row(
GlanceModifier.fillMaxWidth()
.cornerRadius(12.dp)
.background(lesson.type?.darkBackground ?: LessonColors.Background.Dark.Unknown)
) {
Box(
GlanceModifier.fillMaxHeight().width(10.dp)
.background(lesson.type?.foreground ?: LessonColors.Foreground.Unknown)
) { }
Column(GlanceModifier.fillMaxHeight().padding(10.dp, 10.dp)) {
Text(
"${lesson.beginTime} - ${lesson.endTime}, ${lesson.room ?: "???"} - ${lesson.building ?: "?"}",
style = TextStyle(
textAlign = TextAlign.Start,
fontSize = 14.sp, color = GlanceTheme.colors.tertiary
)
)
Spacer(GlanceModifier.fillMaxWidth().height(4.dp))
Text(
lesson.discipline,
modifier = GlanceModifier.fillMaxWidth(),
style = TextStyle(fontSize = 20.sp, color = GlanceTheme.colors.secondary)
)
Spacer(GlanceModifier.fillMaxWidth().height(4.dp))
Text(
lesson.teacher,
modifier = GlanceModifier.fillMaxWidth(),
style = TextStyle(fontSize = 12.sp, color = GlanceTheme.colors.tertiary)
)
}
}
}
@Composable
fun LessonCards(lessons: List<Lesson>) {
lessons.forEach { lesson ->
LessonCard(lesson)
}
}

View File

@@ -0,0 +1,104 @@
package com.example.ssau_schedule.widget
import android.annotation.SuppressLint
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
import androidx.glance.appwidget.cornerRadius
import androidx.glance.appwidget.lazy.LazyColumn
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.height
import androidx.glance.layout.padding
import androidx.glance.text.Text
import androidx.glance.text.TextAlign
import androidx.glance.text.TextStyle
import com.example.ssau_schedule.R
import com.example.ssau_schedule.Utils
import com.example.ssau_schedule.data.base.Database
import com.example.ssau_schedule.data.base.entity.lesson.Lesson
import com.example.ssau_schedule.ui.theme.LessonColors
import com.example.ssau_schedule.ui.theme.SSAU_ScheduleWidgetTheme
import java.text.SimpleDateFormat
import java.util.Date
class ScheduleWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
SSAU_ScheduleWidgetTheme {
WidgetContent(context)
}
}
}
@SuppressLint("SimpleDateFormat")
@Composable
private fun WidgetContent(context: Context) {
val database = remember { Database.getInstance(context) }
val lessons = remember { mutableStateOf<List<Lesson>>(listOf()) }
LaunchedEffect(false) {
lessons.value = database.lessonDao().getAll()
}
Box(GlanceModifier.fillMaxSize().background(GlanceTheme.colors.surface)) {
LazyColumn {
items(7) {
Column {
val todayLessons = lessons.value.filter { lesson ->
lesson.dayOfWeek - 1 == Utils.Date.getDayOfWeek(
Utils.Date.addDays(
Date(),
it
)
) &&
lesson.week == Utils.Date.getWeekOfStudyYear(Date())
}.sortedBy { lesson -> lesson.beginTime }
Box(
GlanceModifier.fillMaxWidth()
.background(LessonColors.Background.Dark.Unknown)
.padding(20.dp, 10.dp).cornerRadius(12.dp)
) {
Text(
SimpleDateFormat("d MMMM").format(Utils.Date.addDays(
Date(),
it
)) + if(todayLessons.isEmpty()) " - "+context.getString(R.string.no_classes) else "",
modifier = GlanceModifier.fillMaxWidth(),
style = TextStyle(
fontSize = 14.sp,
textAlign = TextAlign.Left,
color = GlanceTheme.colors.tertiary
)
)
}
if(todayLessons.isNotEmpty()) LessonCards(todayLessons)
Spacer(GlanceModifier.fillMaxWidth().height(10.dp))
}
}
}
}
}
}
class WidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = ScheduleWidget()
}

View File

@@ -1,50 +1,83 @@
package com.example.ssau_schedule.work
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.glance.appwidget.updateAll
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import com.example.ssau_schedule.R
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
import com.example.ssau_schedule.widget.ScheduleWidget
import java.util.Date
//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)
// }
//}
class RequestLessonsWorker(
private val context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
private val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as
NotificationManager
private val channelId = "ssau_schedule_1"
private val notificationId = 1234
@RequiresApi(Build.VERSION_CODES.O)
override suspend fun doWork(): Result {
val mChannel = NotificationChannel(
channelId,
"SSAUScheduleNotificationChannel", NotificationManager.IMPORTANCE_HIGH
)
mChannel.enableLights(true)
mChannel.enableVibration(true)
notificationManager.createNotificationChannel(mChannel)
val http = Http()
val lessonAPI = LessonAPI(http)
val database = Database.getInstance(context)
val generalData = StoreUtils.getGeneralData(context)
if (generalData == null) {
pushErrorNotification()
return Result.failure()
}
val week = generalData.year.getWeekOfDate(Date())
val (apiLessons, apiErrors) = lessonAPI.getLessons(
generalData.token,
generalData.group,
generalData.year,
week,
)
if (apiErrors != null || apiLessons == null) {
pushErrorNotification()
return Result.failure()
}
val (lessons, convertErrors) = apiLessons.toLessons(week)
if (convertErrors.isNotEmpty()) {
pushErrorNotification()
return Result.failure()
}
database.lessonDao().insert(*lessons.toTypedArray())
ScheduleWidget().updateAll(context)
return Result.success()
}
private fun pushErrorNotification() {
notificationManager.notify(
notificationId,
NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle(context.resources.getString(R.string.failed_get_schedule))
.setContentText(context.resources.getString(R.string.log_into_app_to_update))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.build()
)
}
}

View File

@@ -19,4 +19,8 @@
<string name="education_week">education week</string>
<string name="education_year">education year</string>
<string name="schedule_for_group">Schedule for the group</string>
<string name="failed_get_schedule">Failed to get a schedule!</string>
<string name="log_into_app_to_update">Log into the app to update your schedule</string>
<string name="no_classes_today">No classes today</string>
<string name="no_classes">No classes</string>
</resources>

View File

@@ -18,4 +18,8 @@
<string name="education_week">учебная неделя</string>
<string name="education_year">учебный год</string>
<string name="schedule_for_group">Расписание для группы</string>
<string name="failed_get_schedule">Не удалось получить расписание!</string>
<string name="log_into_app_to_update">Войдите в приложение, чтобы обновить расписание</string>
<string name="no_classes_today">Сегодня нет занятий</string>
<string name="no_classes">Нет занятий</string>
</resources>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="288.0dp"
android:minHeight="216.0dp"
android:resizeMode="vertical|horizontal">
</appwidget-provider>

View File

@@ -14,6 +14,7 @@ media3Common = "1.4.1"
kotlinSerializationJson = "1.7.1"
room = "2.6.1"
workRuntimeKtx = "2.9.1"
glance = "1.1.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -38,6 +39,8 @@ androidx-room-runtime = { group = "androidx.room", name = "room-runtime", versio
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" }
androidx-glance = { group = "androidx.glance", name = "glance-appwidget", version.ref="glance" }
androidx-glance-material = { group = "androidx.glance", name = "glance-material3", version.ref="glance" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }