mirror of
https://github.com/StepanovPlaton/SSAU_Schedule.git
synced 2026-04-03 20:30:40 +04:00
Complete work
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -32,5 +32,16 @@
|
||||
android:theme="@style/Theme.SSAU_Schedule"
|
||||
android:screenOrientation="portrait">
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.WidgetReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_info" />
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -110,7 +110,7 @@ class AuthActivity : ComponentActivity() {
|
||||
var entered by remember { mutableStateOf(false) }
|
||||
|
||||
val keyboardOpen by Utils.keyboardState()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val notificationState = remember { SnackbarHostState() }
|
||||
val logoHeight by animateFloatAsState(
|
||||
if (keyboardOpen && needAuth) 0f else min(
|
||||
LocalConfiguration.current.screenWidthDp,
|
||||
@@ -124,7 +124,7 @@ class AuthActivity : ComponentActivity() {
|
||||
)
|
||||
|
||||
LaunchedEffect(user, group, year) {
|
||||
if(user != null && group != null && year != null) {
|
||||
if (user != null && group != null && year != null) {
|
||||
delay(2500)
|
||||
startActivity(Intent(applicationContext, MainActivity::class.java))
|
||||
}
|
||||
@@ -134,22 +134,30 @@ class AuthActivity : ComponentActivity() {
|
||||
delay(3000)
|
||||
|
||||
val token = AuthStore.getAuthToken(applicationContext)
|
||||
if(token == null) { needAuth = true; return@LaunchedEffect }
|
||||
if (token == null) {
|
||||
needAuth = true; return@LaunchedEffect
|
||||
}
|
||||
|
||||
val (userDetails) = userAPI.getUserDetails(token)
|
||||
if(userDetails == null) { needAuth = true; return@LaunchedEffect }
|
||||
else { user = userDetails }
|
||||
if (userDetails == null) {
|
||||
needAuth = true; return@LaunchedEffect
|
||||
} else {
|
||||
user = userDetails
|
||||
}
|
||||
|
||||
val (groups, groupsError) = groupAPI.getUserGroups(token)
|
||||
if(groups == null) {
|
||||
if(groupsError != null && groupsError !=
|
||||
GroupAPIErrorMessage.USER_NOT_AUTHORIZED) {
|
||||
if (groups == null) {
|
||||
if (groupsError != null && groupsError !=
|
||||
GroupAPIErrorMessage.USER_NOT_AUTHORIZED
|
||||
) {
|
||||
val message = groupsError.getMessage(applicationContext)
|
||||
if(message != null) snackbarHostState.showSnackbar(message)
|
||||
} else { needAuth = true; return@LaunchedEffect }
|
||||
if (message != null) notificationState.showSnackbar(message)
|
||||
} else {
|
||||
needAuth = true; return@LaunchedEffect
|
||||
}
|
||||
} else {
|
||||
val currentGroup = GroupStore.getCurrentGroup(applicationContext)
|
||||
if(currentGroup != null && groups.contains(currentGroup)) group = currentGroup
|
||||
if (currentGroup != null && groups.contains(currentGroup)) group = currentGroup
|
||||
else {
|
||||
GroupStore.setCurrentGroup(groups[0], applicationContext)
|
||||
group = groups[0]
|
||||
@@ -157,28 +165,31 @@ class AuthActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
val (years, yearsError) = yearAPI.getYears(token)
|
||||
if(years == null) {
|
||||
if(yearsError != null && yearsError !=
|
||||
YearAPIErrorMessage.USER_NOT_AUTHORIZED) {
|
||||
if (years == null) {
|
||||
if (yearsError != null && yearsError !=
|
||||
YearAPIErrorMessage.USER_NOT_AUTHORIZED
|
||||
) {
|
||||
val message = yearsError.getMessage(applicationContext)
|
||||
if(message != null) snackbarHostState.showSnackbar(message)
|
||||
} else { needAuth = true; return@LaunchedEffect }
|
||||
if (message != null) notificationState.showSnackbar(message)
|
||||
} else {
|
||||
needAuth = true; return@LaunchedEffect
|
||||
}
|
||||
} else {
|
||||
val currentRawYear = years.find { y -> y.isCurrent }
|
||||
if(currentRawYear != null) {
|
||||
if (currentRawYear != null) {
|
||||
year = currentRawYear.toYear()
|
||||
YearStore.setCurrentYear(year!!, applicationContext, authScope)
|
||||
} else {
|
||||
val message = YearAPIErrorMessage.FAILED_GET_YEARS
|
||||
.getMessage(applicationContext)
|
||||
if(message != null) snackbarHostState.showSnackbar(message)
|
||||
if (message != null) notificationState.showSnackbar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState) {
|
||||
SnackbarHost(hostState = notificationState) {
|
||||
Snackbar(
|
||||
snackbarData = it,
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
@@ -187,8 +198,12 @@ class AuthActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Box(Modifier.background(MaterialTheme.colorScheme.primary)
|
||||
.fillMaxSize().padding(padding).imePadding(),
|
||||
Box(
|
||||
Modifier
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.imePadding(),
|
||||
contentAlignment = BiasAlignment(0f, -0.25f),
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
@@ -196,19 +211,23 @@ class AuthActivity : ComponentActivity() {
|
||||
Modifier
|
||||
.widthIn(0.dp, 500.dp)
|
||||
.height(logoHeight.dp)
|
||||
.padding(20.dp, 0.dp)) {
|
||||
Image(painterResource(R.drawable.ssau_logo_01),
|
||||
.padding(20.dp, 0.dp)
|
||||
) {
|
||||
Image(
|
||||
painterResource(R.drawable.ssau_logo_01),
|
||||
contentDescription = stringResource(R.string.samara_university),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(10.dp),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
alignment = Alignment.TopCenter)
|
||||
alignment = Alignment.TopCenter
|
||||
)
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.padding(20.dp, 0.dp)
|
||||
.widthIn(0.dp, 400.dp)) {
|
||||
.widthIn(0.dp, 400.dp)
|
||||
) {
|
||||
Column {
|
||||
WelcomeMessage(user, group, year)
|
||||
AuthForm(open = needAuth, authScope) {
|
||||
@@ -230,26 +249,37 @@ class AuthActivity : ComponentActivity() {
|
||||
var password by remember { mutableStateOf("") }
|
||||
var error by remember { mutableStateOf<AuthErrorMessage?>(null) }
|
||||
|
||||
val height by animateDpAsState(if (open) 290.dp else 0.dp, label = "Auth form height",
|
||||
val height by animateDpAsState(
|
||||
if (open) 290.dp else 0.dp, label = "Auth form height",
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
)
|
||||
|
||||
Card(Modifier.fillMaxWidth().height(height).padding(0.dp, 10.dp).shadow(10.dp),
|
||||
Card(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(height)
|
||||
.padding(0.dp, 10.dp)
|
||||
.shadow(10.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
),
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth().padding(30.dp, 20.dp),
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(30.dp, 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(stringResource(R.string.sign_in),
|
||||
Text(
|
||||
stringResource(R.string.sign_in),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.displaySmall)
|
||||
style = MaterialTheme.typography.displaySmall
|
||||
)
|
||||
OutlinedTextField(modifier = Modifier.fillMaxWidth(),
|
||||
value = login,
|
||||
onValueChange = { login = it; error = null },
|
||||
@@ -262,14 +292,19 @@ class AuthActivity : ComponentActivity() {
|
||||
label = { Text(stringResource(R.string.password)) },
|
||||
placeholder = { Text(stringResource(R.string.enter_your_password)) })
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Box(Modifier.fillMaxWidth().height(14.dp)) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(14.dp)) {
|
||||
this@Column.AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
visible = error !== null
|
||||
) {
|
||||
Text(error?.getMessage(applicationContext) ?: "",
|
||||
Text(
|
||||
error?.getMessage(applicationContext) ?: "",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.labelSmall)
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
@@ -279,11 +314,10 @@ class AuthActivity : ComponentActivity() {
|
||||
else if (password.length < 5) error = AuthErrorMessage.PASSWORD_IS_TOO_SHORT
|
||||
else scope.launch {
|
||||
val (token) = authAPI.signIn(login, password)
|
||||
if(token != null) {
|
||||
if (token != null) {
|
||||
AuthStore.setAuthToken(token, applicationContext)
|
||||
callback()
|
||||
}
|
||||
else error = AuthErrorMessage.INCORRECT_LOGIN_OR_PASSWORD
|
||||
} else error = AuthErrorMessage.INCORRECT_LOGIN_OR_PASSWORD
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(50),
|
||||
@@ -298,26 +332,35 @@ class AuthActivity : ComponentActivity() {
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
@Composable
|
||||
fun WelcomeMessage(user: User?, group: Group?, year: Year?) {
|
||||
val currentDate = remember { SimpleDateFormat("d MMMM").format(Date()) }
|
||||
val currentDate = remember { SimpleDateFormat("d MMMM").format(Date()) }
|
||||
val currentYear = remember { Calendar.getInstance().get(Calendar.YEAR); }
|
||||
Column(Modifier.fillMaxWidth().animateContentSize(),
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if(user !== null && group != null && year != null) {
|
||||
Text("${stringResource(R.string.hello)} ${user.name}!",
|
||||
if (user !== null && group != null && year != null) {
|
||||
Text(
|
||||
"${stringResource(R.string.hello)} ${user.name}!",
|
||||
color = ApplicationColors.White,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center)
|
||||
Text("${stringResource(R.string.schedule_for_group)} ${group.name}",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(
|
||||
"${stringResource(R.string.schedule_for_group)} ${group.name}",
|
||||
color = ApplicationColors.White,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
textAlign = TextAlign.Center)
|
||||
Text("$currentDate, ${year.getWeekOfDate(Date())} "+
|
||||
"${stringResource(R.string.education_week)}, ${currentYear}-"+
|
||||
"${currentYear+1} ${stringResource(R.string.education_year)}",
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(
|
||||
"$currentDate, ${year.getWeekOfDate(Date())} " +
|
||||
"${stringResource(R.string.education_week)}, ${currentYear}-" +
|
||||
"${currentYear + 1} ${stringResource(R.string.education_year)}",
|
||||
color = ApplicationColors.White,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = TextAlign.Center)
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.example.ssau_schedule
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
@@ -36,18 +35,18 @@ import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import com.example.ssau_schedule.api.Http
|
||||
import com.example.ssau_schedule.api.LessonAPI
|
||||
import com.example.ssau_schedule.api.LessonAPIErrorMessage
|
||||
@@ -55,18 +54,24 @@ import com.example.ssau_schedule.components.EmptyDay
|
||||
import com.example.ssau_schedule.components.LessonCards
|
||||
import com.example.ssau_schedule.data.base.Database
|
||||
import com.example.ssau_schedule.data.base.entity.lesson.Lesson
|
||||
import com.example.ssau_schedule.data.store.GeneralData
|
||||
import com.example.ssau_schedule.data.store.StoreUtils
|
||||
import com.example.ssau_schedule.data.store.Year
|
||||
import com.example.ssau_schedule.ui.theme.SSAU_ScheduleTheme
|
||||
import com.example.ssau_schedule.widget.ScheduleWidget
|
||||
import com.example.ssau_schedule.work.RequestLessonsWorker
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val http = Http()
|
||||
private val lessonAPI = LessonAPI(http)
|
||||
private lateinit var database: Database
|
||||
|
||||
private lateinit var workManager: WorkManager
|
||||
private val workName = "SSAUSchedule"
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
@@ -81,51 +86,73 @@ class MainActivity : ComponentActivity() {
|
||||
@Composable
|
||||
fun MainPage() {
|
||||
database = remember { Database.getInstance(applicationContext) }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
workManager = remember { WorkManager.getInstance(applicationContext) }
|
||||
val notificationState = remember { SnackbarHostState() }
|
||||
val lessons = remember { mutableStateOf<List<Lesson>>(listOf()) }
|
||||
val animationScope = rememberCoroutineScope()
|
||||
val currentDate = remember { mutableStateOf(Date()) }
|
||||
val currentDayOfWeek = remember {
|
||||
mutableIntStateOf(Utils.Date.getDayOfWeek(currentDate.value))
|
||||
}
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = currentDayOfWeek.intValue-1, pageCount = {Int.MAX_VALUE})
|
||||
initialPage = Utils.Date.getDayOfWeek(Date()) - 1, pageCount = { Int.MAX_VALUE })
|
||||
val studyYear = remember { mutableStateOf<Year?>(null) }
|
||||
val loadedWeeks = remember { mutableStateOf<List<Int>>(listOf()) }
|
||||
val workStarted = remember { mutableStateOf(false) }
|
||||
|
||||
// LaunchedEffect(false) {
|
||||
// lessons.value = database.lessonDao().getAll()
|
||||
// }
|
||||
suspend fun getLessons(generalData: GeneralData, week: Int) {
|
||||
val (apiLessons, apiError) = lessonAPI.getLessons(
|
||||
generalData.token, generalData.group, generalData.year, week
|
||||
)
|
||||
if (apiLessons != null && apiError == null) {
|
||||
val (databaseLessons, converterErrors) = apiLessons.toLessons(week)
|
||||
database.lessonDao().insert(*databaseLessons.toTypedArray())
|
||||
converterErrors.forEach { error ->
|
||||
val message = error.getMessage(applicationContext)
|
||||
if (message != null) notificationState.showSnackbar(message)
|
||||
}
|
||||
lessons.value = lessons.value.plus(databaseLessons)
|
||||
loadedWeeks.value = loadedWeeks.value.plus(week)
|
||||
ScheduleWidget().updateAll(applicationContext)
|
||||
} else {
|
||||
if (apiError == LessonAPIErrorMessage.USER_NOT_AUTHORIZED) {
|
||||
startActivity(Intent(applicationContext, AuthActivity::class.java))
|
||||
} else {
|
||||
val message = apiError?.getMessage(applicationContext)
|
||||
if (message != null) notificationState.showSnackbar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(false) {
|
||||
LaunchedEffect(pagerState.currentPage) {
|
||||
val generalData = StoreUtils.getGeneralData(applicationContext)
|
||||
if(generalData == null)
|
||||
if (generalData == null)
|
||||
startActivity(Intent(applicationContext, AuthActivity::class.java))
|
||||
else {
|
||||
val week = generalData.year.getWeekOfDate(Date())
|
||||
val (apiLessons, apiError) = lessonAPI.getLessons(
|
||||
generalData.token, generalData.group, generalData.year, week)
|
||||
if(apiLessons != null && apiError == null) {
|
||||
val (databaseLessons, converterErrors) = apiLessons.toLessons(week)
|
||||
Log.i("Lessons", Json.encodeToString(apiLessons))
|
||||
database.lessonDao().insert(*databaseLessons.toTypedArray())
|
||||
converterErrors.forEach { error ->
|
||||
val message = error.getMessage(applicationContext)
|
||||
if(message != null) snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
lessons.value = databaseLessons
|
||||
} else {
|
||||
if(apiError == LessonAPIErrorMessage.USER_NOT_AUTHORIZED) {
|
||||
startActivity(Intent(applicationContext, AuthActivity::class.java))
|
||||
} else {
|
||||
val message = apiError?.getMessage(applicationContext)
|
||||
if(message != null) snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
if (!workStarted.value) {
|
||||
val workRequest = PeriodicWorkRequestBuilder<RequestLessonsWorker>(
|
||||
repeatInterval = 3,
|
||||
TimeUnit.HOURS
|
||||
).setInitialDelay(1, TimeUnit.HOURS).build()
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
workName,
|
||||
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
|
||||
workRequest
|
||||
)
|
||||
workStarted.value = true
|
||||
}
|
||||
|
||||
studyYear.value = generalData.year
|
||||
val day = Utils.Date.addDays(
|
||||
Date(),
|
||||
pagerState.currentPage - Utils.Date.getDayOfWeek(Date())+1
|
||||
)
|
||||
var week = generalData.year.getWeekOfDate(day)
|
||||
if (!loadedWeeks.value.contains(week)) getLessons(generalData, week)
|
||||
if (Utils.Date.getDayOfWeek(day) == 6) week++
|
||||
if (!loadedWeeks.value.contains(week)) getLessons(generalData, week)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
snackbarHost = {
|
||||
SnackbarHost(hostState = snackbarHostState) {
|
||||
SnackbarHost(hostState = notificationState) {
|
||||
Snackbar(
|
||||
snackbarData = it,
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
@@ -142,68 +169,102 @@ class MainActivity : ComponentActivity() {
|
||||
.imePadding(),
|
||||
) {
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Box(Modifier.fillMaxWidth().height(60.dp)) {
|
||||
Row(Modifier.fillMaxSize().padding(10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Box(Modifier.height(40.dp).width(40.dp)
|
||||
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.clickable {
|
||||
animationScope.launch {
|
||||
pagerState.animateScrollToPage(pagerState.currentPage-1)
|
||||
}
|
||||
},
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(60.dp)) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(10.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.height(40.dp)
|
||||
.width(40.dp)
|
||||
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.clickable {
|
||||
animationScope.launch {
|
||||
pagerState.animateScrollToPage(pagerState.currentPage - 1)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.KeyboardArrowLeft,
|
||||
contentDescription = "Forward icon",
|
||||
Modifier.fillMaxSize(),
|
||||
tint = MaterialTheme.colorScheme.primary)
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Box(Modifier.height(40.dp)
|
||||
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
|
||||
.background(MaterialTheme.colorScheme.surface),
|
||||
Box(
|
||||
Modifier
|
||||
.height(40.dp)
|
||||
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
|
||||
.background(MaterialTheme.colorScheme.surface),
|
||||
) {
|
||||
Row(Modifier.fillMaxHeight().padding(10.dp, 0.dp),
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(10.dp, 0.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.DateRange,
|
||||
Icon(
|
||||
Icons.Default.DateRange,
|
||||
contentDescription = "Date icon",
|
||||
Modifier.height(40.dp).padding(0.dp, 0.dp, 10.dp, 0.dp),
|
||||
tint = MaterialTheme.colorScheme.primary)
|
||||
Text(Utils.Date.format(
|
||||
Utils.Date.addDays(currentDate.value,
|
||||
pagerState.currentPage-currentDayOfWeek.intValue)
|
||||
Modifier
|
||||
.height(40.dp)
|
||||
.padding(0.dp, 0.dp, 10.dp, 0.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
Utils.Date.format(
|
||||
Utils.Date.addDays(
|
||||
Date(),
|
||||
pagerState.currentPage - Utils.Date.getDayOfWeek(
|
||||
Date()
|
||||
)+1
|
||||
)
|
||||
),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
style = MaterialTheme.typography.bodyLarge)
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(Modifier.height(40.dp).width(40.dp)
|
||||
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.clickable {
|
||||
animationScope.launch {
|
||||
pagerState.animateScrollToPage(pagerState.currentPage+1)
|
||||
}
|
||||
},
|
||||
Box(
|
||||
Modifier
|
||||
.height(40.dp)
|
||||
.width(40.dp)
|
||||
.shadow(elevation = 6.dp, shape = RoundedCornerShape(50))
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.clickable {
|
||||
animationScope.launch {
|
||||
pagerState.animateScrollToPage(pagerState.currentPage + 1)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||
contentDescription = "Forward icon",
|
||||
Modifier.fillMaxSize(),
|
||||
tint = MaterialTheme.colorScheme.primary)
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalDivider(Modifier.padding(20.dp, 0.dp))
|
||||
HorizontalPager(state = pagerState) { page ->
|
||||
val todayLessons = lessons.value.filter { lesson ->
|
||||
lesson.dayOfWeek-1 == Utils.Date.getDayOfWeek(
|
||||
Utils.Date.addDays(currentDate.value, page-currentDayOfWeek.intValue)) &&
|
||||
lesson.dayOfWeek - 1 == Utils.Date.getDayOfWeek(
|
||||
Utils.Date.addDays(Date(), page - Utils.Date.getDayOfWeek(Date())+1)
|
||||
) &&
|
||||
lesson.week == Utils.Date.getWeekOfStudyYear(
|
||||
Utils.Date.addDays(currentDate.value, page-currentDayOfWeek.intValue))
|
||||
Utils.Date.addDays(Date(), page - Utils.Date.getDayOfWeek(Date())+1)
|
||||
)
|
||||
}.sortedBy { lesson -> lesson.beginTime }
|
||||
if(todayLessons.isEmpty())
|
||||
if (todayLessons.isEmpty())
|
||||
EmptyDay(Modifier)
|
||||
else
|
||||
LessonCards(todayLessons)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,9 +5,9 @@ import android.util.Log
|
||||
import com.example.ssau_schedule.BuildConfig
|
||||
import com.example.ssau_schedule.R
|
||||
import com.example.ssau_schedule.Utils
|
||||
import com.example.ssau_schedule.data.unsaved.APILessons
|
||||
import com.example.ssau_schedule.data.store.Group
|
||||
import com.example.ssau_schedule.data.store.Year
|
||||
import com.example.ssau_schedule.data.unsaved.APILessons
|
||||
import kotlinx.serialization.SerializationException
|
||||
import okhttp3.Headers.Companion.toHeaders
|
||||
|
||||
@@ -17,7 +17,7 @@ enum class LessonAPIErrorMessage(private val resource: Int?) {
|
||||
USER_NOT_AUTHORIZED(null);
|
||||
|
||||
fun getMessage(context: Context) =
|
||||
if(resource != null) context.getString(resource) else null
|
||||
if (resource != null) context.getString(resource) else null
|
||||
}
|
||||
|
||||
class LessonAPI(private var http: Http) {
|
||||
@@ -29,16 +29,20 @@ class LessonAPI(private var http: Http) {
|
||||
): Pair<APILessons?, LessonAPIErrorMessage?> {
|
||||
val (response) = http.request(
|
||||
Method.GET,
|
||||
"${BuildConfig.LESSONS_URL}?yearId=${year.id}"+
|
||||
"${BuildConfig.LESSONS_URL}?yearId=${year.id}" +
|
||||
"&week=$week&userType=student&groupId=${group.id}",
|
||||
mapOf(
|
||||
Pair("Cookie", token)
|
||||
).toHeaders())
|
||||
if(response?.code == 401) return Pair(null, LessonAPIErrorMessage.USER_NOT_AUTHORIZED)
|
||||
if(response?.body == null) return Pair(null, LessonAPIErrorMessage.FAILED_GET_LESSONS)
|
||||
try { return Pair(Utils.Serializer
|
||||
.decodeFromString<APILessons>(response.body!!.string()), null)
|
||||
} catch(e: SerializationException) {
|
||||
).toHeaders()
|
||||
)
|
||||
if (response?.code == 401) return Pair(null, LessonAPIErrorMessage.USER_NOT_AUTHORIZED)
|
||||
if (response?.body == null) return Pair(null, LessonAPIErrorMessage.FAILED_GET_LESSONS)
|
||||
try {
|
||||
return Pair(
|
||||
Utils.Serializer
|
||||
.decodeFromString<APILessons>(response.body!!.string()), null
|
||||
)
|
||||
} catch (e: SerializationException) {
|
||||
Log.e("Serialization error", e.message.toString())
|
||||
return Pair(null, LessonAPIErrorMessage.FAILED_GET_LESSONS)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
|
||||
@@ -14,7 +14,7 @@ enum class UserAPIErrorMessage(private val resource: Int?) {
|
||||
USER_NOT_AUTHORIZED(null);
|
||||
|
||||
fun getMessage(context: Context) =
|
||||
if(resource != null) context.getString(resource) else null
|
||||
if (resource != null) context.getString(resource) else null
|
||||
}
|
||||
|
||||
class UserAPI(private var http: Http) {
|
||||
@@ -26,12 +26,13 @@ class UserAPI(private var http: Http) {
|
||||
BuildConfig.USER_DETAILS_URL,
|
||||
mapOf(
|
||||
Pair("Cookie", token)
|
||||
).toHeaders())
|
||||
if(response?.code == 401) return Pair(null, UserAPIErrorMessage.USER_NOT_AUTHORIZED)
|
||||
if(response?.body == null) return Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS)
|
||||
).toHeaders()
|
||||
)
|
||||
if (response?.code == 401) return Pair(null, UserAPIErrorMessage.USER_NOT_AUTHORIZED)
|
||||
if (response?.body == null) return Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS)
|
||||
return try {
|
||||
Pair(Utils.Serializer.decodeFromString<User>(response.body!!.string()), null)
|
||||
} catch(e: SerializationException) {
|
||||
} catch (e: SerializationException) {
|
||||
Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Pair(null, UserAPIErrorMessage.FAILED_GET_USER_DETAILS)
|
||||
|
||||
@@ -14,7 +14,7 @@ enum class YearAPIErrorMessage(private val resource: Int?) {
|
||||
USER_NOT_AUTHORIZED(null);
|
||||
|
||||
fun getMessage(context: Context) =
|
||||
if(resource != null) context.getString(resource) else null
|
||||
if (resource != null) context.getString(resource) else null
|
||||
}
|
||||
|
||||
class YearAPI(private var http: Http) {
|
||||
@@ -26,19 +26,18 @@ class YearAPI(private var http: Http) {
|
||||
BuildConfig.YEARS_URL,
|
||||
mapOf(
|
||||
Pair("Cookie", token)
|
||||
).toHeaders())
|
||||
if(response?.code == 401) return Pair(null, YearAPIErrorMessage.USER_NOT_AUTHORIZED)
|
||||
if(response?.body == null) return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
|
||||
).toHeaders()
|
||||
)
|
||||
if (response?.code == 401) return Pair(null, YearAPIErrorMessage.USER_NOT_AUTHORIZED)
|
||||
if (response?.body == null) return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
|
||||
try {
|
||||
val rawYears = Utils.Serializer
|
||||
.decodeFromString<List<RawYear>>(response.body!!.string())
|
||||
return if(rawYears.isNotEmpty()) Pair(rawYears, null)
|
||||
return if (rawYears.isNotEmpty()) Pair(rawYears, null)
|
||||
else Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
|
||||
}
|
||||
catch(e: SerializationException) {
|
||||
} catch (e: SerializationException) {
|
||||
return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
|
||||
}
|
||||
catch (e: IllegalArgumentException) {
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return Pair(null, YearAPIErrorMessage.FAILED_GET_YEARS)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,44 +25,69 @@ import com.example.ssau_schedule.ui.theme.LessonColors
|
||||
|
||||
@Composable
|
||||
fun LessonCard(modifier: Modifier, lesson: Lesson) {
|
||||
Row(modifier.fillMaxWidth()
|
||||
.height(130.dp).padding(14.dp, 8.dp)
|
||||
.shadow(elevation = 6.dp, shape = RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
if(isSystemInDarkTheme())
|
||||
lesson.type?.darkBackground ?: LessonColors.Background.Dark.Unknown
|
||||
else lesson.type?.lightBackground ?: LessonColors.Background.Light.Unknown
|
||||
)
|
||||
Row(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.height(130.dp)
|
||||
.padding(14.dp, 8.dp)
|
||||
.shadow(elevation = 6.dp, shape = RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
if (isSystemInDarkTheme())
|
||||
lesson.type?.darkBackground ?: LessonColors.Background.Dark.Unknown
|
||||
else lesson.type?.lightBackground ?: LessonColors.Background.Light.Unknown
|
||||
)
|
||||
) {
|
||||
Box(modifier.fillMaxHeight().width(16.dp).shadow(4.dp)
|
||||
.background(lesson.type?.foreground ?: LessonColors.Foreground.Unknown))
|
||||
Column(modifier.fillMaxHeight().padding(10.dp, 10.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween) {
|
||||
Row(modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text("${lesson.beginTime} - ${lesson.endTime}",
|
||||
Box(
|
||||
modifier
|
||||
.fillMaxHeight()
|
||||
.width(16.dp)
|
||||
.shadow(4.dp)
|
||||
.background(lesson.type?.foreground ?: LessonColors.Foreground.Unknown)
|
||||
)
|
||||
Column(
|
||||
modifier
|
||||
.fillMaxHeight()
|
||||
.padding(10.dp, 10.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
"${lesson.beginTime} - ${lesson.endTime}",
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
style = MaterialTheme.typography.bodyMedium)
|
||||
Text("512 - 5",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
"${lesson.room ?: "???"} - ${lesson.building ?: "?"}",
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
style = MaterialTheme.typography.bodyMedium)
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
AutoResizeText(lesson.discipline,
|
||||
AutoResizeText(
|
||||
lesson.discipline,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
fontSizeRange = FontSizeRange(10.sp, 24.sp),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = MaterialTheme.typography.titleLarge)
|
||||
Text(lesson.teacher,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Text(
|
||||
lesson.teacher,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = MaterialTheme.typography.titleSmall)
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LessonCards(lessons: List<Lesson>) {
|
||||
Box(Modifier.fillMaxHeight().fillMaxWidth()) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth()) {
|
||||
LazyColumn {
|
||||
items(lessons.count()) {
|
||||
LessonCard(Modifier, lessons[it])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?,
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -11,16 +11,34 @@ enum class LessonConverterErrorMessage(private val resource: Int?) {
|
||||
NO_DISCIPLINE_FOR_IET_LESSON(R.string.failed_get_lessons);
|
||||
|
||||
fun getMessage(context: Context) =
|
||||
if(resource != null) context.getString(resource) else null
|
||||
if (resource != null) context.getString(resource) else null
|
||||
}
|
||||
|
||||
@Serializable data class APILessonType(val name: String)
|
||||
@Serializable data class APILessonDiscipline(val name: String)
|
||||
@Serializable data class APILessonTeacher(val name: String)
|
||||
@Serializable data class APILessonTime(val beginTime: String, val endTime: String)
|
||||
@Serializable data class APILessonConference(val url: String)
|
||||
@Serializable data class APILessonFlow(val discipline: APILessonDiscipline)
|
||||
@Serializable data class APILessonWeekDay(val id: Int)
|
||||
@Serializable
|
||||
data class APILessonType(val name: String)
|
||||
@Serializable
|
||||
data class APILessonDiscipline(val name: String)
|
||||
@Serializable
|
||||
data class APILessonTeacher(val name: String)
|
||||
@Serializable
|
||||
data class APILessonTime(val beginTime: String, val endTime: String)
|
||||
@Serializable
|
||||
data class APILessonConference(val url: String)
|
||||
@Serializable
|
||||
data class APILessonFlow(val discipline: APILessonDiscipline)
|
||||
@Serializable
|
||||
data class APILessonWeekDay(val id: Int)
|
||||
|
||||
@Serializable
|
||||
data class APILessonBuilding(val name: String)
|
||||
@Serializable
|
||||
data class APILessonRoom(val name: String)
|
||||
@Serializable
|
||||
data class APILessonWeeks(
|
||||
val building: APILessonBuilding,
|
||||
val room: APILessonRoom,
|
||||
val week: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class APILesson(
|
||||
@@ -30,21 +48,27 @@ data class APILesson(
|
||||
val teachers: List<APILessonTeacher>,
|
||||
val time: APILessonTime,
|
||||
val conference: APILessonConference?,
|
||||
val weekday: APILessonWeekDay
|
||||
val weekday: APILessonWeekDay,
|
||||
val weeks: List<APILessonWeeks>
|
||||
) {
|
||||
fun toLesson(week: Int): Pair<Lesson?, LessonConverterErrorMessage?> {
|
||||
return if(teachers.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_TEACHER_FOR_LESSON)
|
||||
else Pair(Lesson(
|
||||
id = id,
|
||||
type = LessonType.getTypeFromName(type.name),
|
||||
discipline = discipline.name,
|
||||
teacher = teachers[0].name,
|
||||
beginTime = time.beginTime,
|
||||
endTime = time.endTime,
|
||||
conferenceUrl = conference?.url,
|
||||
dayOfWeek = weekday.id,
|
||||
week = week
|
||||
), null)
|
||||
val weekInfo = weeks.find { w -> w.week == week }
|
||||
return if (teachers.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_TEACHER_FOR_LESSON)
|
||||
else Pair(
|
||||
Lesson(
|
||||
id = id,
|
||||
type = LessonType.getTypeFromName(type.name),
|
||||
discipline = discipline.name,
|
||||
teacher = teachers[0].name,
|
||||
beginTime = time.beginTime,
|
||||
endTime = time.endTime,
|
||||
conferenceUrl = conference?.url,
|
||||
dayOfWeek = weekday.id,
|
||||
week = week,
|
||||
building = weekInfo?.building?.name,
|
||||
room = weekInfo?.room?.name
|
||||
), null
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -57,22 +81,31 @@ data class APIIETLesson(
|
||||
val teachers: List<APILessonTeacher>,
|
||||
val time: APILessonTime,
|
||||
val conference: APILessonConference?,
|
||||
val weekday: APILessonWeekDay
|
||||
val weekday: APILessonWeekDay,
|
||||
val weeks: List<APILessonWeeks>
|
||||
) {
|
||||
fun toLesson(week: Int): Pair<Lesson?, LessonConverterErrorMessage?> {
|
||||
return if(teachers.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_TEACHER_FOR_LESSON)
|
||||
else if(flows.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_DISCIPLINE_FOR_IET_LESSON)
|
||||
else Pair(Lesson(
|
||||
id = id,
|
||||
type = LessonType.getTypeFromName(type.name),
|
||||
discipline = flows[0].discipline.name,
|
||||
teacher = teachers[0].name,
|
||||
beginTime = time.beginTime,
|
||||
endTime = time.endTime,
|
||||
conferenceUrl = conference?.url,
|
||||
dayOfWeek = weekday.id,
|
||||
week = week
|
||||
), null)
|
||||
val weekInfo = weeks.find { w -> w.week == week }
|
||||
return if (teachers.isEmpty()) Pair(null, LessonConverterErrorMessage.NO_TEACHER_FOR_LESSON)
|
||||
else if (flows.isEmpty()) Pair(
|
||||
null,
|
||||
LessonConverterErrorMessage.NO_DISCIPLINE_FOR_IET_LESSON
|
||||
)
|
||||
else Pair(
|
||||
Lesson(
|
||||
id = id,
|
||||
type = LessonType.getTypeFromName(type.name),
|
||||
discipline = flows[0].discipline.name,
|
||||
teacher = teachers[0].name,
|
||||
beginTime = time.beginTime,
|
||||
endTime = time.endTime,
|
||||
conferenceUrl = conference?.url,
|
||||
dayOfWeek = weekday.id,
|
||||
week = week,
|
||||
building = weekInfo?.building?.name,
|
||||
room = weekInfo?.room?.name
|
||||
), null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,13 +119,13 @@ data class APILessons(
|
||||
val exceptions = mutableListOf<LessonConverterErrorMessage>()
|
||||
lessons.forEach { lesson ->
|
||||
val (databaseLesson, exception) = lesson.toLesson(week)
|
||||
if(databaseLesson != null) databaseLessons.add(databaseLesson)
|
||||
if(exception != null) exceptions.add(exception)
|
||||
if (databaseLesson != null) databaseLessons.add(databaseLesson)
|
||||
if (exception != null) exceptions.add(exception)
|
||||
}
|
||||
ietLessons.forEach { ietLesson ->
|
||||
val (databaseIetLesson, exception) = ietLesson.toLesson(week)
|
||||
if(databaseIetLesson != null) databaseLessons.add(databaseIetLesson)
|
||||
if(exception != null) exceptions.add(exception)
|
||||
if (databaseIetLesson != null) databaseLessons.add(databaseIetLesson)
|
||||
if (exception != null) exceptions.add(exception)
|
||||
}
|
||||
return Pair(databaseLessons, exceptions)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ class LessonColors {
|
||||
val Unknown = Color(0xFFE2E2E2)
|
||||
}
|
||||
}
|
||||
|
||||
class Dark {
|
||||
companion object {
|
||||
val Lecture = Color(0xFF444946)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
104
app/src/main/java/com/example/ssau_schedule/widget/Widget.kt
Normal file
104
app/src/main/java/com/example/ssau_schedule/widget/Widget.kt
Normal 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()
|
||||
}
|
||||
|
||||
@@ -1,50 +1,83 @@
|
||||
package com.example.ssau_schedule.work
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import com.example.ssau_schedule.R
|
||||
import com.example.ssau_schedule.api.Http
|
||||
import com.example.ssau_schedule.api.LessonAPI
|
||||
import com.example.ssau_schedule.data.base.Database
|
||||
import com.example.ssau_schedule.data.store.StoreUtils
|
||||
import com.example.ssau_schedule.widget.ScheduleWidget
|
||||
import java.util.Date
|
||||
|
||||
//class RequestLessonsWorker(
|
||||
// private val context: Context,
|
||||
// private val workerParams: WorkerParameters
|
||||
//): CoroutineWorker(context, workerParams) {
|
||||
// private val notificationManager =
|
||||
// context.getSystemService(Context.NOTIFICATION_SERVICE) as
|
||||
// NotificationManager
|
||||
//
|
||||
// override suspend fun doWork(): Result {
|
||||
// val http = Http()
|
||||
// val lessonAPI = LessonAPI(http)
|
||||
// val database = Database.getInstance(context)
|
||||
//
|
||||
// val generalData = StoreUtils.getGeneralData(context) ?: return Result.failure()
|
||||
// val week = inputData.getInt("week", -1)
|
||||
// if(week == -1) return Result.failure()
|
||||
//
|
||||
//
|
||||
// val (apiLessons, apiErrors) = lessonAPI.getLessons(
|
||||
// generalData.token,
|
||||
// generalData.group,
|
||||
// generalData.year,
|
||||
// week,
|
||||
// )
|
||||
// if(apiErrors != null || apiLessons == null) return Result.failure()
|
||||
//
|
||||
// val (lessons, convertErrors) = apiLessons.toLessons(week)
|
||||
// if(convertErrors.isNotEmpty()) {
|
||||
// var builder = NotificationCompat.Builder(context, "1")
|
||||
// .setContentTitle("Title")
|
||||
// .setContentText("Content")
|
||||
// .setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
// }
|
||||
// database.lessonDao().insert(*lessons.to)
|
||||
// }
|
||||
//}
|
||||
|
||||
class RequestLessonsWorker(
|
||||
private val context: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : CoroutineWorker(context, workerParams) {
|
||||
private val notificationManager =
|
||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as
|
||||
NotificationManager
|
||||
private val channelId = "ssau_schedule_1"
|
||||
private val notificationId = 1234
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
override suspend fun doWork(): Result {
|
||||
val mChannel = NotificationChannel(
|
||||
channelId,
|
||||
"SSAUScheduleNotificationChannel", NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
mChannel.enableLights(true)
|
||||
mChannel.enableVibration(true)
|
||||
notificationManager.createNotificationChannel(mChannel)
|
||||
|
||||
val http = Http()
|
||||
val lessonAPI = LessonAPI(http)
|
||||
val database = Database.getInstance(context)
|
||||
|
||||
val generalData = StoreUtils.getGeneralData(context)
|
||||
if (generalData == null) {
|
||||
pushErrorNotification()
|
||||
return Result.failure()
|
||||
}
|
||||
val week = generalData.year.getWeekOfDate(Date())
|
||||
val (apiLessons, apiErrors) = lessonAPI.getLessons(
|
||||
generalData.token,
|
||||
generalData.group,
|
||||
generalData.year,
|
||||
week,
|
||||
)
|
||||
if (apiErrors != null || apiLessons == null) {
|
||||
pushErrorNotification()
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
val (lessons, convertErrors) = apiLessons.toLessons(week)
|
||||
if (convertErrors.isNotEmpty()) {
|
||||
pushErrorNotification()
|
||||
return Result.failure()
|
||||
}
|
||||
database.lessonDao().insert(*lessons.toTypedArray())
|
||||
ScheduleWidget().updateAll(context)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun pushErrorNotification() {
|
||||
notificationManager.notify(
|
||||
notificationId,
|
||||
NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentTitle(context.resources.getString(R.string.failed_get_schedule))
|
||||
.setContentText(context.resources.getString(R.string.log_into_app_to_update))
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,8 @@
|
||||
<string name="education_week">education week</string>
|
||||
<string name="education_year">education year</string>
|
||||
<string name="schedule_for_group">Schedule for the group</string>
|
||||
<string name="failed_get_schedule">Failed to get a schedule!</string>
|
||||
<string name="log_into_app_to_update">Log into the app to update your schedule</string>
|
||||
<string name="no_classes_today">No classes today</string>
|
||||
<string name="no_classes">No classes</string>
|
||||
</resources>
|
||||
@@ -18,4 +18,8 @@
|
||||
<string name="education_week">учебная неделя</string>
|
||||
<string name="education_year">учебный год</string>
|
||||
<string name="schedule_for_group">Расписание для группы</string>
|
||||
<string name="failed_get_schedule">Не удалось получить расписание!</string>
|
||||
<string name="log_into_app_to_update">Войдите в приложение, чтобы обновить расписание</string>
|
||||
<string name="no_classes_today">Сегодня нет занятий</string>
|
||||
<string name="no_classes">Нет занятий</string>
|
||||
</resources>
|
||||
8
app/src/main/res/xml/widget_info.xml
Normal file
8
app/src/main/res/xml/widget_info.xml
Normal 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>
|
||||
Reference in New Issue
Block a user