From 31b9d4f631f721b86599d3995fa349e1eef67a62 Mon Sep 17 00:00:00 2001 From: StepanovPlaton Date: Sun, 15 Dec 2024 20:39:36 +0400 Subject: [PATCH] Complete work --- .idea/misc.xml | 3 +- app/build.gradle.kts | 6 + .../1.json | 82 ------- app/src/main/AndroidManifest.xml | 13 +- .../com/example/ssau_schedule/AuthActivity.kt | 137 +++++++---- .../com/example/ssau_schedule/MainActivity.kt | 213 +++++++++++------- .../java/com/example/ssau_schedule/Utils.kt | 11 +- .../com/example/ssau_schedule/api/Auth.kt | 8 +- .../com/example/ssau_schedule/api/Group.kt | 9 +- .../com/example/ssau_schedule/api/Http.kt | 8 +- .../com/example/ssau_schedule/api/Lesson.kt | 22 +- .../com/example/ssau_schedule/api/User.kt | 11 +- .../com/example/ssau_schedule/api/Year.kt | 17 +- .../ssau_schedule/components/EmptyDay.kt | 35 +-- .../ssau_schedule/components/LessonCards.kt | 71 ++++-- .../ssau_schedule/data/base/Database.kt | 10 +- .../data/base/entity/lesson/Lesson.kt | 44 ++-- .../example/ssau_schedule/data/store/Auth.kt | 4 +- .../example/ssau_schedule/data/store/Group.kt | 8 +- .../example/ssau_schedule/data/store/Utils.kt | 2 - .../example/ssau_schedule/data/store/Year.kt | 20 +- .../ssau_schedule/data/unsaved/APILesson.kt | 111 +++++---- .../example/ssau_schedule/ui/theme/Color.kt | 1 + .../example/ssau_schedule/ui/theme/Theme.kt | 14 +- .../ssau_schedule/widget/LessonCards.kt | 65 ++++++ .../example/ssau_schedule/widget/Widget.kt | 104 +++++++++ .../ssau_schedule/work/RequestLessons.kt | 109 +++++---- app/src/main/res/values-en/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + app/src/main/res/xml/widget_info.xml | 8 + gradle/libs.versions.toml | 3 + 31 files changed, 767 insertions(+), 390 deletions(-) delete mode 100644 app/schemas/com.example.ssau_schedule.data.base.Database/1.json create mode 100644 app/src/main/java/com/example/ssau_schedule/widget/LessonCards.kt create mode 100644 app/src/main/java/com/example/ssau_schedule/widget/Widget.kt create mode 100644 app/src/main/res/xml/widget_info.xml diff --git a/.idea/misc.xml b/.idea/misc.xml index 773fe0f..0ad17cb 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,7 @@ + - + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ef4372b..5668d56 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) } diff --git a/app/schemas/com.example.ssau_schedule.data.base.Database/1.json b/app/schemas/com.example.ssau_schedule.data.base.Database/1.json deleted file mode 100644 index 51cb78c..0000000 --- a/app/schemas/com.example.ssau_schedule.data.base.Database/1.json +++ /dev/null @@ -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')" - ] - } -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 610e9a2..8d52ae4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/ssau_schedule/AuthActivity.kt b/app/src/main/java/com/example/ssau_schedule/AuthActivity.kt index 4a4b46a..0d372ef 100644 --- a/app/src/main/java/com/example/ssau_schedule/AuthActivity.kt +++ b/app/src/main/java/com/example/ssau_schedule/AuthActivity.kt @@ -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(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 + ) } } } diff --git a/app/src/main/java/com/example/ssau_schedule/MainActivity.kt b/app/src/main/java/com/example/ssau_schedule/MainActivity.kt index 82bd2b4..0ba4832 100644 --- a/app/src/main/java/com/example/ssau_schedule/MainActivity.kt +++ b/app/src/main/java/com/example/ssau_schedule/MainActivity.kt @@ -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>(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(null) } + val loadedWeeks = remember { mutableStateOf>(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( + 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) diff --git a/app/src/main/java/com/example/ssau_schedule/Utils.kt b/app/src/main/java/com/example/ssau_schedule/Utils.kt index 3aa112b..5acc054 100644 --- a/app/src/main/java/com/example/ssau_schedule/Utils.kt +++ b/app/src/main/java/com/example/ssau_schedule/Utils.kt @@ -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 { diff --git a/app/src/main/java/com/example/ssau_schedule/api/Auth.kt b/app/src/main/java/com/example/ssau_schedule/api/Auth.kt index 0eafddc..4866bab 100644 --- a/app/src/main/java/com/example/ssau_schedule/api/Auth.kt +++ b/app/src/main/java/com/example/ssau_schedule/api/Auth.kt @@ -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) } diff --git a/app/src/main/java/com/example/ssau_schedule/api/Group.kt b/app/src/main/java/com/example/ssau_schedule/api/Group.kt index 6f3840b..484ba2a 100644 --- a/app/src/main/java/com/example/ssau_schedule/api/Group.kt +++ b/app/src/main/java/com/example/ssau_schedule/api/Group.kt @@ -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 diff --git a/app/src/main/java/com/example/ssau_schedule/api/Http.kt b/app/src/main/java/com/example/ssau_schedule/api/Http.kt index a034b61..ddbaa33 100644 --- a/app/src/main/java/com/example/ssau_schedule/api/Http.kt +++ b/app/src/main/java/com/example/ssau_schedule/api/Http.kt @@ -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)) } }) diff --git a/app/src/main/java/com/example/ssau_schedule/api/Lesson.kt b/app/src/main/java/com/example/ssau_schedule/api/Lesson.kt index 4b405be..a7777c9 100644 --- a/app/src/main/java/com/example/ssau_schedule/api/Lesson.kt +++ b/app/src/main/java/com/example/ssau_schedule/api/Lesson.kt @@ -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 { 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(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(response.body!!.string()), null + ) + } catch (e: SerializationException) { Log.e("Serialization error", e.message.toString()) return Pair(null, LessonAPIErrorMessage.FAILED_GET_LESSONS) } catch (e: IllegalArgumentException) { diff --git a/app/src/main/java/com/example/ssau_schedule/api/User.kt b/app/src/main/java/com/example/ssau_schedule/api/User.kt index 9499f23..d17175b 100644 --- a/app/src/main/java/com/example/ssau_schedule/api/User.kt +++ b/app/src/main/java/com/example/ssau_schedule/api/User.kt @@ -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(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) diff --git a/app/src/main/java/com/example/ssau_schedule/api/Year.kt b/app/src/main/java/com/example/ssau_schedule/api/Year.kt index f70423e..5f95bce 100644 --- a/app/src/main/java/com/example/ssau_schedule/api/Year.kt +++ b/app/src/main/java/com/example/ssau_schedule/api/Year.kt @@ -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>(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) } } diff --git a/app/src/main/java/com/example/ssau_schedule/components/EmptyDay.kt b/app/src/main/java/com/example/ssau_schedule/components/EmptyDay.kt index 23d9a82..4cf9d6c 100644 --- a/app/src/main/java/com/example/ssau_schedule/components/EmptyDay.kt +++ b/app/src/main/java/com/example/ssau_schedule/components/EmptyDay.kt @@ -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 + ) } } } \ No newline at end of file diff --git a/app/src/main/java/com/example/ssau_schedule/components/LessonCards.kt b/app/src/main/java/com/example/ssau_schedule/components/LessonCards.kt index f34abd8..6baacc1 100644 --- a/app/src/main/java/com/example/ssau_schedule/components/LessonCards.kt +++ b/app/src/main/java/com/example/ssau_schedule/components/LessonCards.kt @@ -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) { - Box(Modifier.fillMaxHeight().fillMaxWidth()) { + Box( + Modifier + .fillMaxHeight() + .fillMaxWidth()) { LazyColumn { items(lessons.count()) { LessonCard(Modifier, lessons[it]) diff --git a/app/src/main/java/com/example/ssau_schedule/data/base/Database.kt b/app/src/main/java/com/example/ssau_schedule/data/base/Database.kt index 3c95f25..d359f5e 100644 --- a/app/src/main/java/com/example/ssau_schedule/data/base/Database.kt +++ b/app/src/main/java/com/example/ssau_schedule/data/base/Database.kt @@ -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 diff --git a/app/src/main/java/com/example/ssau_schedule/data/base/entity/lesson/Lesson.kt b/app/src/main/java/com/example/ssau_schedule/data/base/entity/lesson/Lesson.kt index 929ba63..0ac31eb 100644 --- a/app/src/main/java/com/example/ssau_schedule/data/base/entity/lesson/Lesson.kt +++ b/app/src/main/java/com/example/ssau_schedule/data/base/entity/lesson/Lesson.kt @@ -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?, ) \ No newline at end of file diff --git a/app/src/main/java/com/example/ssau_schedule/data/store/Auth.kt b/app/src/main/java/com/example/ssau_schedule/data/store/Auth.kt index e62bae5..65c1750 100644 --- a/app/src/main/java/com/example/ssau_schedule/data/store/Auth.kt +++ b/app/src/main/java/com/example/ssau_schedule/data/store/Auth.kt @@ -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, diff --git a/app/src/main/java/com/example/ssau_schedule/data/store/Group.kt b/app/src/main/java/com/example/ssau_schedule/data/store/Group.kt index 5ec5036..4c49b65 100644 --- a/app/src/main/java/com/example/ssau_schedule/data/store/Group.kt +++ b/app/src/main/java/com/example/ssau_schedule/data/store/Group.kt @@ -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 } diff --git a/app/src/main/java/com/example/ssau_schedule/data/store/Utils.kt b/app/src/main/java/com/example/ssau_schedule/data/store/Utils.kt index a585078..52b25e8 100644 --- a/app/src/main/java/com/example/ssau_schedule/data/store/Utils.kt +++ b/app/src/main/java/com/example/ssau_schedule/data/store/Utils.kt @@ -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, diff --git a/app/src/main/java/com/example/ssau_schedule/data/store/Year.kt b/app/src/main/java/com/example/ssau_schedule/data/store/Year.kt index b4f6d0f..6fc3c9f 100644 --- a/app/src/main/java/com/example/ssau_schedule/data/store/Year.kt +++ b/app/src/main/java/com/example/ssau_schedule/data/store/Year.kt @@ -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 } diff --git a/app/src/main/java/com/example/ssau_schedule/data/unsaved/APILesson.kt b/app/src/main/java/com/example/ssau_schedule/data/unsaved/APILesson.kt index 5934f0d..d6935a4 100644 --- a/app/src/main/java/com/example/ssau_schedule/data/unsaved/APILesson.kt +++ b/app/src/main/java/com/example/ssau_schedule/data/unsaved/APILesson.kt @@ -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, val time: APILessonTime, val conference: APILessonConference?, - val weekday: APILessonWeekDay + val weekday: APILessonWeekDay, + val weeks: List ) { fun toLesson(week: Int): Pair { - 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, val time: APILessonTime, val conference: APILessonConference?, - val weekday: APILessonWeekDay + val weekday: APILessonWeekDay, + val weeks: List ) { fun toLesson(week: Int): Pair { - 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() 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) } diff --git a/app/src/main/java/com/example/ssau_schedule/ui/theme/Color.kt b/app/src/main/java/com/example/ssau_schedule/ui/theme/Color.kt index 251e48e..03490a8 100644 --- a/app/src/main/java/com/example/ssau_schedule/ui/theme/Color.kt +++ b/app/src/main/java/com/example/ssau_schedule/ui/theme/Color.kt @@ -38,6 +38,7 @@ class LessonColors { val Unknown = Color(0xFFE2E2E2) } } + class Dark { companion object { val Lecture = Color(0xFF444946) diff --git a/app/src/main/java/com/example/ssau_schedule/ui/theme/Theme.kt b/app/src/main/java/com/example/ssau_schedule/ui/theme/Theme.kt index abb8007..751d5a9 100644 --- a/app/src/main/java/com/example/ssau_schedule/ui/theme/Theme.kt +++ b/app/src/main/java/com/example/ssau_schedule/ui/theme/Theme.kt @@ -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 +) diff --git a/app/src/main/java/com/example/ssau_schedule/widget/LessonCards.kt b/app/src/main/java/com/example/ssau_schedule/widget/LessonCards.kt new file mode 100644 index 0000000..44826a6 --- /dev/null +++ b/app/src/main/java/com/example/ssau_schedule/widget/LessonCards.kt @@ -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) { + lessons.forEach { lesson -> + LessonCard(lesson) + } +} diff --git a/app/src/main/java/com/example/ssau_schedule/widget/Widget.kt b/app/src/main/java/com/example/ssau_schedule/widget/Widget.kt new file mode 100644 index 0000000..430a270 --- /dev/null +++ b/app/src/main/java/com/example/ssau_schedule/widget/Widget.kt @@ -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>(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() +} + diff --git a/app/src/main/java/com/example/ssau_schedule/work/RequestLessons.kt b/app/src/main/java/com/example/ssau_schedule/work/RequestLessons.kt index dc7c99c..95fcad5 100644 --- a/app/src/main/java/com/example/ssau_schedule/work/RequestLessons.kt +++ b/app/src/main/java/com/example/ssau_schedule/work/RequestLessons.kt @@ -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) -// } -//} \ No newline at end of file + +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() + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 39e9219..4b08261 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -19,4 +19,8 @@ education week education year Schedule for the group + Failed to get a schedule! + Log into the app to update your schedule + No classes today + No classes \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2c00f7a..2e337d5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,4 +18,8 @@ учебная неделя учебный год Расписание для группы + Не удалось получить расписание! + Войдите в приложение, чтобы обновить расписание + Сегодня нет занятий + Нет занятий \ No newline at end of file diff --git a/app/src/main/res/xml/widget_info.xml b/app/src/main/res/xml/widget_info.xml new file mode 100644 index 0000000..f62d28f --- /dev/null +++ b/app/src/main/res/xml/widget_info.xml @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c1ff9c8..eb31141 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }