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,
@@ -134,19 +134,27 @@ 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
@@ -159,10 +167,13 @@ 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) {
@@ -171,14 +182,14 @@ class AuthActivity : ComponentActivity() {
} 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))
@@ -282,8 +317,7 @@ class AuthActivity : ComponentActivity() {
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),
@@ -300,24 +334,33 @@ class AuthActivity : ComponentActivity() {
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())} "+ )
Text(
"$currentDate, ${year.getWeekOfDate(Date())} " +
"${stringResource(R.string.education_week)}, ${currentYear}-" + "${stringResource(R.string.education_week)}, ${currentYear}-" +
"${currentYear + 1} ${stringResource(R.string.education_year)}", "${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()
// }
LaunchedEffect(false) {
val generalData = StoreUtils.getGeneralData(applicationContext)
if(generalData == null)
startActivity(Intent(applicationContext, AuthActivity::class.java))
else {
val week = generalData.year.getWeekOfDate(Date())
val (apiLessons, apiError) = lessonAPI.getLessons( val (apiLessons, apiError) = lessonAPI.getLessons(
generalData.token, generalData.group, generalData.year, week) generalData.token, generalData.group, generalData.year, week
)
if (apiLessons != null && apiError == null) { if (apiLessons != null && apiError == null) {
val (databaseLessons, converterErrors) = apiLessons.toLessons(week) val (databaseLessons, converterErrors) = apiLessons.toLessons(week)
Log.i("Lessons", Json.encodeToString(apiLessons))
database.lessonDao().insert(*databaseLessons.toTypedArray()) database.lessonDao().insert(*databaseLessons.toTypedArray())
converterErrors.forEach { error -> converterErrors.forEach { error ->
val message = error.getMessage(applicationContext) val message = error.getMessage(applicationContext)
if(message != null) snackbarHostState.showSnackbar(message) if (message != null) notificationState.showSnackbar(message)
} }
lessons.value = databaseLessons lessons.value = lessons.value.plus(databaseLessons)
loadedWeeks.value = loadedWeeks.value.plus(week)
ScheduleWidget().updateAll(applicationContext)
} else { } else {
if (apiError == LessonAPIErrorMessage.USER_NOT_AUTHORIZED) { if (apiError == LessonAPIErrorMessage.USER_NOT_AUTHORIZED) {
startActivity(Intent(applicationContext, AuthActivity::class.java)) startActivity(Intent(applicationContext, AuthActivity::class.java))
} else { } else {
val message = apiError?.getMessage(applicationContext) val message = apiError?.getMessage(applicationContext)
if(message != null) snackbarHostState.showSnackbar(message) if (message != null) notificationState.showSnackbar(message)
} }
} }
} }
LaunchedEffect(pagerState.currentPage) {
val generalData = StoreUtils.getGeneralData(applicationContext)
if (generalData == null)
startActivity(Intent(applicationContext, AuthActivity::class.java))
else {
if (!workStarted.value) {
val workRequest = PeriodicWorkRequestBuilder<RequestLessonsWorker>(
repeatInterval = 3,
TimeUnit.HOURS
).setInitialDelay(1, TimeUnit.HOURS).build()
workManager.enqueueUniquePeriodicWork(
workName,
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
workRequest
)
workStarted.value = true
}
studyYear.value = generalData.year
val day = Utils.Date.addDays(
Date(),
pagerState.currentPage - Utils.Date.getDayOfWeek(Date())+1
)
var week = generalData.year.getWeekOfDate(day)
if (!loadedWeeks.value.contains(week)) getLessons(generalData, week)
if (Utils.Date.getDayOfWeek(day) == 6) week++
if (!loadedWeeks.value.contains(week)) getLessons(generalData, week)
}
} }
Scaffold( Scaffold(
snackbarHost = { snackbarHost = {
SnackbarHost(hostState = snackbarHostState) { SnackbarHost(hostState = notificationState) {
Snackbar( Snackbar(
snackbarData = it, snackbarData = it,
containerColor = MaterialTheme.colorScheme.errorContainer, containerColor = MaterialTheme.colorScheme.errorContainer,
@@ -142,10 +169,20 @@ 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)) {
Row(
Modifier
.fillMaxSize()
.padding(10.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Box(
Modifier
.height(40.dp)
.width(40.dp)
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50)) .shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.clickable { .clickable {
@@ -154,32 +191,52 @@ class MainActivity : ComponentActivity() {
} }
}, },
) { ) {
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(
Modifier
.height(40.dp)
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50)) .shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
.background(MaterialTheme.colorScheme.surface), .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(
Modifier
.height(40.dp)
.width(40.dp)
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50)) .shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
.background(MaterialTheme.colorScheme.surface) .background(MaterialTheme.colorScheme.surface)
.clickable { .clickable {
@@ -188,10 +245,12 @@ class MainActivity : ComponentActivity() {
} }
}, },
) { ) {
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
)
} }
} }
} }
@@ -199,9 +258,11 @@ class MainActivity : ComponentActivity() {
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)

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")
@@ -57,19 +56,21 @@ class Utils {
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),
@@ -39,7 +38,8 @@ 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

@@ -25,7 +25,8 @@ 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?.code == 401) return Pair(null, GroupAPIErrorMessage.USER_NOT_AUTHORIZED)
if (response?.body == null) return Pair(null, GroupAPIErrorMessage.FAILED_GET_USER_GROUPS) if (response?.body == null) return Pair(null, GroupAPIErrorMessage.FAILED_GET_USER_GROUPS)
else { else {

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
@@ -33,11 +33,15 @@ class LessonAPI(private var http: Http) {
"&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?.code == 401) return Pair(null, LessonAPIErrorMessage.USER_NOT_AUTHORIZED)
if (response?.body == null) return Pair(null, LessonAPIErrorMessage.FAILED_GET_LESSONS) if (response?.body == null) return Pair(null, LessonAPIErrorMessage.FAILED_GET_LESSONS)
try { return Pair(Utils.Serializer try {
.decodeFromString<APILessons>(response.body!!.string()), null) return Pair(
Utils.Serializer
.decodeFromString<APILessons>(response.body!!.string()), null
)
} catch (e: SerializationException) { } 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)

View File

@@ -26,7 +26,8 @@ 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?.code == 401) return Pair(null, UserAPIErrorMessage.USER_NOT_AUTHORIZED)
if (response?.body == null) return Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS) if (response?.body == null) return Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS)
return try { return try {

View File

@@ -26,7 +26,8 @@ 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?.code == 401) return Pair(null, YearAPIErrorMessage.USER_NOT_AUTHORIZED)
if (response?.body == null) return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS) if (response?.body == null) return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
try { try {
@@ -34,11 +35,9 @@ class YearAPI(private var http: Http) {
.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,28 +1,33 @@
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
.fillMaxHeight()
.fillMaxWidth()) {
Row(
modifier
.fillMaxWidth()
.padding(14.dp, 8.dp) .padding(14.dp, 8.dp)
.shadow(elevation = 6.dp, shape = RoundedCornerShape(12.dp)) .shadow(elevation = 6.dp, shape = RoundedCornerShape(12.dp))
.background( .background(
@@ -30,12 +35,16 @@ fun EmptyDay(modifier: Modifier) {
else LessonColors.Background.Light.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,8 +25,11 @@ 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
.fillMaxWidth()
.height(130.dp)
.padding(14.dp, 8.dp)
.shadow(elevation = 6.dp, shape = RoundedCornerShape(12.dp)) .shadow(elevation = 6.dp, shape = RoundedCornerShape(12.dp))
.background( .background(
if (isSystemInDarkTheme()) if (isSystemInDarkTheme())
@@ -34,35 +37,57 @@ fun LessonCard(modifier: Modifier, lesson: Lesson) {
else lesson.type?.lightBackground ?: LessonColors.Background.Light.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

@@ -48,8 +48,10 @@ class GroupStore {
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

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

@@ -14,13 +14,31 @@ enum class LessonConverterErrorMessage(private val resource: Int?) {
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,11 +48,14 @@ 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?> {
val weekInfo = weeks.find { w -> w.week == week }
return if (teachers.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_TEACHER_FOR_LESSON) return if (teachers.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_TEACHER_FOR_LESSON)
else Pair(Lesson( else Pair(
Lesson(
id = id, id = id,
type = LessonType.getTypeFromName(type.name), type = LessonType.getTypeFromName(type.name),
discipline = discipline.name, discipline = discipline.name,
@@ -43,8 +64,11 @@ data class APILesson(
endTime = time.endTime, endTime = time.endTime,
conferenceUrl = conference?.url, conferenceUrl = conference?.url,
dayOfWeek = weekday.id, dayOfWeek = weekday.id,
week = week week = week,
), null) building = weekInfo?.building?.name,
room = weekInfo?.room?.name
), null
)
} }
} }
@@ -57,12 +81,18 @@ 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?> {
val weekInfo = weeks.find { w -> w.week == week }
return if (teachers.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_TEACHER_FOR_LESSON) return if (teachers.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_TEACHER_FOR_LESSON)
else if(flows.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_DISCIPLINE_FOR_IET_LESSON) else if (flows.isEmpty()) Pair(
else Pair(Lesson( null,
LessonConverterErrorMessage.NO_DISCIPLINE_FOR_IET_LESSON
)
else Pair(
Lesson(
id = id, id = id,
type = LessonType.getTypeFromName(type.name), type = LessonType.getTypeFromName(type.name),
discipline = flows[0].discipline.name, discipline = flows[0].discipline.name,
@@ -71,8 +101,11 @@ data class APIIETLesson(
endTime = time.endTime, endTime = time.endTime,
conferenceUrl = conference?.url, conferenceUrl = conference?.url,
dayOfWeek = weekday.id, dayOfWeek = weekday.id,
week = week week = week,
), null) building = weekInfo?.building?.name,
room = weekInfo?.room?.name
), null
)
} }
} }

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,
@@ -35,3 +38,12 @@ fun SSAU_ScheduleTheme(
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" }