10 Commits
13 changed files with 234 additions and 72 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
![solon-logo](solon_logo.png)
# Zorientujte se rychleji v této odlehčené verzi Školy OnLine
# Odkaz na stažení aplikace pro Android [zde](https://gitlab.com/Odweta/solon/-/releases/2.4/downloads/app-release.apk) (verze 2.4)
# Odkaz na stažení aplikace pro Android [zde](https://gitlab.com/Odweta/solon/-/releases/2.5/downloads/app-release.apk) (verze 2.5)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Nyní na F-Droidu"
height="80">](https://f-droid.org/packages/com.odweta.solon/)
+1 -1
View File
@@ -28,7 +28,7 @@ echo
echo
echo "[signing]"
echo
~/Android/Sdk/build-tools/35.0.0/apksigner sign --alignment-preserved --ks ~/Documents/android-keystore.jks --ks-pass pass:$ks_pass app/build/outputs/apk/release/com.odweta.solon_$ver.apk
~/Android/Sdk/build-tools/35.0.1/apksigner sign --alignment-preserved --ks ~/Documents/android-keystore.jks --ks-pass pass:$ks_pass app/build/outputs/apk/release/com.odweta.solon_$ver.apk
# create the tag
echo
+24
View File
@@ -0,0 +1,24 @@
#!/data/data/com.termux/files/usr/bin/bash
cd mobile
INTERNAL_STORAGE=/data/data/com.termux/files/home/storage/downloads/..
echo -n 'keystore key for signing: '
read kskey
echo 'building the release bundle'
gradle bundleRelease
mkdir -p $INTERNAL_STORAGE/solon-bin/
cp $SOLON/mobile/app/build/outputs/bundle/release/app-release.aab $INTERNAL_STORAGE/solon-bin/
echo 'app-bundle.aab copied to \$INTERNAL_STORAGE/solon-bin'
apksigner sign --ks $INTERNAL_STORAGE/Documents/android-keystore.jks --ks-pass pass:$kskey --min-sdk-version=28 $INTERNAL_STORAGE/solon-bin/app-release.aab
echo 'aab signed with default android keystore'
cd ..
+1
View File
@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
+3 -3
View File
@@ -12,7 +12,7 @@ tasks.register("printVersionName") {
tasks.register("deploy") {
dependsOn("assembleDebug")
dependsOn("installDebug")
//dependsOn("installDebug")
doLast {
exec {
commandLine("adb", "shell", "am", "start", "-n", "com.odweta.solon/.MainActivity")
@@ -32,8 +32,8 @@ android {
applicationId = "com.odweta.solon"
minSdk = 28
targetSdk = 34
versionCode = 211
versionName = "2.4"
versionCode = 213
versionName = "2.5"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -199,8 +199,10 @@ enum class ScreenElement {
class Global {
lateinit var timetableJSON: JSONObject
lateinit var marksJSON: JSONObject
lateinit var finalMarksJSON: JSONObject
lateinit var marks: MutableMap<String, MutableList<Mark>>
lateinit var newMarks: MutableMap<String, MutableList<Mark>>
lateinit var finalMarks: Map<String, Int>
lateinit var settings: SettingsData
lateinit var fileDir: File
@@ -237,6 +239,8 @@ class Global {
var subjectToShow by mutableStateOf("")
var markToShow by mutableStateOf(Mark())
var demoMode: Boolean = false
inner class FilePaths {
val accountData: String = "account_data.txt"
val timetable: String = "timetable.json"
@@ -244,6 +248,7 @@ class Global {
val marks: String = "marks.json"
val settings: String = "settings.txt"
val lectureNotes: String = "lecture_notes.csv"
val finalMarks: String = "final_marks.txt"
}
val filePaths = FilePaths()
@@ -253,6 +258,7 @@ class Global {
val userData = "$apiBase/v1/user"
val timetable = "$apiBase/v1/timeTable"
var marks: String = "$apiBase/v1/students/<student id>/marks/list"
var finalMarks: String ="$apiBase/v1/students/<student id>/marks/final"
}
val apiEndPoints = APIEndPoints()
@@ -34,15 +34,18 @@ fun remoteLoad(ctx: Context) {
// fetch remote data
global.timetableJSON = networkRequestFactory.perform(NetworkRequestType.Timetable)
global.marksJSON = networkRequestFactory.perform(NetworkRequestType.Marks)
global.finalMarksJSON = networkRequestFactory.perform(NetworkRequestType.FinalMarks)
// save to local for later local load
Util.saveStringToFile(global.filePaths.timetable, global.timetableJSON.toString())
Util.saveStringToFile(global.filePaths.marks, global.marksJSON.toString())
Util.saveStringToFile(global.filePaths.finalMarks, global.finalMarksJSON.toString())
launchContinue(
ctx,
global.timetableJSON,
global.marksJSON
global.marksJSON,
global.finalMarksJSON
)
}
} else {
@@ -50,7 +53,8 @@ fun remoteLoad(ctx: Context) {
launchContinue(
ctx,
networkRequestFactory.perform(NetworkRequestType.Timetable),
networkRequestFactory.perform(NetworkRequestType.Marks)
networkRequestFactory.perform(NetworkRequestType.Marks),
networkRequestFactory.perform(NetworkRequestType.FinalMarks)
)
}
}
@@ -61,39 +65,43 @@ fun localLoad(ctx: Context) {
global.studentId = Parsing.getStudentId()
global.studentClass = Parsing.getStudentClass()
global.apiEndPoints.marks = "${global.apiEndPoints.apiBase}/v1/students/${global.studentId}/marks/list"
global.apiEndPoints.finalMarks = "${global.apiEndPoints.apiBase}/v1/students/${global.studentId}/marks/final"
val localTimetable = JSONObject(Util.fileToString(global.filePaths.timetable))
val localMarks = JSONObject(Util.fileToString(global.filePaths.marks))
val localFinalMarks = JSONObject(Util.fileToString(global.filePaths.finalMarks))
launchContinue(
ctx,
localTimetable,
localMarks
localMarks,
localFinalMarks
)
}
fun launchContinue(
ctx: Context,
timetable: JSONObject,
marks: JSONObject
marks: JSONObject,
finalMarks: JSONObject
) {
// parse timetable and marks
val parsedTimetable = Parsing.parseTimetable(timetable)
global.timetable = parsedTimetable
val parsedMarks = Parsing.parseMarks(marks)
global.marks = parsedMarks
global.timetable = Parsing.parseTimetable(timetable)
global.marks = Parsing.parseMarks(marks)
global.newMarks = Parsing.parseNewMarks(marks)
// // load the settings
// global.settings = Util.fileToString(global.filePaths.settings).toSettingsData()
global.finalMarks = Parsing.parseFinalMarks(finalMarks)
// load the lecture notes
Util.loadLectureNotes()
Util.csvToLectureNotes()
//Util.loadLectureNotes()
//Util.csvToLectureNotes()
// load the settings
settings = Util.fileToString(global.filePaths.settings).toSettingsData()
// listen for internet availability in case of disconnection
if (!global.internetAwaitListenerRunning)
setOnInternetAvailListener(ctx)
// FIXME maybe? causes IndexOutOfBoundsException????? prob not tho
if (!global.launched) {
// show the default (main) screen
global.currentScreen = Screen.Main
@@ -1,5 +1,6 @@
package com.odweta.solon
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -25,6 +26,7 @@ import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -121,7 +123,7 @@ fun NewMarksScreen() {
modifier = Modifier.verticalScroll(ss)
) {
for ((k, v) in global.newMarks) {
if (v.size > 0) {
if (v.isNotEmpty()) {
Column(
modifier = Modifier
.padding(horizontal = 10.dp)
@@ -242,14 +244,16 @@ fun FinalMarksScreen() {
Column(
modifier = Modifier.verticalScroll(ss)
) {
Text("test", color = Color.White)
Text("test", color = Color.White)
Text("test", color = Color.White)
Text("test", color = Color.White)
Text("test", color = Color.White)
Text("test", color = Color.White)
Text("test", color = Color.White)
Text("test", color = Color.White)
Text(Util.fileToString(global.filePaths.finalMarks), color = Color.White)
for ((subject, mark) in global.finalMarks) {
// LaunchedEffect(Unit) {
// Log.d("debug", networkRequestFactory.perform(NetworkRequestType.Apologies).body.string?.toString())
//}
Row {
Text(subject, color = Color.White)
Text(mark.toString(), color = Color.White, modifier = Modifier.padding(10.dp))
}
}
}
}
}
@@ -337,8 +341,10 @@ fun MarksAll() {
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.clickable {
global.marksDepthLevel = MarksDepthLevel.Subject
global.subjectToShow = subject
if (markList[0].grade != "0") {
global.marksDepthLevel = MarksDepthLevel.Subject
global.subjectToShow = subject
} // else { isInDemo == true }
}
} else {
Modifier
@@ -349,8 +355,10 @@ fun MarksAll() {
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.clickable {
global.marksDepthLevel = MarksDepthLevel.Subject
global.subjectToShow = subject
if (markList[0].grade != "0") {
global.marksDepthLevel = MarksDepthLevel.Subject
global.subjectToShow = subject
} // else { isInDemo == true }
}
}
@@ -463,7 +471,7 @@ fun MarksAll() {
)
}
FloatingActionButton(
/*FloatingActionButton(
onClick = { global.currentScreenElement = ScreenElement.FinalMarks },
modifier = Modifier
.padding(10.dp)
@@ -479,7 +487,7 @@ fun MarksAll() {
contentDescription = "Final marks",
tint = PastelYellow // You can customize the icon color
)
}
}*/
}
}
}
@@ -32,6 +32,7 @@ fun setupAPI() {
global.studentId = Parsing.getStudentId()
global.apiEndPoints.marks = "${global.apiEndPoints.apiBase}/v1/students/${global.studentId}/marks/list"
global.apiEndPoints.finalMarks = "${global.apiEndPoints.apiBase}/v1/students/${global.studentId}/marks/final"
global.isAPISetUp = true
}
@@ -40,6 +41,7 @@ enum class NetworkRequestType {
Token,
Timetable,
Marks,
FinalMarks,
UserData
}
@@ -71,6 +73,7 @@ object NetworkRequestFactory: NetworkRequestInterface {
}
NetworkRequestType.Timetable -> return getAPI(global.apiEndPoints.timetable)
NetworkRequestType.Marks -> return getAPI(global.apiEndPoints.marks)
NetworkRequestType.FinalMarks -> return getAPI(global.apiEndPoints.finalMarks)
NetworkRequestType.UserData -> return getAPI(global.apiEndPoints.userData)
}
}
@@ -82,7 +85,13 @@ object NetworkRequestFactory: NetworkRequestInterface {
.get()
.build()
return client.newCall(request).execute()
return try {
client.newCall(request).execute()
} catch (e: Exception) {
Response.Builder()
.code(500)
.build()
}
}
private fun post(url: String, headers: Headers, body: FormBody): Response {
@@ -92,7 +101,13 @@ object NetworkRequestFactory: NetworkRequestInterface {
.post(body)
.build()
return client.newCall(request).execute()
return try {
client.newCall(request).execute()
} catch (e: Exception) {
Response.Builder()
.code(500)
.build()
}
}
private fun getAPI(url: String): JSONObject {
@@ -386,16 +386,6 @@ class Parsing {
for (i in 0..<marksJSONArray.length()) {
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
}
for (subj in subjectMap.values) {
@@ -416,6 +406,14 @@ class Parsing {
return marks
}
fun parseFinalMarks(json: JSONObject): Map<String, Int> {
return mapOf(
"AJ" to 1,
"B" to 5,
"Pr" to 1
)
}
fun getStudentClass(): String {
return if (!userIsParent()) {
global.userData
@@ -45,7 +45,9 @@ fun SettingsScreen() {
// load the settings dat every time the settings screen is opened,
// maybe i can add a flag that determines if the settings are up to date?
settings = Util.fileToString(global.filePaths.settings).toSettingsData()
if (!global.demoMode) {
settings = Util.fileToString(global.filePaths.settings).toSettingsData()
}
val scrollState = rememberScrollState()
@@ -60,7 +62,9 @@ fun SettingsScreen() {
SettingsTableRow(settings.useAmoledMode)
SettingsTableRow(settings.showRefreshButton)
if (!global.demoMode) {
SettingsTableRow(settings.showRefreshButton)
}
SettingsTableRow(settings.useAnimations)
}
@@ -89,26 +89,55 @@ fun signInButtonCallback(ctx: Context, usernameText: String, passwordText: Strin
}
CoroutineScope(Dispatchers.IO).launch {
val valid = Account.isValid(acc)
checked = true
if (valid) {
Account.save(acc)
global.currentScreen = Screen.Splash
launchStart(ctx)
if (global.username == "demo" && global.password == "Demo1234") {
// start the demo
launchDemo(ctx)
} else {
withContext(Dispatchers.Main) {
Toast.makeText(
ctx,
"Neplatné přihlašovací údaje nebo nejste připojeni k internetu, zkuste to znovu.",
Toast.LENGTH_LONG
).show()
val valid = Account.isValid(acc)
checked = true
if (valid) {
Account.save(acc)
global.currentScreen = Screen.Splash
launchStart(ctx)
} else {
withContext(Dispatchers.Main) {
Toast.makeText(
ctx,
"Neplatné přihlašovací údaje nebo nejste připojeni k internetu, zkuste to znovu.",
Toast.LENGTH_LONG
).show()
}
}
}
}
}
fun launchDemo(ctx: Context) {
global.demoMode = true
settings = Util.fileToString(global.filePaths.settings).toSettingsData()
settings.showRefreshButton.checked.value = false
val d = Day()
d.name = "demoday"
d.date = "1. 1."
d.subjects = listOf(Subject.empty())
global.timetable = listOf(d)
global.marks = mutableMapOf("demomarks" to mutableListOf(Mark().none()))
global.newMarks = global.marks
global.status = "demo"
global.launched = true
global.refreshed = true
global.currentScreen = Screen.Main
global.currentScreenElement = ScreenElement.Timetable
}
@Composable
fun SignInScreen() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -65,8 +66,11 @@ import com.odweta.solon.ui.theme.PastelRed
import com.odweta.solon.ui.theme.PastelRedTransparent
import com.odweta.solon.ui.theme.PastelYellow
import com.odweta.solon.ui.theme.PastelYellowTransparent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
@@ -74,14 +78,63 @@ import java.time.format.DateTimeFormatter
var showDetailDialog by mutableStateOf(false)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TimetableScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
val pagerState = rememberPagerState(pageCount = { if (global.timetable.isNotEmpty()) global.timetable.size else 6 }) // FIXME
Column {
//QuickDays(pagerState)
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
DayPager(pagerState)
if (showDetailDialog) DetailDialog()
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun QuickDays(pagerState: PagerState) {
@Composable
fun QuickDay(day: Day, modifier: Modifier, idx: Int) {
val scope = rememberCoroutineScope()
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = modifier.padding(5.dp).background(
shape = RoundedCornerShape(global.dimens.roundedCorner),
color = PastelYellowTransparent
).clickable(onClick = {
scope.launch {
withContext(Dispatchers.Main) {
pagerState.animateScrollToPage(idx)
}
}
})
) {
Text(
text = "${day.name[0]}${day.name[1]}",
color = Color.White,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold
)
Spacer(Modifier.height(5.dp))
Text(
text = day.date,
color = Color.White,
textAlign = TextAlign.Center
)
}
}
Row(
Modifier.fillMaxWidth().padding(bottom = 15.dp).padding(horizontal = 5.dp)
) {
DayPager()
if (showDetailDialog) DetailDialog()
for ((idx, day) in global.timetable.withIndex()) {
QuickDay(day, Modifier.weight(1f), idx)
}
}
}
@@ -227,13 +280,28 @@ fun DetailDialog() {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DayPager() {
val pagerState = rememberPagerState(pageCount = { if (global.timetable.isNotEmpty()) global.timetable.size else 6 }) // FIXME
fun DayPager(pagerState: PagerState) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize()
) { pageIndex ->
DayScreen(global.timetable[pageIndex])
if (global.timetable.isNotEmpty()) {
DayScreen(global.timetable[pageIndex])
} else {
NoDayScreen()
}
}
}
@Composable
fun NoDayScreen() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
"Zatím žádný rozvrh.",
color = Color.White,
fontSize = 30.sp,
fontStyle = FontStyle.Italic
)
}
}
@@ -258,6 +326,7 @@ private fun getSubjectColor(s: Subject, transparent: Boolean): Color {
}
}
}
fun isTimeBetweenT(givenTime: LocalTime, startTime: LocalTime, endTime: LocalTime): Boolean {
// Check if the given time is between startTime and endTime
return givenTime.isAfter(startTime) && givenTime.isBefore(endTime)
@@ -534,10 +603,10 @@ private fun DayScreen(day: Day) {
Box(modifier = Modifier
.fillMaxSize()
.zIndex(99f)
.clickable {
/*.clickable {
global.selectedLecture = lectureId
showDetailDialog = true
})
}*/)
if (
(global.lectureNotes[global.selectedLecture] != null &&