refined the launch proccess and improved the architecture of global variables

This commit is contained in:
Odweta
2024-12-20 17:31:28 +01:00
parent 3240313f75
commit f9d4ff562d
14 changed files with 403 additions and 113 deletions
@@ -38,10 +38,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun AboutScreen(modifier: Modifier = Modifier) {
fun AboutScreen() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler {
isShowing.value = IsShowing.Menu
isShowing = IsShowing.Menu
}
Column(
@@ -50,35 +50,7 @@ fun AboutScreen(modifier: Modifier = Modifier) {
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(start = 5.dp)
) {
Button(
onClick = {
isShowing.value = IsShowing.Menu
},
colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
containerColor = Color.Black
),
modifier = Modifier
.border(
width = 5.dp,
shape = RoundedCornerShape(roundedCornerDimen),
color = PastelYellow
)
.width(70.dp)
.height(70.dp)
) {
Icon(
imageVector = Icons.Default.ArrowBackIosNew,
contentDescription = "Back icon",
modifier = Modifier.size(40.dp)
)
}
}
backButtonFactory.get(BackButtonType.Menu)()
Column(
modifier = Modifier.fillMaxWidth(),
@@ -126,7 +98,7 @@ fun AboutScreen(modifier: Modifier = Modifier) {
}
@Composable
fun HorizontalLine(modifier: Modifier = Modifier) {
fun HorizontalLine() {
Spacer(modifier = Modifier.height(20.dp))
Spacer(modifier = Modifier
@@ -0,0 +1,75 @@
package com.odweta.solon
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
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.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBackIosNew
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.odweta.solon.ui.theme.PastelYellow
enum class BackButtonType {
Menu,
MarksSubject,
MarksAll
}
interface BackButtonFactoryInterface {
@Composable
fun get(type: BackButtonType): @Composable () -> Unit
}
@Composable
fun BackButton(onClick: () -> Unit) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(start = 5.dp)
) {
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
containerColor = Color.Black
),
modifier = Modifier
.border(
width = 5.dp,
shape = RoundedCornerShape(roundedCornerDimen),
color = PastelYellow
)
.width(70.dp)
.height(70.dp)
) {
Icon(
imageVector = Icons.Default.ArrowBackIosNew,
contentDescription = "Back icon",
modifier = Modifier.size(40.dp)
)
}
}
}
object BackButtonFactory: BackButtonFactoryInterface {
@Composable
override fun get(type: BackButtonType): @Composable () -> Unit {
return when (type) {
BackButtonType.Menu -> { { BackButton({ isShowing = IsShowing.Menu }) } }
BackButtonType.MarksAll -> { { BackButton({ marksShow = MarksShow.All }) } }
BackButtonType.MarksSubject -> { { BackButton({ marksShow = MarksShow.Subject }) } }
}
}
}
val backButtonFactory = BackButtonFactory
@@ -7,14 +7,16 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import com.odweta.solon.ui.theme.PastelYellow
import okhttp3.OkHttpClient
import org.json.JSONObject
import java.io.File
// Global values (dimens, modifiers, ...)
@@ -72,7 +74,7 @@ var screenToShow: MutableState<Screen> = mutableStateOf(Screen.Main)
var signedInAndReadyToLaunch: MutableState<Boolean> = mutableStateOf(false)
var isShowing: MutableState<IsShowing> = mutableStateOf(IsShowing.Timetable)
var isShowing by mutableStateOf(IsShowing.Timetable)
var toolbarText: MutableState<String> = mutableStateOf("")
@@ -280,4 +282,46 @@ class Mark {
return mark
}
}
}
object FilePaths {
const val accountData: String = "account_data.txt"
const val timeTable: String = "timetable.json"
const val userData: String = "userdata.json"
const val marks: String = "marks.json"
const val settings: String = "settings.txt"
const val lectureNotes: String = "lecture_notes.csv"
}
object APIEndpoints {
const val apiBaseUrl = "https://aplikace.skolaonline.cz/solapi/api"
const val tokenUrl = "$apiBaseUrl/connect/token"
lateinit var token: String
const val userDataUrl = "$apiBaseUrl/v1/user"
lateinit var userData: String
lateinit var studentId: String
const val timeTableUrl = "$apiBaseUrl/v1/timeTable"
var marksUrl = "$apiBaseUrl/v1/students/$studentId/marks/list?SemesterId=$syid"
}
enum class MainElement {
Timetable,
Marks,
Menu
}
object Global {
lateinit var token: String
lateinit var userData: JSONObject
lateinit var timetable: List<Day>
lateinit var marks: MutableMap<String, MutableList<Mark>>
var noSignInBackHandle: Boolean = false
val filePaths: FilePaths = FilePaths
val apiEndPoints: APIEndpoints = APIEndpoints
lateinit var settings: SettingsData
var lectureNotes: HashMap<Int, LectureNote> = hashMapOf()
var currentScreen by mutableStateOf(Screen.Main)
var currentMainElement by mutableStateOf(MainElement.Timetable)
}
val global = Global
@@ -0,0 +1,111 @@
package com.odweta.solon
import android.content.Context
import androidx.compose.runtime.mutableStateOf
import com.odweta.solon.Util.Companion.getLectureId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.json.JSONObject
fun launchStart(ctx: Context) {
if (Account.haveSaved()) {
if (Util.internetAvail(ctx)) {
remoteLoad(ctx)
} else {
localLoad(ctx)
}
} else {
global.noSignInBackHandle = true
screenToShow.value = Screen.SignIn
}
}
fun remoteLoad(ctx: Context) {
if (!isAPISetUp)
CoroutineScope(Dispatchers.IO)
.launch { Network.setupAPI() }
lateinit var token: String
CoroutineScope(Dispatchers.IO)
.launch { token = Network.getToken() }
global.token = token
lateinit var userData: JSONObject
CoroutineScope(Dispatchers.IO)
.launch { userData = Network.getUserData() }
global.userData = userData
lateinit var remoteTimetable: JSONObject
CoroutineScope(Dispatchers.IO)
.launch { remoteTimetable = Network.getTimeTable() }
lateinit var remoteMarks: JSONObject
CoroutineScope(Dispatchers.IO)
.launch { remoteMarks = Network.getMarks() }
launchContinue(
ctx,
remoteTimetable,
remoteMarks
)
}
fun localLoad(ctx: Context) {
val localTimetable = JSONObject(Util.fileToString(timeTableFilePath))
val localMarks = JSONObject(Util.fileToString(marksFilePath))
launchContinue(
ctx,
localTimetable,
localMarks
)
}
fun launchContinue(
ctx: Context,
timetable: JSONObject,
marks: JSONObject
) {
// parse timetable and marks
val parsedTimetable = Parsing.parseTimetable(timetable)
global.timetable = parsedTimetable
val parsedMarks = Parsing.parseMarks(marks)
global.marks = parsedMarks
// load the settings
val settingsData = Util.fileToString(global.filePaths.settings).toSettingsData()
global.settings = settingsData
// load the lecture notes
val lectureNotes = Util.fileToString(global.filePaths.lectureNotes)
Parsing.parseLectureNotes(lectureNotes)
// listen for internet availability in case of disconnection
if (!internetAwaitListenerRunning)
setOnInternetAvailListener(ctx)
// show the default (main) screen
global.currentScreen = Screen.Main
// inside show the timetable
global.currentMainElement = MainElement.Timetable
}
private fun setOnInternetAvailListener(ctx: Context) {
internetAwaitListenerRunning = true
CoroutineScope(Dispatchers.IO).launch {
while (true) {
if (Util.internetAvail(ctx)) {
CoroutineScope(Dispatchers.IO).launch {
remoteLoad(ctx)
}
} else {
showStatus("Bez připojení k internetu.")
}
delay(1000) // Check for internet availability every second
}
}
}
@@ -64,7 +64,7 @@ class MainActivity : ComponentActivity() {
.fillMaxWidth()
.weight(1f)
) {
when (isShowing.value) {
when (isShowing) {
IsShowing.Timetable -> TimetableScreen()
IsShowing.Marks -> MarksScreen()
IsShowing.Menu -> MenuScreen()
@@ -14,21 +14,18 @@ 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.ArrowBackIosNew
import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -55,13 +52,13 @@ enum class MarksShow {
Detail
}
var marksShow: MutableState<MarksShow> = mutableStateOf(MarksShow.All)
var marksShow by mutableStateOf(MarksShow.All)
private var subjectToShow: String = ""
private var markToShow: Mark = Mark()
@Composable
fun MarksScreen() {
when (marksShow.value) {
when (marksShow) {
MarksShow.All -> MarksAll()
MarksShow.Subject -> MarksSubject()
MarksShow.Detail -> MarksDetail()
@@ -145,7 +142,7 @@ fun MarksAll() {
shape = RoundedCornerShape(roundedCornerDimen)
)
.clickable {
marksShow.value = MarksShow.Subject
marksShow = MarksShow.Subject
subjectToShow = subject
}
} else {
@@ -157,7 +154,7 @@ fun MarksAll() {
shape = RoundedCornerShape(roundedCornerDimen)
)
.clickable {
marksShow.value = MarksShow.Subject
marksShow = MarksShow.Subject
subjectToShow = subject
}
}
@@ -268,7 +265,7 @@ fun MarksAll() {
fun MarksSubject() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler {
marksShow.value = MarksShow.All
marksShow = MarksShow.All
}
val scrollState = rememberScrollState()
@@ -283,29 +280,7 @@ fun MarksSubject() {
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(
onClick = {
marksShow.value = MarksShow.All
},
colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
containerColor = Color.Black
),
modifier = Modifier
.border(
width = 5.dp,
shape = RoundedCornerShape(roundedCornerDimen),
color = PastelYellow
)
.width(70.dp)
.height(70.dp)
) {
Icon(
imageVector = Icons.Default.ArrowBackIosNew,
contentDescription = "Back icon",
modifier = Modifier.size(40.dp)
)
}
backButtonFactory.get(BackButtonType.MarksAll)()
Box(modifier = Modifier.weight(1f))
@@ -397,7 +372,7 @@ fun MarksSubject() {
.padding(vertical = 10.dp)
.clickable {
markToShow = mark
marksShow.value = MarksShow.Detail
marksShow = MarksShow.Detail
}
) {
Box(
@@ -450,7 +425,7 @@ fun MarksSubject() {
fun MarksDetail() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler {
marksShow.value = MarksShow.Subject
marksShow = MarksShow.Subject
}
val scrollState = rememberScrollState()
@@ -466,29 +441,7 @@ fun MarksDetail() {
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(
onClick = {
marksShow.value = MarksShow.Subject
},
colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
containerColor = Color.Black
),
modifier = Modifier
.border(
width = borderWidthDimen,
shape = RoundedCornerShape(roundedCornerDimen),
color = PastelYellow
)
.width(70.dp)
.height(70.dp)
) {
Icon(
imageVector = Icons.Default.ArrowBackIosNew,
contentDescription = "Back icon",
modifier = Modifier.size(40.dp)
)
}
backButtonFactory.get(BackButtonType.MarksSubject)()
Box(modifier = Modifier.weight(1f))
@@ -540,7 +493,8 @@ fun MarksDetail() {
color = PastelYellow,
shape = RoundedCornerShape(roundedCornerDimen)
)
.background(if (settings.useAmoledMode.checked.value) {
.background(
if (settings.useAmoledMode.checked.value) {
Color.Black
} else {
PastelYellowTransparent
@@ -44,11 +44,11 @@ fun MenuScreen(modifier: Modifier = Modifier) {
Spacer(modifier = Modifier.height(80.dp))
MenuButton({ isShowing.value = IsShowing.About }, "O aplikaci")
MenuButton({ isShowing = IsShowing.About }, "O aplikaci")
Spacer(modifier = Modifier.height(80.dp))
MenuButton({ isShowing.value = IsShowing.Settings }, "Nastavení")
MenuButton({ isShowing = IsShowing.Settings }, "Nastavení")
}
// Dialog implementation
@@ -67,7 +67,7 @@ fun NavbarScreen(modifier: Modifier = Modifier) {
Button(
onClick = {
isShowing.value = IsShowing.Timetable
isShowing = IsShowing.Timetable
timetableColor.value = PastelYellow
marksColor.value = Color.White
menuColor.value = Color.White
@@ -88,11 +88,11 @@ fun NavbarScreen(modifier: Modifier = Modifier) {
Button(
onClick = {
isShowing.value = IsShowing.Marks
isShowing = IsShowing.Marks
timetableColor.value = Color.White
marksColor.value = PastelYellow
menuColor.value = Color.White
marksShow.value = MarksShow.All
marksShow = MarksShow.All
},
colors = ButtonDefaults.buttonColors(
contentColor = marksColor.value,
@@ -110,7 +110,7 @@ fun NavbarScreen(modifier: Modifier = Modifier) {
Button(
onClick = {
isShowing.value = IsShowing.Menu
isShowing = IsShowing.Menu
timetableColor.value = Color.White
marksColor.value = Color.White
menuColor.value = PastelYellow
@@ -29,8 +29,12 @@ class Network {
try {
val resp = client.newCall(request).execute()
val respString = resp.body?.string()
respString.toString()
val respString = resp.body?.string().toString()
if (JSONObject(respString).has("access_token")) {
JSONObject(respString).getString("access_token")
} else {
""""error": "network""""
}
//val respJson = respString?.let { JSONObject(it) }
//respJson.toString()
} catch (e: JSONException) {
@@ -138,6 +142,27 @@ class Network {
}
}
suspend fun getUserData(): JSONObject {
return withContext(Dispatchers.IO) {
val request = Request.Builder()
.url(userDataUrl)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Bearer $token") // using Bearer auth token
.get()
.build()
try {
val resp = client.newCall(request).execute()
val respString = resp.body?.string().toString()
JSONObject(respString)
} catch (e: JSONException) {
JSONObject("""{"error": "json"}""")
} catch (e: IOException) {
JSONObject("""{"error": "network"}""")
}
}
}
suspend fun getTimeTableJSON(): String {
return withContext(Dispatchers.IO) {
val userIsParent = Parsing.userIsParent()
@@ -162,6 +187,32 @@ class Network {
}
}
suspend fun getTimeTable(): JSONObject {
return withContext(Dispatchers.IO) {
val userIsParent = Parsing.userIsParent()
var url = timeTableUrl
if (userIsParent) {
url += "?studentId=$studentId"
}
val request = Request.Builder()
.url(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Bearer $token")
.get()
.build()
try {
val resp = client.newCall(request).execute()
JSONObject(resp.body?.string().toString())
} catch (e: JSONException) {
JSONObject("""{"error": "json"}""")
} catch (e: IOException) {
JSONObject("""{"error": "network"}""")
}
}
}
suspend fun getMarksJSON(): String {
return withContext(Dispatchers.IO) {
val request = Request.Builder()
@@ -179,5 +230,25 @@ class Network {
}
}
}
suspend fun getMarks(): JSONObject {
return withContext(Dispatchers.IO) {
val request = Request.Builder()
.url(marksUrl)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Bearer $token")
.get()
.build()
try {
val resp = client.newCall(request).execute()
JSONObject(resp.body?.string().toString())
} catch (e: JSONException) {
JSONObject("""{"error": "json"}""")
} catch (e: IOException) {
JSONObject("""{"error": "network"}""")
}
}
}
}
}
@@ -1,5 +1,7 @@
package com.odweta.solon
import androidx.compose.runtime.mutableStateOf
import com.odweta.solon.Util.Companion.getLectureId
import org.json.JSONObject
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@@ -49,8 +51,8 @@ class Parsing {
else false
}
fun parseTimeTableJSON(jsonString: String): List<Day> {
val jsonObject = JSONObject(jsonString)
fun parseTimetable(jsonString: JSONObject): List<Day> {
val jsonObject = jsonString
/*
* return a List that looks like this:
@@ -320,9 +322,9 @@ class Parsing {
return week
}
fun parseMarksJSON(jsonString: String): MutableMap<String, MutableList<Mark>> {
fun parseMarks(jsonString: JSONObject): MutableMap<String, MutableList<Mark>> {
// no marks
val json = JSONObject(jsonString)
val json = jsonString
if (json.has("marks") && json.getJSONArray("marks").length() == 0) {
val noMarks = mutableMapOf<String, MutableList<Mark>>()
val list = mutableListOf<Mark>()
@@ -379,5 +381,38 @@ class Parsing {
return marks
}
fun parseLectureNotes(lectureNotes: String) {
for (day in global.timetable) {
for ((subjectIndex, subject) in day.subjects.withIndex()) {
val id = getLectureId(day.date, subjectIndex)
global.lectureNotes[id] = LectureNote(
id = id,
type = mutableStateOf(LectureNoteType.Normal),
note = mutableStateOf(""),
number = subject.number.toInt(),
lectureName = subject.name
)
}
}
val lines = lectureNotes.lines()
for (line in lines) {
if (line.isBlank()) continue
val parts = line.split(",")
if (parts.size >= 3) {
val id = parts[0].trim().toInt()
val type = parts[1].trim().toLectureNoteType()
var note = parts[2].trim()
for (part in 3.until(parts.size)) {
note += ",${parts[part]}"
}
global.lectureNotes[id]?.note = mutableStateOf(note)
global.lectureNotes[id]?.type = mutableStateOf(type)
}
}
}
}
}
@@ -37,7 +37,7 @@ import com.odweta.solon.ui.theme.PastelYellow
fun SettingsScreen(modifier: Modifier = Modifier) {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler {
isShowing.value = IsShowing.Menu
isShowing = IsShowing.Menu
}
val scrollState = rememberScrollState()
@@ -54,7 +54,7 @@ fun SettingsScreen(modifier: Modifier = Modifier) {
) {
Button(
onClick = {
isShowing.value = IsShowing.Menu
isShowing = IsShowing.Menu
},
colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
@@ -146,4 +146,26 @@ fun SettingsTableRow(setting: Setting) {
)
)
}
}
fun String.toSettingsData(): SettingsData {
val settings = this.split("\n")
val auxSettingsData = SettingsData()
val useSvobodaMode = settings[0].split("#")
val useAmoledMode = settings[1].split("#")
val showRefreshButton = settings[2].split("#")
auxSettingsData.useSvobodaMode.id = SettingsId.Svoboda
auxSettingsData.useSvobodaMode.name = useSvobodaMode[1]
auxSettingsData.useSvobodaMode.checked.value = useSvobodaMode[2].toBoolean()
auxSettingsData.useAmoledMode.id = SettingsId.Amoled
auxSettingsData.useAmoledMode.name = useAmoledMode[1]
auxSettingsData.useAmoledMode.checked.value = useAmoledMode[2].toBoolean()
auxSettingsData.showRefreshButton.id = SettingsId.RefreshButton
auxSettingsData.showRefreshButton.name = showRefreshButton[1]
auxSettingsData.showRefreshButton.checked.value = showRefreshButton[2].toBoolean()
return auxSettingsData
}
@@ -109,8 +109,10 @@ fun signInButtonCallback(ctx: Context, coscope: CoroutineScope, buttonText: Stri
@Composable
fun SignInScreen() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler {
screenToShow.value = Screen.Main
if (!global.noSignInBackHandle) {
BackHandler {
screenToShow.value = Screen.Main
}
}
var usernameText by remember { mutableStateOf("") }
@@ -43,4 +43,8 @@ fun ToolbarScreen() {
color = Color.White
)
}
}
fun showStatus(text: String) {
toolbarText.value = text
}
@@ -265,7 +265,7 @@ class Util {
})
}
private fun csvToLectureNotes() {
fun csvToLectureNotes() {
val lines = fileToString(lectureNotesFilePath).lines()
for (line in lines) {
if (line.isBlank()) continue
@@ -286,7 +286,7 @@ class Util {
}
}
private fun lectureNotesToCsvString(hashMap: HashMap<Int, LectureNote>): String {
fun lectureNotesToCsvString(hashMap: HashMap<Int, LectureNote>): String {
var result = ""
for ((key, lectureNote) in hashMap) {
result +=