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"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <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" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View File

@@ -73,6 +73,9 @@ android {
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = "1.5.1" kotlinCompilerExtensionVersion = "1.5.1"
} }
kotlinOptions {
jvmTarget = "1.8"
}
packaging { packaging {
resources { resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
@@ -114,4 +117,7 @@ dependencies {
ksp(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx) 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"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<application <application
android:allowBackup="true" android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
@@ -32,5 +32,16 @@
android:theme="@style/Theme.SSAU_Schedule" android:theme="@style/Theme.SSAU_Schedule"
android:screenOrientation="portrait"> android:screenOrientation="portrait">
</activity> </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> </application>
</manifest> </manifest>

View File

@@ -110,7 +110,7 @@ class AuthActivity : ComponentActivity() {
var entered by remember { mutableStateOf(false) } var entered by remember { mutableStateOf(false) }
val keyboardOpen by Utils.keyboardState() val keyboardOpen by Utils.keyboardState()
val snackbarHostState = remember { SnackbarHostState() } val notificationState = remember { SnackbarHostState() }
val logoHeight by animateFloatAsState( val logoHeight by animateFloatAsState(
if (keyboardOpen && needAuth) 0f else min( if (keyboardOpen && needAuth) 0f else min(
LocalConfiguration.current.screenWidthDp, LocalConfiguration.current.screenWidthDp,
@@ -124,7 +124,7 @@ class AuthActivity : ComponentActivity() {
) )
LaunchedEffect(user, group, year) { LaunchedEffect(user, group, year) {
if(user != null && group != null && year != null) { if (user != null && group != null && year != null) {
delay(2500) delay(2500)
startActivity(Intent(applicationContext, MainActivity::class.java)) startActivity(Intent(applicationContext, MainActivity::class.java))
} }
@@ -134,22 +134,30 @@ class AuthActivity : ComponentActivity() {
delay(3000) delay(3000)
val token = AuthStore.getAuthToken(applicationContext) val token = AuthStore.getAuthToken(applicationContext)
if(token == null) { needAuth = true; return@LaunchedEffect } if (token == null) {
needAuth = true; return@LaunchedEffect
}
val (userDetails) = userAPI.getUserDetails(token) val (userDetails) = userAPI.getUserDetails(token)
if(userDetails == null) { needAuth = true; return@LaunchedEffect } if (userDetails == null) {
else { user = userDetails } needAuth = true; return@LaunchedEffect
} else {
user = userDetails
}
val (groups, groupsError) = groupAPI.getUserGroups(token) val (groups, groupsError) = groupAPI.getUserGroups(token)
if(groups == null) { if (groups == null) {
if(groupsError != null && groupsError != if (groupsError != null && groupsError !=
GroupAPIErrorMessage.USER_NOT_AUTHORIZED) { GroupAPIErrorMessage.USER_NOT_AUTHORIZED
) {
val message = groupsError.getMessage(applicationContext) val message = groupsError.getMessage(applicationContext)
if(message != null) snackbarHostState.showSnackbar(message) if (message != null) notificationState.showSnackbar(message)
} else { needAuth = true; return@LaunchedEffect } } else {
needAuth = true; return@LaunchedEffect
}
} else { } else {
val currentGroup = GroupStore.getCurrentGroup(applicationContext) val currentGroup = GroupStore.getCurrentGroup(applicationContext)
if(currentGroup != null && groups.contains(currentGroup)) group = currentGroup if (currentGroup != null && groups.contains(currentGroup)) group = currentGroup
else { else {
GroupStore.setCurrentGroup(groups[0], applicationContext) GroupStore.setCurrentGroup(groups[0], applicationContext)
group = groups[0] group = groups[0]
@@ -157,28 +165,31 @@ class AuthActivity : ComponentActivity() {
} }
val (years, yearsError) = yearAPI.getYears(token) val (years, yearsError) = yearAPI.getYears(token)
if(years == null) { if (years == null) {
if(yearsError != null && yearsError != if (yearsError != null && yearsError !=
YearAPIErrorMessage.USER_NOT_AUTHORIZED) { YearAPIErrorMessage.USER_NOT_AUTHORIZED
) {
val message = yearsError.getMessage(applicationContext) val message = yearsError.getMessage(applicationContext)
if(message != null) snackbarHostState.showSnackbar(message) if (message != null) notificationState.showSnackbar(message)
} else { needAuth = true; return@LaunchedEffect } } else {
needAuth = true; return@LaunchedEffect
}
} else { } else {
val currentRawYear = years.find { y -> y.isCurrent } val currentRawYear = years.find { y -> y.isCurrent }
if(currentRawYear != null) { if (currentRawYear != null) {
year = currentRawYear.toYear() year = currentRawYear.toYear()
YearStore.setCurrentYear(year!!, applicationContext, authScope) YearStore.setCurrentYear(year!!, applicationContext, authScope)
} else { } else {
val message = YearAPIErrorMessage.FAILED_GET_YEARS val message = YearAPIErrorMessage.FAILED_GET_YEARS
.getMessage(applicationContext) .getMessage(applicationContext)
if(message != null) snackbarHostState.showSnackbar(message) if (message != null) notificationState.showSnackbar(message)
} }
} }
} }
Scaffold( Scaffold(
snackbarHost = { snackbarHost = {
SnackbarHost(hostState = snackbarHostState) { SnackbarHost(hostState = notificationState) {
Snackbar( Snackbar(
snackbarData = it, snackbarData = it,
containerColor = MaterialTheme.colorScheme.errorContainer, containerColor = MaterialTheme.colorScheme.errorContainer,
@@ -187,8 +198,12 @@ class AuthActivity : ComponentActivity() {
} }
} }
) { padding -> ) { padding ->
Box(Modifier.background(MaterialTheme.colorScheme.primary) Box(
.fillMaxSize().padding(padding).imePadding(), Modifier
.background(MaterialTheme.colorScheme.primary)
.fillMaxSize()
.padding(padding)
.imePadding(),
contentAlignment = BiasAlignment(0f, -0.25f), contentAlignment = BiasAlignment(0f, -0.25f),
) { ) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
@@ -196,19 +211,23 @@ class AuthActivity : ComponentActivity() {
Modifier Modifier
.widthIn(0.dp, 500.dp) .widthIn(0.dp, 500.dp)
.height(logoHeight.dp) .height(logoHeight.dp)
.padding(20.dp, 0.dp)) { .padding(20.dp, 0.dp)
Image(painterResource(R.drawable.ssau_logo_01), ) {
Image(
painterResource(R.drawable.ssau_logo_01),
contentDescription = stringResource(R.string.samara_university), contentDescription = stringResource(R.string.samara_university),
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(10.dp), .padding(10.dp),
contentScale = ContentScale.FillWidth, contentScale = ContentScale.FillWidth,
alignment = Alignment.TopCenter) alignment = Alignment.TopCenter
)
} }
Box( Box(
Modifier Modifier
.padding(20.dp, 0.dp) .padding(20.dp, 0.dp)
.widthIn(0.dp, 400.dp)) { .widthIn(0.dp, 400.dp)
) {
Column { Column {
WelcomeMessage(user, group, year) WelcomeMessage(user, group, year)
AuthForm(open = needAuth, authScope) { AuthForm(open = needAuth, authScope) {
@@ -230,26 +249,37 @@ class AuthActivity : ComponentActivity() {
var password by remember { mutableStateOf("") } var password by remember { mutableStateOf("") }
var error by remember { mutableStateOf<AuthErrorMessage?>(null) } 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( animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy, dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessLow 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( colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
), ),
) { ) {
Column(Modifier.fillMaxWidth().padding(30.dp, 20.dp), Column(
Modifier
.fillMaxWidth()
.padding(30.dp, 20.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text(stringResource(R.string.sign_in), Text(
stringResource(R.string.sign_in),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.displaySmall) style = MaterialTheme.typography.displaySmall
)
OutlinedTextField(modifier = Modifier.fillMaxWidth(), OutlinedTextField(modifier = Modifier.fillMaxWidth(),
value = login, value = login,
onValueChange = { login = it; error = null }, onValueChange = { login = it; error = null },
@@ -262,14 +292,19 @@ class AuthActivity : ComponentActivity() {
label = { Text(stringResource(R.string.password)) }, label = { Text(stringResource(R.string.password)) },
placeholder = { Text(stringResource(R.string.enter_your_password)) }) placeholder = { Text(stringResource(R.string.enter_your_password)) })
Spacer(Modifier.height(2.dp)) Spacer(Modifier.height(2.dp))
Box(Modifier.fillMaxWidth().height(14.dp)) { Box(
Modifier
.fillMaxWidth()
.height(14.dp)) {
this@Column.AnimatedVisibility( this@Column.AnimatedVisibility(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
visible = error !== null visible = error !== null
) { ) {
Text(error?.getMessage(applicationContext) ?: "", Text(
error?.getMessage(applicationContext) ?: "",
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.labelSmall) style = MaterialTheme.typography.labelSmall
)
} }
} }
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
@@ -279,11 +314,10 @@ class AuthActivity : ComponentActivity() {
else if (password.length < 5) error = AuthErrorMessage.PASSWORD_IS_TOO_SHORT else if (password.length < 5) error = AuthErrorMessage.PASSWORD_IS_TOO_SHORT
else scope.launch { else scope.launch {
val (token) = authAPI.signIn(login, password) val (token) = authAPI.signIn(login, password)
if(token != null) { if (token != null) {
AuthStore.setAuthToken(token, applicationContext) AuthStore.setAuthToken(token, applicationContext)
callback() callback()
} } else error = AuthErrorMessage.INCORRECT_LOGIN_OR_PASSWORD
else error = AuthErrorMessage.INCORRECT_LOGIN_OR_PASSWORD
} }
}, },
shape = RoundedCornerShape(50), shape = RoundedCornerShape(50),
@@ -298,26 +332,35 @@ class AuthActivity : ComponentActivity() {
@SuppressLint("SimpleDateFormat") @SuppressLint("SimpleDateFormat")
@Composable @Composable
fun WelcomeMessage(user: User?, group: Group?, year: Year?) { 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); } val currentYear = remember { Calendar.getInstance().get(Calendar.YEAR); }
Column(Modifier.fillMaxWidth().animateContentSize(), Column(
Modifier
.fillMaxWidth()
.animateContentSize(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if(user !== null && group != null && year != null) { if (user !== null && group != null && year != null) {
Text("${stringResource(R.string.hello)} ${user.name}!", Text(
"${stringResource(R.string.hello)} ${user.name}!",
color = ApplicationColors.White, color = ApplicationColors.White,
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center) textAlign = TextAlign.Center
Text("${stringResource(R.string.schedule_for_group)} ${group.name}", )
Text(
"${stringResource(R.string.schedule_for_group)} ${group.name}",
color = ApplicationColors.White, color = ApplicationColors.White,
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
textAlign = TextAlign.Center) textAlign = TextAlign.Center
Text("$currentDate, ${year.getWeekOfDate(Date())} "+ )
"${stringResource(R.string.education_week)}, ${currentYear}-"+ Text(
"${currentYear+1} ${stringResource(R.string.education_year)}", "$currentDate, ${year.getWeekOfDate(Date())} " +
"${stringResource(R.string.education_week)}, ${currentYear}-" +
"${currentYear + 1} ${stringResource(R.string.education_year)}",
color = ApplicationColors.White, color = ApplicationColors.White,
style = MaterialTheme.typography.labelSmall, 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.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
@@ -36,18 +35,18 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow 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.text.font.FontWeight
import androidx.compose.ui.unit.dp 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.Http
import com.example.ssau_schedule.api.LessonAPI import com.example.ssau_schedule.api.LessonAPI
import com.example.ssau_schedule.api.LessonAPIErrorMessage 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.components.LessonCards
import com.example.ssau_schedule.data.base.Database import com.example.ssau_schedule.data.base.Database
import com.example.ssau_schedule.data.base.entity.lesson.Lesson 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.StoreUtils
import com.example.ssau_schedule.data.store.Year
import com.example.ssau_schedule.ui.theme.SSAU_ScheduleTheme 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.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val http = Http() private val http = Http()
private val lessonAPI = LessonAPI(http) private val lessonAPI = LessonAPI(http)
private lateinit var database: Database private lateinit var database: Database
private lateinit var workManager: WorkManager
private val workName = "SSAUSchedule"
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
@@ -81,51 +86,73 @@ class MainActivity : ComponentActivity() {
@Composable @Composable
fun MainPage() { fun MainPage() {
database = remember { Database.getInstance(applicationContext) } 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 lessons = remember { mutableStateOf<List<Lesson>>(listOf()) }
val animationScope = rememberCoroutineScope() val animationScope = rememberCoroutineScope()
val currentDate = remember { mutableStateOf(Date()) }
val currentDayOfWeek = remember {
mutableIntStateOf(Utils.Date.getDayOfWeek(currentDate.value))
}
val pagerState = rememberPagerState( 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) { suspend fun getLessons(generalData: GeneralData, week: Int) {
// lessons.value = database.lessonDao().getAll() 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) val generalData = StoreUtils.getGeneralData(applicationContext)
if(generalData == null) if (generalData == null)
startActivity(Intent(applicationContext, AuthActivity::class.java)) startActivity(Intent(applicationContext, AuthActivity::class.java))
else { else {
val week = generalData.year.getWeekOfDate(Date()) if (!workStarted.value) {
val (apiLessons, apiError) = lessonAPI.getLessons( val workRequest = PeriodicWorkRequestBuilder<RequestLessonsWorker>(
generalData.token, generalData.group, generalData.year, week) repeatInterval = 3,
if(apiLessons != null && apiError == null) { TimeUnit.HOURS
val (databaseLessons, converterErrors) = apiLessons.toLessons(week) ).setInitialDelay(1, TimeUnit.HOURS).build()
Log.i("Lessons", Json.encodeToString(apiLessons)) workManager.enqueueUniquePeriodicWork(
database.lessonDao().insert(*databaseLessons.toTypedArray()) workName,
converterErrors.forEach { error -> ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
val message = error.getMessage(applicationContext) workRequest
if(message != null) snackbarHostState.showSnackbar(message) )
} workStarted.value = true
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)
}
} }
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( Scaffold(
snackbarHost = { snackbarHost = {
SnackbarHost(hostState = snackbarHostState) { SnackbarHost(hostState = notificationState) {
Snackbar( Snackbar(
snackbarData = it, snackbarData = it,
containerColor = MaterialTheme.colorScheme.errorContainer, containerColor = MaterialTheme.colorScheme.errorContainer,
@@ -142,68 +169,102 @@ class MainActivity : ComponentActivity() {
.imePadding(), .imePadding(),
) { ) {
Column(Modifier.fillMaxHeight()) { Column(Modifier.fillMaxHeight()) {
Box(Modifier.fillMaxWidth().height(60.dp)) { Box(
Row(Modifier.fillMaxSize().padding(10.dp), Modifier
horizontalArrangement = Arrangement.SpaceBetween) { .fillMaxWidth()
Box(Modifier.height(40.dp).width(40.dp) .height(60.dp)) {
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50)) Row(
.background(MaterialTheme.colorScheme.surface) Modifier
.clickable { .fillMaxSize()
animationScope.launch { .padding(10.dp),
pagerState.animateScrollToPage(pagerState.currentPage-1) 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", contentDescription = "Forward icon",
Modifier.fillMaxSize(), Modifier.fillMaxSize(),
tint = MaterialTheme.colorScheme.primary) tint = MaterialTheme.colorScheme.primary
)
} }
Box(Modifier.height(40.dp) Box(
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50)) Modifier
.background(MaterialTheme.colorScheme.surface), .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 verticalAlignment = Alignment.CenterVertically
) { ) {
Icon(Icons.Default.DateRange, Icon(
Icons.Default.DateRange,
contentDescription = "Date icon", contentDescription = "Date icon",
Modifier.height(40.dp).padding(0.dp, 0.dp, 10.dp, 0.dp), Modifier
tint = MaterialTheme.colorScheme.primary) .height(40.dp)
Text(Utils.Date.format( .padding(0.dp, 0.dp, 10.dp, 0.dp),
Utils.Date.addDays(currentDate.value, tint = MaterialTheme.colorScheme.primary
pagerState.currentPage-currentDayOfWeek.intValue) )
Text(
Utils.Date.format(
Utils.Date.addDays(
Date(),
pagerState.currentPage - Utils.Date.getDayOfWeek(
Date()
)+1
)
), ),
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.bodyLarge) style = MaterialTheme.typography.bodyLarge
)
} }
} }
Box(Modifier.height(40.dp).width(40.dp) Box(
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50)) Modifier
.background(MaterialTheme.colorScheme.surface) .height(40.dp)
.clickable { .width(40.dp)
animationScope.launch { .shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
pagerState.animateScrollToPage(pagerState.currentPage+1) .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", contentDescription = "Forward icon",
Modifier.fillMaxSize(), Modifier.fillMaxSize(),
tint = MaterialTheme.colorScheme.primary) tint = MaterialTheme.colorScheme.primary
)
} }
} }
} }
HorizontalDivider(Modifier.padding(20.dp, 0.dp)) HorizontalDivider(Modifier.padding(20.dp, 0.dp))
HorizontalPager(state = pagerState) { page -> HorizontalPager(state = pagerState) { page ->
val todayLessons = lessons.value.filter { lesson -> val todayLessons = lessons.value.filter { lesson ->
lesson.dayOfWeek-1 == Utils.Date.getDayOfWeek( lesson.dayOfWeek - 1 == Utils.Date.getDayOfWeek(
Utils.Date.addDays(currentDate.value, page-currentDayOfWeek.intValue)) && Utils.Date.addDays(Date(), page - Utils.Date.getDayOfWeek(Date())+1)
) &&
lesson.week == Utils.Date.getWeekOfStudyYear( 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 } }.sortedBy { lesson -> lesson.beginTime }
if(todayLessons.isEmpty()) if (todayLessons.isEmpty())
EmptyDay(Modifier) EmptyDay(Modifier)
else else
LessonCards(todayLessons) LessonCards(todayLessons)

View File

@@ -2,7 +2,6 @@ package com.example.ssau_schedule
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.Rect import android.graphics.Rect
import android.util.Log
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -12,7 +11,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.LocalDate
import java.util.Calendar import java.util.Calendar
class Utils { class Utils {
@@ -45,6 +43,7 @@ class Utils {
companion object { companion object {
@SuppressLint("SimpleDateFormat") @SuppressLint("SimpleDateFormat")
val StoreDateFormat = SimpleDateFormat("yyyy-MM-dd") val StoreDateFormat = SimpleDateFormat("yyyy-MM-dd")
@SuppressLint("SimpleDateFormat") @SuppressLint("SimpleDateFormat")
val DateFormat = SimpleDateFormat("dd MMMM") val DateFormat = SimpleDateFormat("dd MMMM")
@@ -55,21 +54,23 @@ class Utils {
fun getDayOfWeek(date: java.util.Date): Int { fun getDayOfWeek(date: java.util.Date): Int {
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
calendar.time = date 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 { private fun getWeekOfYear(date: java.util.Date): Int {
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
calendar.time = date calendar.time = date
return calendar.get(Calendar.WEEK_OF_YEAR) - 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 { fun getWeekOfStudyYear(date: java.util.Date): Int {
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
calendar.time = java.util.Date() calendar.time = java.util.Date()
val year = calendar.get(Calendar.YEAR) val year = calendar.get(Calendar.YEAR)
calendar.time = parse("${year}-09-01") calendar.time = parse("${year}-09-01")
return getWeekOfYear(date) - (calendar.get(Calendar.WEEK_OF_YEAR) - 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 { 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.RequestBody.Companion.toRequestBody
import okhttp3.Response import okhttp3.Response
import org.json.JSONArray 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), 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); INCORRECT_LOGIN_OR_PASSWORD(R.string.incorrect_login_or_password);
fun getMessage(context: Context) = 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) { class AuthorizationAPI(private var http: Http) {
@@ -39,8 +38,9 @@ class AuthorizationAPI(private var http: Http) {
).toString().toRequestBody("application/json".toMediaType()), ).toString().toRequestBody("application/json".toMediaType()),
mapOf( mapOf(
Pair("Next-Action", "b395d17834d8b7df06372cbf1f241170a272d540") Pair("Next-Action", "b395d17834d8b7df06372cbf1f241170a272d540")
).toHeaders()) ).toHeaders()
val token = if(response?.headers?.toMap()?.containsKey("set-cookie") == true) )
val token = if (response?.headers?.toMap()?.containsKey("set-cookie") == true)
response.headers("set-cookie").joinToString(", ") else null response.headers("set-cookie").joinToString(", ") else null
return Pair(token, exception) return Pair(token, exception)
} }

View File

@@ -15,7 +15,7 @@ enum class GroupAPIErrorMessage(private val resource: Int?) {
USER_NOT_AUTHORIZED(null); USER_NOT_AUTHORIZED(null);
fun getMessage(context: Context) = 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) { class GroupAPI(private var http: Http) {
@@ -25,9 +25,10 @@ class GroupAPI(private var http: Http) {
BuildConfig.USER_GROUPS_URL, BuildConfig.USER_GROUPS_URL,
mapOf( mapOf(
Pair("Cookie", token) Pair("Cookie", token)
).toHeaders()) ).toHeaders()
if(response?.code == 401) return Pair(null, GroupAPIErrorMessage.USER_NOT_AUTHORIZED) )
if(response?.body == null) return Pair(null, GroupAPIErrorMessage.FAILED_GET_USER_GROUPS) if (response?.code == 401) return Pair(null, GroupAPIErrorMessage.USER_NOT_AUTHORIZED)
if (response?.body == null) return Pair(null, GroupAPIErrorMessage.FAILED_GET_USER_GROUPS)
else { else {
try { try {
val groups = Utils.Serializer val groups = Utils.Serializer

View File

@@ -43,8 +43,12 @@ class Http {
override fun onResponse(call: Call, response: Response) { override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) if (!response.isSuccessful)
coroutine.resume(Pair(response, coroutine.resume(
HttpRequestException("Http response is not successful"))) Pair(
response,
HttpRequestException("Http response is not successful")
)
)
else coroutine.resume(Pair(response, null)) 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.BuildConfig
import com.example.ssau_schedule.R import com.example.ssau_schedule.R
import com.example.ssau_schedule.Utils 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.Group
import com.example.ssau_schedule.data.store.Year import com.example.ssau_schedule.data.store.Year
import com.example.ssau_schedule.data.unsaved.APILessons
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import okhttp3.Headers.Companion.toHeaders import okhttp3.Headers.Companion.toHeaders
@@ -17,7 +17,7 @@ enum class LessonAPIErrorMessage(private val resource: Int?) {
USER_NOT_AUTHORIZED(null); USER_NOT_AUTHORIZED(null);
fun getMessage(context: Context) = 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) { class LessonAPI(private var http: Http) {
@@ -29,16 +29,20 @@ class LessonAPI(private var http: Http) {
): Pair<APILessons?, LessonAPIErrorMessage?> { ): Pair<APILessons?, LessonAPIErrorMessage?> {
val (response) = http.request( val (response) = http.request(
Method.GET, Method.GET,
"${BuildConfig.LESSONS_URL}?yearId=${year.id}"+ "${BuildConfig.LESSONS_URL}?yearId=${year.id}" +
"&week=$week&userType=student&groupId=${group.id}", "&week=$week&userType=student&groupId=${group.id}",
mapOf( mapOf(
Pair("Cookie", token) Pair("Cookie", token)
).toHeaders()) ).toHeaders()
if(response?.code == 401) return Pair(null, LessonAPIErrorMessage.USER_NOT_AUTHORIZED) )
if(response?.body == null) return Pair(null, LessonAPIErrorMessage.FAILED_GET_LESSONS) if (response?.code == 401) return Pair(null, LessonAPIErrorMessage.USER_NOT_AUTHORIZED)
try { return Pair(Utils.Serializer if (response?.body == null) return Pair(null, LessonAPIErrorMessage.FAILED_GET_LESSONS)
.decodeFromString<APILessons>(response.body!!.string()), null) try {
} catch(e: SerializationException) { return Pair(
Utils.Serializer
.decodeFromString<APILessons>(response.body!!.string()), null
)
} catch (e: SerializationException) {
Log.e("Serialization error", e.message.toString()) Log.e("Serialization error", e.message.toString())
return Pair(null, LessonAPIErrorMessage.FAILED_GET_LESSONS) return Pair(null, LessonAPIErrorMessage.FAILED_GET_LESSONS)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {

View File

@@ -14,7 +14,7 @@ enum class UserAPIErrorMessage(private val resource: Int?) {
USER_NOT_AUTHORIZED(null); USER_NOT_AUTHORIZED(null);
fun getMessage(context: Context) = 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) { class UserAPI(private var http: Http) {
@@ -26,12 +26,13 @@ class UserAPI(private var http: Http) {
BuildConfig.USER_DETAILS_URL, BuildConfig.USER_DETAILS_URL,
mapOf( mapOf(
Pair("Cookie", token) Pair("Cookie", token)
).toHeaders()) ).toHeaders()
if(response?.code == 401) return Pair(null, UserAPIErrorMessage.USER_NOT_AUTHORIZED) )
if(response?.body == null) return Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS) 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 { return try {
Pair(Utils.Serializer.decodeFromString<User>(response.body!!.string()), null) Pair(Utils.Serializer.decodeFromString<User>(response.body!!.string()), null)
} catch(e: SerializationException) { } catch (e: SerializationException) {
Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS) Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS) Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS)

View File

@@ -14,7 +14,7 @@ enum class YearAPIErrorMessage(private val resource: Int?) {
USER_NOT_AUTHORIZED(null); USER_NOT_AUTHORIZED(null);
fun getMessage(context: Context) = 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) { class YearAPI(private var http: Http) {
@@ -26,19 +26,18 @@ class YearAPI(private var http: Http) {
BuildConfig.YEARS_URL, BuildConfig.YEARS_URL,
mapOf( mapOf(
Pair("Cookie", token) Pair("Cookie", token)
).toHeaders()) ).toHeaders()
if(response?.code == 401) return Pair(null, YearAPIErrorMessage.USER_NOT_AUTHORIZED) )
if(response?.body == null) return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS) if (response?.code == 401) return Pair(null, YearAPIErrorMessage.USER_NOT_AUTHORIZED)
if (response?.body == null) return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
try { try {
val rawYears = Utils.Serializer val rawYears = Utils.Serializer
.decodeFromString<List<RawYear>>(response.body!!.string()) .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) else Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
} } catch (e: SerializationException) {
catch(e: SerializationException) {
return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS) return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
} } catch (e: IllegalArgumentException) {
catch (e: IllegalArgumentException) {
return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS) return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
} }
} }

View File

@@ -1,41 +1,50 @@
package com.example.ssau_schedule.components package com.example.ssau_schedule.components
import androidx.compose.foundation.border
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.example.ssau_schedule.R
import com.example.ssau_schedule.ui.theme.LessonColors import com.example.ssau_schedule.ui.theme.LessonColors
@Composable @Composable
fun EmptyDay(modifier: Modifier) { fun EmptyDay(modifier: Modifier) {
Box(Modifier.fillMaxHeight().fillMaxWidth()) { Box(
Row(modifier.fillMaxWidth() Modifier
.padding(14.dp, 8.dp) .fillMaxHeight()
.shadow(elevation = 6.dp, shape = RoundedCornerShape(12.dp)) .fillMaxWidth()) {
.background( Row(
if(isSystemInDarkTheme()) LessonColors.Background.Dark.Unknown modifier
else LessonColors.Background.Light.Unknown .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("Сегодня нет занятий", AutoResizeText(
modifier = modifier.fillMaxWidth().padding(14.dp), stringResource(R.string.no_classes_today),
modifier = modifier
.fillMaxWidth()
.padding(14.dp),
fontSizeRange = FontSizeRange(10.sp, 24.sp), fontSizeRange = FontSizeRange(10.sp, 24.sp),
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleLarge, 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 @Composable
fun LessonCard(modifier: Modifier, lesson: Lesson) { fun LessonCard(modifier: Modifier, lesson: Lesson) {
Row(modifier.fillMaxWidth() Row(
.height(130.dp).padding(14.dp, 8.dp) modifier
.shadow(elevation = 6.dp, shape = RoundedCornerShape(12.dp)) .fillMaxWidth()
.background( .height(130.dp)
if(isSystemInDarkTheme()) .padding(14.dp, 8.dp)
lesson.type?.darkBackground ?: LessonColors.Background.Dark.Unknown .shadow(elevation = 6.dp, shape = RoundedCornerShape(12.dp))
else lesson.type?.lightBackground ?: LessonColors.Background.Light.Unknown .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) Box(
.background(lesson.type?.foreground ?: LessonColors.Foreground.Unknown)) modifier
Column(modifier.fillMaxHeight().padding(10.dp, 10.dp), .fillMaxHeight()
verticalArrangement = Arrangement.SpaceBetween) { .width(16.dp)
Row(modifier.fillMaxWidth(), .shadow(4.dp)
horizontalArrangement = Arrangement.SpaceBetween) { .background(lesson.type?.foreground ?: LessonColors.Foreground.Unknown)
Text("${lesson.beginTime} - ${lesson.endTime}", )
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, color = MaterialTheme.colorScheme.tertiary,
style = MaterialTheme.typography.bodyMedium) style = MaterialTheme.typography.bodyMedium
Text("512 - 5", )
Text(
"${lesson.room ?: "???"} - ${lesson.building ?: "?"}",
color = MaterialTheme.colorScheme.tertiary, color = MaterialTheme.colorScheme.tertiary,
style = MaterialTheme.typography.bodyMedium) style = MaterialTheme.typography.bodyMedium
)
} }
AutoResizeText(lesson.discipline, AutoResizeText(
lesson.discipline,
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
fontSizeRange = FontSizeRange(10.sp, 24.sp), fontSizeRange = FontSizeRange(10.sp, 24.sp),
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleLarge) style = MaterialTheme.typography.titleLarge
Text(lesson.teacher, )
Text(
lesson.teacher,
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.secondary, color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleSmall) style = MaterialTheme.typography.titleSmall
)
} }
} }
} }
@Composable @Composable
fun LessonCards(lessons: List<Lesson>) { fun LessonCards(lessons: List<Lesson>) {
Box(Modifier.fillMaxHeight().fillMaxWidth()) { Box(
Modifier
.fillMaxHeight()
.fillMaxWidth()) {
LazyColumn { LazyColumn {
items(lessons.count()) { items(lessons.count()) {
LessonCard(Modifier, lessons[it]) LessonCard(Modifier, lessons[it])

View File

@@ -1,7 +1,6 @@
package com.example.ssau_schedule.data.base package com.example.ssau_schedule.data.base
import android.content.Context import android.content.Context
import androidx.room.AutoMigration
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverter 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 import com.example.ssau_schedule.data.base.entity.lesson.LessonType
class Converters { class Converters {
@TypeConverter fun toLessonType(value: String) = LessonType.getTypeFromName(value) @TypeConverter
@TypeConverter fun fromLessonType(value: LessonType) = value.displayName fun toLessonType(value: String) = LessonType.getTypeFromName(value)
@TypeConverter
fun fromLessonType(value: LessonType) = value.displayName
} }
@androidx.room.Database( @androidx.room.Database(
entities = [Lesson::class], entities = [Lesson::class],
version = 1, version = 1,
autoMigrations = []) autoMigrations = []
)
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class Database : RoomDatabase() { abstract class Database : RoomDatabase() {
abstract fun lessonDao(): LessonDao abstract fun lessonDao(): LessonDao

View File

@@ -13,34 +13,48 @@ enum class LessonType(
val darkBackground: Color, val darkBackground: Color,
val lightBackground: Color, val lightBackground: Color,
) { ) {
LECTURE("Лекция", LECTURE(
"Лекция",
LessonColors.Foreground.Lecture, LessonColors.Foreground.Lecture,
LessonColors.Background.Dark.Lecture, LessonColors.Background.Dark.Lecture,
LessonColors.Background.Light.Lecture), LessonColors.Background.Light.Lecture
PRACTICE("Практика", ),
PRACTICE(
"Практика",
LessonColors.Foreground.Practice, LessonColors.Foreground.Practice,
LessonColors.Background.Dark.Practice, LessonColors.Background.Dark.Practice,
LessonColors.Background.Light.Practice), LessonColors.Background.Light.Practice
LABORATORY("Лабораторная", ),
LABORATORY(
"Лабораторная",
LessonColors.Foreground.Laboratory, LessonColors.Foreground.Laboratory,
LessonColors.Background.Dark.Laboratory, LessonColors.Background.Dark.Laboratory,
LessonColors.Background.Light.Laboratory), LessonColors.Background.Light.Laboratory
OTHER("Другое", ),
OTHER(
"Другое",
LessonColors.Foreground.Other, LessonColors.Foreground.Other,
LessonColors.Background.Dark.Other, LessonColors.Background.Dark.Other,
LessonColors.Background.Light.Other), LessonColors.Background.Light.Other
EXAMINATION("Экзамен", ),
EXAMINATION(
"Экзамен",
LessonColors.Foreground.Examination, LessonColors.Foreground.Examination,
LessonColors.Background.Dark.Examination, LessonColors.Background.Dark.Examination,
LessonColors.Background.Light.Examination), LessonColors.Background.Light.Examination
TEST("Зачёт", ),
TEST(
"Зачёт",
LessonColors.Foreground.Test, LessonColors.Foreground.Test,
LessonColors.Background.Dark.Test, LessonColors.Background.Dark.Test,
LessonColors.Background.Light.Test), LessonColors.Background.Light.Test
CONSULTATION("Консультация", ),
CONSULTATION(
"Консультация",
LessonColors.Foreground.Consultation, LessonColors.Foreground.Consultation,
LessonColors.Background.Dark.Consultation, LessonColors.Background.Dark.Consultation,
LessonColors.Background.Light.Consultation); LessonColors.Background.Light.Consultation
);
companion object { companion object {
fun getTypeFromName(name: String) = fun getTypeFromName(name: String) =
@@ -60,4 +74,6 @@ data class Lesson(
@ColumnInfo(name = "begin_time") val beginTime: String, @ColumnInfo(name = "begin_time") val beginTime: String,
@ColumnInfo(name = "end_time") val endTime: String, @ColumnInfo(name = "end_time") val endTime: String,
@ColumnInfo(name = "conference_url") val conferenceUrl: 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( suspend fun setAuthToken(
token: String, token: String,
context: Context, context: Context,
) { context.authStore.edit { authStore -> authStore[Keys.AUTH_TOKEN] = token } } ) {
context.authStore.edit { authStore -> authStore[Keys.AUTH_TOKEN] = token }
}
fun setAuthToken( fun setAuthToken(
token: String, token: String,

View File

@@ -47,9 +47,11 @@ class GroupStore {
.map { groupStore -> groupStore[Keys.CURRENT_GROUP_ID] }.first() .map { groupStore -> groupStore[Keys.CURRENT_GROUP_ID] }.first()
val currentGroupName = context.groupStore.data val currentGroupName = context.groupStore.data
.map { groupStore -> groupStore[Keys.CURRENT_GROUP_NAME] }.first() .map { groupStore -> groupStore[Keys.CURRENT_GROUP_NAME] }.first()
return if(currentGroupId != null && currentGroupName != null) return if (currentGroupId != null && currentGroupName != null)
Group(id = currentGroupId, Group(
name = currentGroupName) id = currentGroupId,
name = currentGroupName
)
else null else null
} }

View File

@@ -3,8 +3,6 @@ package com.example.ssau_schedule.data.store
import android.content.Context import android.content.Context
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
data class GeneralData( data class GeneralData(
val token: String, val token: String,

View File

@@ -30,7 +30,7 @@ data class Year(
calendar.time = startDate calendar.time = startDate
val firstWeek = calendar.get(Calendar.WEEK_OF_YEAR) val firstWeek = calendar.get(Calendar.WEEK_OF_YEAR)
calendar.time = date 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") val CURRENT_YEAR_END = stringPreferencesKey("year_end")
} }
} }
companion object { companion object {
suspend fun setCurrentYear( suspend fun setCurrentYear(
year: Year, year: Year,
@@ -80,17 +81,22 @@ class YearStore {
val currentYearId = context.yearStore.data val currentYearId = context.yearStore.data
.map { yearStore -> yearStore[Keys.CURRENT_YEAR_ID] }.first() .map { yearStore -> yearStore[Keys.CURRENT_YEAR_ID] }.first()
val currentYearStartDate = context.yearStore.data val currentYearStartDate = context.yearStore.data
.map { yearStore -> yearStore[Keys.CURRENT_YEAR_START] .map { yearStore ->
yearStore[Keys.CURRENT_YEAR_START]
}.first() }.first()
val currentYearEndDate = context.yearStore.data val currentYearEndDate = context.yearStore.data
.map { yearStore -> yearStore[Keys.CURRENT_YEAR_END] .map { yearStore ->
yearStore[Keys.CURRENT_YEAR_END]
}.first() }.first()
return if(currentYearId != null && return if (currentYearId != null &&
currentYearStartDate != null && currentYearStartDate != null &&
currentYearEndDate != null) currentYearEndDate != null
Year(id = currentYearId, )
Year(
id = currentYearId,
startDate = Utils.Date.parse(currentYearStartDate), startDate = Utils.Date.parse(currentYearStartDate),
endDate = Utils.Date.parse(currentYearEndDate)) endDate = Utils.Date.parse(currentYearEndDate)
)
else null 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); NO_DISCIPLINE_FOR_IET_LESSON(R.string.failed_get_lessons);
fun getMessage(context: Context) = 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
@Serializable data class APILessonDiscipline(val name: String) data class APILessonType(val name: String)
@Serializable data class APILessonTeacher(val name: String) @Serializable
@Serializable data class APILessonTime(val beginTime: String, val endTime: String) data class APILessonDiscipline(val name: String)
@Serializable data class APILessonConference(val url: String) @Serializable
@Serializable data class APILessonFlow(val discipline: APILessonDiscipline) data class APILessonTeacher(val name: String)
@Serializable data class APILessonWeekDay(val id: Int) @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 @Serializable
data class APILesson( data class APILesson(
@@ -30,21 +48,27 @@ data class APILesson(
val teachers: List<APILessonTeacher>, val teachers: List<APILessonTeacher>,
val time: APILessonTime, val time: APILessonTime,
val conference: APILessonConference?, val conference: APILessonConference?,
val weekday: APILessonWeekDay val weekday: APILessonWeekDay,
val weeks: List<APILessonWeeks>
) { ) {
fun toLesson(week: Int): Pair<Lesson?, LessonConverterErrorMessage?> { fun toLesson(week: Int): Pair<Lesson?, LessonConverterErrorMessage?> {
return if(teachers.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_TEACHER_FOR_LESSON) val weekInfo = weeks.find { w -> w.week == week }
else Pair(Lesson( return if (teachers.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_TEACHER_FOR_LESSON)
id = id, else Pair(
type = LessonType.getTypeFromName(type.name), Lesson(
discipline = discipline.name, id = id,
teacher = teachers[0].name, type = LessonType.getTypeFromName(type.name),
beginTime = time.beginTime, discipline = discipline.name,
endTime = time.endTime, teacher = teachers[0].name,
conferenceUrl = conference?.url, beginTime = time.beginTime,
dayOfWeek = weekday.id, endTime = time.endTime,
week = week conferenceUrl = conference?.url,
), null) 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 teachers: List<APILessonTeacher>,
val time: APILessonTime, val time: APILessonTime,
val conference: APILessonConference?, val conference: APILessonConference?,
val weekday: APILessonWeekDay val weekday: APILessonWeekDay,
val weeks: List<APILessonWeeks>
) { ) {
fun toLesson(week: Int): Pair<Lesson?, LessonConverterErrorMessage?> { fun toLesson(week: Int): Pair<Lesson?, LessonConverterErrorMessage?> {
return if(teachers.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_TEACHER_FOR_LESSON) val weekInfo = weeks.find { w -> w.week == week }
else if(flows.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_DISCIPLINE_FOR_IET_LESSON) return if (teachers.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_TEACHER_FOR_LESSON)
else Pair(Lesson( else if (flows.isEmpty()) Pair(
id = id, null,
type = LessonType.getTypeFromName(type.name), LessonConverterErrorMessage.NO_DISCIPLINE_FOR_IET_LESSON
discipline = flows[0].discipline.name, )
teacher = teachers[0].name, else Pair(
beginTime = time.beginTime, Lesson(
endTime = time.endTime, id = id,
conferenceUrl = conference?.url, type = LessonType.getTypeFromName(type.name),
dayOfWeek = weekday.id, discipline = flows[0].discipline.name,
week = week teacher = teachers[0].name,
), null) 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>() val exceptions = mutableListOf<LessonConverterErrorMessage>()
lessons.forEach { lesson -> lessons.forEach { lesson ->
val (databaseLesson, exception) = lesson.toLesson(week) val (databaseLesson, exception) = lesson.toLesson(week)
if(databaseLesson != null) databaseLessons.add(databaseLesson) if (databaseLesson != null) databaseLessons.add(databaseLesson)
if(exception != null) exceptions.add(exception) if (exception != null) exceptions.add(exception)
} }
ietLessons.forEach { ietLesson -> ietLessons.forEach { ietLesson ->
val (databaseIetLesson, exception) = ietLesson.toLesson(week) val (databaseIetLesson, exception) = ietLesson.toLesson(week)
if(databaseIetLesson != null) databaseLessons.add(databaseIetLesson) if (databaseIetLesson != null) databaseLessons.add(databaseIetLesson)
if(exception != null) exceptions.add(exception) if (exception != null) exceptions.add(exception)
} }
return Pair(databaseLessons, exceptions) return Pair(databaseLessons, exceptions)
} }

View File

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

View File

@@ -5,6 +5,9 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.glance.GlanceTheme
import androidx.glance.color.ColorProviders
import androidx.glance.material3.ColorProviders
private val DarkColorScheme = darkColorScheme( private val DarkColorScheme = darkColorScheme(
primary = ApplicationColors.Primary01, primary = ApplicationColors.Primary01,
@@ -31,7 +34,16 @@ fun SSAU_ScheduleTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit content: @Composable () -> Unit
) = MaterialTheme( ) = MaterialTheme(
colorScheme = if(darkTheme) DarkColorScheme else LightColorScheme, colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme,
typography = Typography, typography = Typography,
content = content 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 package com.example.ssau_schedule.work
import android.app.Notification import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.glance.appwidget.updateAll
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.example.ssau_schedule.R
import com.example.ssau_schedule.api.Http import com.example.ssau_schedule.api.Http
import com.example.ssau_schedule.api.LessonAPI import com.example.ssau_schedule.api.LessonAPI
import com.example.ssau_schedule.data.base.Database import com.example.ssau_schedule.data.base.Database
import com.example.ssau_schedule.data.store.StoreUtils 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, class RequestLessonsWorker(
// private val workerParams: WorkerParameters private val context: Context,
//): CoroutineWorker(context, workerParams) { workerParams: WorkerParameters
// private val notificationManager = ) : CoroutineWorker(context, workerParams) {
// context.getSystemService(Context.NOTIFICATION_SERVICE) as private val notificationManager =
// NotificationManager context.getSystemService(Context.NOTIFICATION_SERVICE) as
// NotificationManager
// override suspend fun doWork(): Result { private val channelId = "ssau_schedule_1"
// val http = Http() private val notificationId = 1234
// val lessonAPI = LessonAPI(http)
// val database = Database.getInstance(context) @RequiresApi(Build.VERSION_CODES.O)
// override suspend fun doWork(): Result {
// val generalData = StoreUtils.getGeneralData(context) ?: return Result.failure() val mChannel = NotificationChannel(
// val week = inputData.getInt("week", -1) channelId,
// if(week == -1) return Result.failure() "SSAUScheduleNotificationChannel", NotificationManager.IMPORTANCE_HIGH
// )
// mChannel.enableLights(true)
// val (apiLessons, apiErrors) = lessonAPI.getLessons( mChannel.enableVibration(true)
// generalData.token, notificationManager.createNotificationChannel(mChannel)
// generalData.group,
// generalData.year, val http = Http()
// week, val lessonAPI = LessonAPI(http)
// ) val database = Database.getInstance(context)
// if(apiErrors != null || apiLessons == null) return Result.failure()
// val generalData = StoreUtils.getGeneralData(context)
// val (lessons, convertErrors) = apiLessons.toLessons(week) if (generalData == null) {
// if(convertErrors.isNotEmpty()) { pushErrorNotification()
// var builder = NotificationCompat.Builder(context, "1") return Result.failure()
// .setContentTitle("Title") }
// .setContentText("Content") val week = generalData.year.getWeekOfDate(Date())
// .setPriority(NotificationCompat.PRIORITY_DEFAULT) val (apiLessons, apiErrors) = lessonAPI.getLessons(
// } generalData.token,
// database.lessonDao().insert(*lessons.to) 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_week">education week</string>
<string name="education_year">education year</string> <string name="education_year">education year</string>
<string name="schedule_for_group">Schedule for the group</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> </resources>

View File

@@ -18,4 +18,8 @@
<string name="education_week">учебная неделя</string> <string name="education_week">учебная неделя</string>
<string name="education_year">учебный год</string> <string name="education_year">учебный год</string>
<string name="schedule_for_group">Расписание для группы</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> </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" kotlinSerializationJson = "1.7.1"
room = "2.6.1" room = "2.6.1"
workRuntimeKtx = "2.9.1" workRuntimeKtx = "2.9.1"
glance = "1.1.0"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -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-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", 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-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] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }