diff --git a/mobile/.idea/caches/deviceStreaming.xml b/mobile/.idea/caches/deviceStreaming.xml new file mode 100644 index 0000000..406736c --- /dev/null +++ b/mobile/.idea/caches/deviceStreaming.xml @@ -0,0 +1,340 @@ + + + + + + \ No newline at end of file diff --git a/mobile/app/build.gradle.kts b/mobile/app/build.gradle.kts index 8614826..d76082d 100644 --- a/mobile/app/build.gradle.kts +++ b/mobile/app/build.gradle.kts @@ -32,7 +32,7 @@ android { applicationId = "com.odweta.solon" minSdk = 28 targetSdk = 34 - versionCode = 208 + versionCode = 210 versionName = "2.3.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/mobile/app/release/baselineProfiles/0/app-release.dm b/mobile/app/release/baselineProfiles/0/app-release.dm deleted file mode 100644 index 68e52a9..0000000 Binary files a/mobile/app/release/baselineProfiles/0/app-release.dm and /dev/null differ diff --git a/mobile/app/release/baselineProfiles/1/app-release.dm b/mobile/app/release/baselineProfiles/1/app-release.dm deleted file mode 100644 index 11e8fa4..0000000 Binary files a/mobile/app/release/baselineProfiles/1/app-release.dm and /dev/null differ diff --git a/mobile/app/release/output-metadata.json b/mobile/app/release/output-metadata.json index 8c787b5..bc64d41 100644 --- a/mobile/app/release/output-metadata.json +++ b/mobile/app/release/output-metadata.json @@ -11,9 +11,9 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 1, - "versionName": "2.0", - "outputFile": "app-release.apk" + "versionCode": 208, + "versionName": "2.3.0", + "outputFile": "com.odweta.solon_2.3.0.apk" } ], "elementType": "File", @@ -22,14 +22,14 @@ "minApi": 28, "maxApi": 30, "baselineProfiles": [ - "baselineProfiles/1/app-release.dm" + "baselineProfiles/1/com.odweta.solon_2.3.0.dm" ] }, { "minApi": 31, "maxApi": 2147483647, "baselineProfiles": [ - "baselineProfiles/0/app-release.dm" + "baselineProfiles/0/com.odweta.solon_2.3.0.dm" ] } ], diff --git a/mobile/app/src/main/java/com/odweta/solon/Account.kt b/mobile/app/src/main/java/com/odweta/solon/Account.kt index 053c339..aba929c 100644 --- a/mobile/app/src/main/java/com/odweta/solon/Account.kt +++ b/mobile/app/src/main/java/com/odweta/solon/Account.kt @@ -1,5 +1,6 @@ package com.odweta.solon +import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject @@ -8,16 +9,9 @@ import java.nio.charset.Charset class Account(val username: String, val password: String, private val error: Int) { companion object { - private fun parseAccountDataFile(file: File): Account { - val lines: List = file.readLines() - - if (lines.size >= 2) { - if (lines[0] != "" && lines[1] != "") { - return Account(lines[0], lines[1], 0) - } - } - - return Account("", "", 1) + private fun parseAccountDataFile(): Account { + val lines: List = File(global.fileDir, global.filePaths.accountData).readLines() + return Account(lines[0], lines[1], 0) } fun haveSaved(): Boolean { @@ -27,15 +21,17 @@ class Account(val username: String, val password: String, private val error: Int */ // check if file exists - val file = File(global.fileDir, global.filePaths.accountData) - return if (!file.exists()) { + return if ( + !Util.fileExists(global.filePaths.accountData) || + Util.fileIsEmpty(global.filePaths.accountData) + ) { // it does not exist - file.createNewFile() + File(global.fileDir, global.filePaths.accountData).createNewFile() false } else { // it exists, so it shall be parsed - val acc: Account = parseAccountDataFile(file) + val acc: Account = parseAccountDataFile() global.username = acc.username global.password = acc.password acc.error == 0 @@ -43,23 +39,20 @@ class Account(val username: String, val password: String, private val error: Int } fun save(acc: Account): Boolean { - return try { - val file = File(global.fileDir, global.filePaths.accountData) - file.writeText("${acc.username}\n${acc.password}\n", Charset.forName("UTF-8")) - - true - } catch (e: Exception) { - false - } + val payload = "${acc.username}\n${acc.password}" + return Util.saveStringToFile(global.filePaths.accountData, payload) } - suspend fun isValid(acc: Account): Boolean { - return withContext(Dispatchers.IO) { - global.username = acc.username - global.password = acc.password + fun isValid(acc: Account): Boolean { + global.username = acc.username + global.password = acc.password - val token = networkRequestFactory.perform(NetworkRequestType.Token).getString("token") - !JSONObject(token).has("error") + val tokenJSON = networkRequestFactory.perform(NetworkRequestType.Token) + return if (tokenJSON.has("token")) { + val token = tokenJSON.getString("token") + !token.contains("\"error\"") + } else { + false } } } diff --git a/mobile/app/src/main/java/com/odweta/solon/Global.kt b/mobile/app/src/main/java/com/odweta/solon/Global.kt index e4e8dc3..295f395 100644 --- a/mobile/app/src/main/java/com/odweta/solon/Global.kt +++ b/mobile/app/src/main/java/com/odweta/solon/Global.kt @@ -76,7 +76,15 @@ fun String.toLectureNoteType(): LectureNoteType { } } -class LectureNote(id: Int, note: MutableState, type: MutableState, number: Int, lectureName: String) { +data class LectureNote( + var id: Int, + var note: MutableState = mutableStateOf(""), + var type: MutableState = mutableStateOf(LectureNoteType.Normal), + var number: Int, + var lectureName: String +) + +class LectureNoteOLD(id: Int, note: MutableState, type: MutableState, number: Int, lectureName: String) { var id: Int = 0 var note: MutableState = mutableStateOf("") diff --git a/mobile/app/src/main/java/com/odweta/solon/Launch.kt b/mobile/app/src/main/java/com/odweta/solon/Launch.kt index f0671d5..850f6b4 100644 --- a/mobile/app/src/main/java/com/odweta/solon/Launch.kt +++ b/mobile/app/src/main/java/com/odweta/solon/Launch.kt @@ -81,6 +81,7 @@ fun launchContinue( global.settings = settingsData // load the lecture notes + Util.loadLectureNotes() Util.csvToLectureNotes() // listen for internet availability in case of disconnection diff --git a/mobile/app/src/main/java/com/odweta/solon/Marks.kt b/mobile/app/src/main/java/com/odweta/solon/Marks.kt index 27fb11c..4975ed9 100644 --- a/mobile/app/src/main/java/com/odweta/solon/Marks.kt +++ b/mobile/app/src/main/java/com/odweta/solon/Marks.kt @@ -14,12 +14,16 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AutoAwesome +import androidx.compose.material.icons.filled.WorkspacePremium +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -57,6 +61,7 @@ import java.time.LocalDate import java.time.format.DateTimeFormatter var showNewMarksDialog by mutableStateOf(false) +var showFinalMarksDialog by mutableStateOf(false) enum class MarksDepthLevel { All, @@ -71,8 +76,14 @@ fun MarksScreen() { MarksDepthLevel.Subject -> MarksSubject() MarksDepthLevel.Detail -> MarksDetail() } + NewMarksDialog() + FinalMarksDialog() +} + +@Composable +fun NewMarksDialog() { if (showNewMarksDialog) { - val scrollStateNewMarks = rememberScrollState() + val ss = rememberScrollState() Dialog( onDismissRequest = { showNewMarksDialog = false }, @@ -82,7 +93,9 @@ fun MarksScreen() { ) ) { Column( - Modifier.verticalScroll(scrollStateNewMarks).padding(vertical = 10.dp), + Modifier + .verticalScroll(ss) + .padding(vertical = 10.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( @@ -91,14 +104,17 @@ fun MarksScreen() { fontWeight = FontWeight.Bold, fontSize = 30.sp, textAlign = TextAlign.Center, - modifier = Modifier.background( - color = Color.Black, - shape = RoundedCornerShape(global.dimens.roundedCorner) - ).border( - shape = RoundedCornerShape(global.dimens.roundedCorner), - color = PastelYellow, - width = global.dimens.borderWidth - ).padding(10.dp) + modifier = Modifier + .background( + color = Color.Black, + shape = RoundedCornerShape(global.dimens.roundedCorner) + ) + .border( + shape = RoundedCornerShape(global.dimens.roundedCorner), + color = PastelYellow, + width = global.dimens.borderWidth + ) + .padding(10.dp) ) Spacer(Modifier.height(10.dp)) for ((k, v) in global.newMarks) { @@ -133,7 +149,8 @@ fun MarksScreen() { Text( i.theme, color = Color.White, - modifier = Modifier.padding(end = 5.dp, top = 5.dp) + modifier = Modifier + .padding(end = 5.dp, top = 5.dp) .weight(1f) ) Column( @@ -146,7 +163,8 @@ fun MarksScreen() { .background( color = getAvgColor(i.grade, true), shape = RoundedCornerShape(global.dimens.roundedCorner) - ).padding(5.dp), + ) + .padding(5.dp), style = TextStyle( shadow = Shadow( color = Color.Black, @@ -173,6 +191,47 @@ fun MarksScreen() { } } +@Composable +fun FinalMarksDialog() { + if (showFinalMarksDialog) { + val ss = rememberScrollState() + + Dialog( + onDismissRequest = { showFinalMarksDialog = false }, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true + ) + ) { + Column( + Modifier + .verticalScroll(ss) + .padding(vertical = 10.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + "Uzavřené známky", + color = Color.White, + fontWeight = FontWeight.Bold, + fontSize = 30.sp, + textAlign = TextAlign.Center, + modifier = Modifier + .background( + color = Color.Black, + shape = RoundedCornerShape(global.dimens.roundedCorner) + ) + .border( + shape = RoundedCornerShape(global.dimens.roundedCorner), + color = PastelYellow, + width = global.dimens.borderWidth + ) + .padding(10.dp) + ) + } + } + } +} + private fun getAvgColor(avg: String, isBorder: Boolean = false): Color { val aux = avg.replace(",", ".").toFloat() return if (aux in 1f..<1.5f) { @@ -360,21 +419,46 @@ fun MarksAll() { } Box(Modifier.fillMaxSize()) { - FloatingActionButton( - onClick = { showNewMarksDialog = true }, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(10.dp) - .background(Color.Black, shape = RoundedCornerShape(global.dimens.roundedCorner)), - containerColor = Color.Black, - contentColor = PastelYellow + Row( + modifier = Modifier.align(Alignment.BottomCenter) ) { - Icon( - modifier = Modifier.padding(start = 4.dp, top = 4.dp), - imageVector = Icons.Default.AutoAwesome, - contentDescription = "New marks in subject", - tint = PastelYellow // You can customize the icon color - ) + FloatingActionButton( + onClick = { showNewMarksDialog = true }, + modifier = Modifier +// .size(40.dp) + .padding(10.dp) + .background( + Color.Black, + shape = RoundedCornerShape(global.dimens.roundedCorner) + ), + contentColor = PastelYellow, + containerColor = Color.Black + ) { + Icon( + imageVector = Icons.Default.AutoAwesome, + contentDescription = "New marks in subject", + tint = PastelYellow // You can customize the icon color + ) + } + + FloatingActionButton( + onClick = { showFinalMarksDialog = true }, + modifier = Modifier +// .size(40.dp) + .padding(10.dp) + .background( + Color.Black, + shape = RoundedCornerShape(global.dimens.roundedCorner) + ), + contentColor = PastelYellow, + containerColor = Color.Black + ) { + Icon( + imageVector = Icons.Default.WorkspacePremium, + contentDescription = "Final marks", + tint = PastelYellow // You can customize the icon color + ) + } } } } diff --git a/mobile/app/src/main/java/com/odweta/solon/Menu.kt b/mobile/app/src/main/java/com/odweta/solon/Menu.kt index 1b52f85..26a7dc6 100644 --- a/mobile/app/src/main/java/com/odweta/solon/Menu.kt +++ b/mobile/app/src/main/java/com/odweta/solon/Menu.kt @@ -40,7 +40,10 @@ fun MenuScreen() { Spacer(modifier = Modifier.height(80.dp)) - MenuButton({ global.currentScreen = Screen.SignIn }, "Změnit přihlášení") + MenuButton({ + global.noSignInBackHandle = false + global.currentScreen = Screen.SignIn + }, "Změnit přihlášení") Spacer(modifier = Modifier.height(80.dp)) diff --git a/mobile/app/src/main/java/com/odweta/solon/Network.kt b/mobile/app/src/main/java/com/odweta/solon/Network.kt index 5472c91..889a9e2 100644 --- a/mobile/app/src/main/java/com/odweta/solon/Network.kt +++ b/mobile/app/src/main/java/com/odweta/solon/Network.kt @@ -11,15 +11,21 @@ import org.json.JSONObject fun setupAPI() { global.token = "NETWORK_UNREACHABLE" - val token = networkRequestFactory.perform(NetworkRequestType.Token).getString("token") - if (""""error": """ !in token) { - global.token = token + + val tokenJSON = networkRequestFactory.perform(NetworkRequestType.Token) + if (tokenJSON.has("token")) { + val token = tokenJSON.getString("token") + if ("\"error\": " !in token) { + global.token = token + } } - if (global.token == "NETWORK_UNREACHABLE" || """"error": """ in global.token) { + + if (global.token == "NETWORK_UNREACHABLE" || "\"error\":" in global.token) { global.userData = JSONObject() global.studentId = "" return } + global.userData = networkRequestFactory.perform(NetworkRequestType.UserData) Util.saveStringToFile(global.filePaths.userData, global.userData.toString()) global.studentClass = Parsing.getStudentClass() @@ -90,7 +96,6 @@ object NetworkRequestFactory: NetworkRequestInterface { } private fun getAPI(url: String): JSONObject { - Log.d("debug TOKEN", global.token) val response = get(url, headersOf( "Content-Type", "application/x-www-form-urlencoded", "Authorization", "Bearer ${global.token}" diff --git a/mobile/app/src/main/java/com/odweta/solon/Parsing.kt b/mobile/app/src/main/java/com/odweta/solon/Parsing.kt index 9af49e8..f1d5f6b 100644 --- a/mobile/app/src/main/java/com/odweta/solon/Parsing.kt +++ b/mobile/app/src/main/java/com/odweta/solon/Parsing.kt @@ -153,12 +153,15 @@ class Parsing { subject.name = subj.getJSONObject("subject").getString("abbrev") subject.place = subj.getJSONArray("rooms").getJSONObject(0).getString("abbrev") - subject.teacher = if (subj.getJSONArray("teachers").length() > 0) { - subj.getJSONArray("teachers").getJSONObject(0) - .getString("fullName") // TODO: keep in mind that fullName might not be a valid field (name and surname are!) - } else { - "" - } + subject.teacher = + if (subj.getJSONArray("teachers").length() > 0) { + subj.getJSONArray("teachers") + .getJSONObject(0).getString("name") + " " + + subj.getJSONArray("teachers") + .getJSONObject(0).getString("name") + } else { + "" + } subject.substitute = when (type) { "ROZVRH" -> "0" "SUPLOVANI" -> "1" @@ -322,7 +325,6 @@ class Parsing { timetable.add(day) } - Log.d("debug", "timetable: $timetable") return timetable } diff --git a/mobile/app/src/main/java/com/odweta/solon/SignIn.kt b/mobile/app/src/main/java/com/odweta/solon/SignIn.kt index 48e56c3..f29c248 100644 --- a/mobile/app/src/main/java/com/odweta/solon/SignIn.kt +++ b/mobile/app/src/main/java/com/odweta/solon/SignIn.kt @@ -1,6 +1,7 @@ package com.odweta.solon import android.content.Context +import android.util.Log import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image @@ -40,6 +41,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext private var buttonText by mutableStateOf("Přihlásit se") @@ -96,11 +98,13 @@ fun signInButtonCallback(ctx: Context, usernameText: String, passwordText: Strin global.currentScreen = Screen.Splash launchStart(ctx) } else { - Toast.makeText( - ctx, - "Neplatné přihlašovací údaje nebo nejste připojeni k internetu, zkuste to znovu.", - Toast.LENGTH_LONG - ).show() + withContext(Dispatchers.Main) { + Toast.makeText( + ctx, + "Neplatné přihlašovací údaje nebo nejste připojeni k internetu, zkuste to znovu.", + Toast.LENGTH_LONG + ).show() + } } } } @@ -115,7 +119,6 @@ fun SignInScreen() { var usernameText by remember { mutableStateOf("") } var passwordText by remember { mutableStateOf("") } val ctx = LocalContext.current - val coscope = rememberCoroutineScope() var isPasswordVisible by remember { mutableStateOf(false) } // State for visibility toggle val visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation() diff --git a/mobile/app/src/main/java/com/odweta/solon/Timetable.kt b/mobile/app/src/main/java/com/odweta/solon/Timetable.kt index 47c4b8c..0357015 100644 --- a/mobile/app/src/main/java/com/odweta/solon/Timetable.kt +++ b/mobile/app/src/main/java/com/odweta/solon/Timetable.kt @@ -356,6 +356,8 @@ private fun DayScreen(day: Day) { ) { for ((subjectIndex, subject) in day.subjects.withIndex()) { val lectureId = Util.getLectureId(day.date, subjectIndex) + global.lectureNotes[lectureId]?.number = subject.number.toInt() + global.lectureNotes[lectureId]?.lectureName = subject.name val amoledModifier = if (settings.useAmoledMode.checked.value) { Modifier