30 Commits
Author SHA1 Message Date
Odweta 12d891a0e4 made new release 2.5 2025-02-04 19:20:48 +01:00
Odweta 3bac016a8b made google play demo account; removed quickdays; removed lecturenotes; fixed settings not applying right away bug 2025-02-04 19:15:56 +01:00
Odweta 81e875bea8 added a deploy script for google play usable from a Termux environment 2025-01-29 19:28:09 +01:00
odweta aed00c0694 added quick naviagation for days at the top of the timetable 2025-01-17 19:32:25 +01:00
odweta 6cbe12fb10 made new release 2.4.1 2025-01-16 14:51:28 +01:00
odweta 992c0b4a34 made new release 2.4.1 2025-01-16 13:29:30 +01:00
odweta d8bda87b38 made new release 2.4.1 2025-01-16 13:28:47 +01:00
odweta cca561f11b update README with release 2.4.1 2025-01-15 21:31:11 +01:00
odweta 7907429aea made new release 2.4.1 2025-01-15 21:29:06 +01:00
odweta 4cb07126e2 fixed network timeout and started working on final marks 2025-01-15 11:32:16 +01:00
Odweta 1d885ade70 messages 2025-01-13 20:46:05 +01:00
Odweta 8615eee3a7 created TempScreen for on-demand recompositions of the whole app.
enhanced UI
2025-01-09 10:40:20 +01:00
Odweta e3bb1b9cfa update README to latest verName 2025-01-08 17:19:09 +01:00
Odweta 0109c7aa09 Merge branch 'patch-1' into 'master'
Edit README.md

See merge request Odweta/solon!4
2025-01-08 16:16:44 +00:00
Poussinou 7800eeb61b Edit README.md 2025-01-08 16:16:44 +00:00
Odweta 4b7fbfd766 optimized suboptimal code 2025-01-08 17:02:20 +01:00
Odweta 66f3ad791f made new release 2.4 2025-01-03 21:27:19 +01:00
Odweta 910e968653 made new release 2.5 2025-01-03 11:44:28 +01:00
Odweta 31197830d0 tried to make changes to some live data, did not go well so rollback 2025-01-03 11:42:43 +01:00
Odweta a619c8b92e the same 2025-01-03 10:23:52 +01:00
Odweta 04d8527434 sign out ui improvements 2025-01-03 10:22:57 +01:00
Odweta bea50f7fe7 buttons now have icons and text instead of just text 2025-01-03 10:21:33 +01:00
Odweta e9a422eda0 ui improvements, now even marks are animated 2025-01-03 10:06:15 +01:00
Odweta cc86373989 update deploy.sh 2024-12-31 17:34:24 +01:00
Odweta 8b98d92c2b fix deploy 2024-12-31 17:32:27 +01:00
Odweta b10438c109 made new release 2024-12-31 17:28:44 +01:00
Odweta 69f48750f1 update deploy script 2024-12-31 17:27:59 +01:00
Odweta 81fb398ae2 added animations, changed signOut screen 2024-12-31 17:22:09 +01:00
Odweta 1516a00841 fixed app crashes and some other improvements 2024-12-30 17:31:27 +01:00
Odweta 6dae99d311 updated deploying 2024-12-22 20:28:50 +01:00
37 changed files with 1124 additions and 537 deletions
+2 -15
View File
@@ -1,16 +1,3 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
mobile/app/build mobile/app/build
mobile/local.properties
mobile/.gradle
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-239e49af:192a4be9597:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+4 -1
View File
@@ -1,3 +1,6 @@
![solon-logo](solon_logo.png) ![solon-logo](solon_logo.png)
# Zorientujte se rychleji v této odlehčené verzi Školy OnLine # 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.3.0/downloads/app-release.apk) (verze 2.3.0) # 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/)
-4
View File
@@ -1,4 +0,0 @@
- add settings
- color schemes
- modes
- new logo
+33 -14
View File
@@ -1,26 +1,39 @@
#!/bin/bash #!/bin/bash
echo -n "version number: " echo -n "[glpat]: "
read ver
echo -n "glpat: "
read glpat read glpat
echo -n "[keystore password]: "
read ks_pass
cd ./mobile cd ./mobile
ver=$(./gradlew printVersionName -q)
# add, commit, push
echo
echo "[add, commit, push]"
echo
git add ..
git commit -m "made new release $ver"
git push -f
# build # build
echo
echo "[building]"
echo
./gradlew assembleRelease ./gradlew assembleRelease
# sign # sign
~/Android/Sdk/build-tools/35.0.0/apksigner sign --alignment-preserved --ks ~/Documents/android-keystore.jks app/build/outputs/apk/release/com.odweta.solon_$ver.apk echo
echo "[signing]"
# add, commit, push echo
git add .. ~/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
git commit -m "made new release $ver"
git push
# create the tag # create the tag
echo "\n[creating tag v$ver]" echo
echo "[creating tag v$ver]"
echo
data_str='{ "tag_name": "'$ver'", "ref": "HEAD", "message": "Tagging release v'$ver'" }' data_str='{ "tag_name": "'$ver'", "ref": "HEAD", "message": "Tagging release v'$ver'" }'
curl --request POST \ curl --request POST \
--header "PRIVATE-TOKEN: $glpat" \ --header "PRIVATE-TOKEN: $glpat" \
@@ -29,7 +42,9 @@ curl --request POST \
"https://gitlab.com/api/v4/projects/47146187/repository/tags" "https://gitlab.com/api/v4/projects/47146187/repository/tags"
# make a release # make a release
echo "\n[publishing release $ver]" echo
echo "[publishing release $ver]"
echo
data_str='{ "name": "v'$ver'", "tag_name": "'$ver'", "ref": "HEAD" }' data_str='{ "name": "v'$ver'", "tag_name": "'$ver'", "ref": "HEAD" }'
curl --request POST \ curl --request POST \
--header "PRIVATE-TOKEN: $glpat" \ --header "PRIVATE-TOKEN: $glpat" \
@@ -38,14 +53,18 @@ curl --request POST \
"https://gitlab.com/api/v4/projects/47146187/releases" "https://gitlab.com/api/v4/projects/47146187/releases"
# attach a file # attach a file
echo "\n[attaching the release apk]" echo
echo "[attaching the release apk]"
echo
asset_url="https://gitlab.com/-/project/47146187"$(curl --request POST \ asset_url="https://gitlab.com/-/project/47146187"$(curl --request POST \
--header "PRIVATE-TOKEN: $glpat" \ --header "PRIVATE-TOKEN: $glpat" \
--form "file=@app/build/outputs/apk/release/com.odweta.solon_$ver.apk" \ --form "file=@app/build/outputs/apk/release/com.odweta.solon_$ver.apk" \
"https://gitlab.com/api/v4/projects/47146187/uploads" | jq .url -r) "https://gitlab.com/api/v4/projects/47146187/uploads" | jq .url -r)
# add the file as a stable release asset # add the file as a stable release asset
echo "\n[creating release asset]" echo
echo "[creating release asset]"
echo
curl --request POST \ curl --request POST \
--header "PRIVATE-TOKEN: $glpat" \ --header "PRIVATE-TOKEN: $glpat" \
--data name="app-release.apk" \ --data name="app-release.apk" \
+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 ..
+123
View File
@@ -0,0 +1,123 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
-6
View File
@@ -13,12 +13,6 @@
</DropdownSelection> </DropdownSelection>
<DialogSelection /> <DialogSelection />
</SelectionState> </SelectionState>
<SelectionState runConfigName="TimetablePreview">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="RBPreview">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates> </selectionStates>
</component> </component>
</project> </project>
+1
View File
@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" /> <component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+22 -2
View File
@@ -4,6 +4,26 @@ plugins {
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
} }
tasks.register("printVersionName") {
doLast {
println(android.defaultConfig.versionName)
}
}
tasks.register("deploy") {
dependsOn("assembleDebug")
//dependsOn("installDebug")
doLast {
exec {
commandLine("adb", "shell", "am", "start", "-n", "com.odweta.solon/.MainActivity")
}
}
}
tasks.register("b") {
dependsOn("assembleDebug")
}
android { android {
namespace = "com.odweta.solon" namespace = "com.odweta.solon"
compileSdk = 34 compileSdk = 34
@@ -12,8 +32,8 @@ android {
applicationId = "com.odweta.solon" applicationId = "com.odweta.solon"
minSdk = 28 minSdk = 28
targetSdk = 34 targetSdk = 34
versionCode = 208 versionCode = 213
versionName = "2.3.0" versionName = "2.5"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
Binary file not shown.
Binary file not shown.
-37
View File
@@ -1,37 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.odweta.solon",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "2.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 28
}
@@ -1,5 +1,6 @@
package com.odweta.solon package com.odweta.solon
import android.util.Log
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.json.JSONObject import org.json.JSONObject
@@ -8,17 +9,10 @@ import java.nio.charset.Charset
class Account(val username: String, val password: String, private val error: Int) { class Account(val username: String, val password: String, private val error: Int) {
companion object { companion object {
private fun parseAccountDataFile(file: File): Account { private fun parseAccountDataFile(): Account {
val lines: List<String> = file.readLines() val lines: List<String> = File(global.fileDir, global.filePaths.accountData).readLines()
if (lines.size >= 2) {
if (lines[0] != "" && lines[1] != "") {
return Account(lines[0], lines[1], 0) return Account(lines[0], lines[1], 0)
} }
}
return Account("", "", 1)
}
fun haveSaved(): Boolean { fun haveSaved(): Boolean {
/* check if a file exists /* check if a file exists
@@ -27,15 +21,17 @@ class Account(val username: String, val password: String, private val error: Int
*/ */
// check if file exists // 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 // it does not exist
file.createNewFile() File(global.fileDir, global.filePaths.accountData).createNewFile()
false false
} else { } else {
// it exists, so it shall be parsed // it exists, so it shall be parsed
val acc: Account = parseAccountDataFile(file) val acc: Account = parseAccountDataFile()
global.username = acc.username global.username = acc.username
global.password = acc.password global.password = acc.password
acc.error == 0 acc.error == 0
@@ -43,23 +39,20 @@ class Account(val username: String, val password: String, private val error: Int
} }
fun save(acc: Account): Boolean { fun save(acc: Account): Boolean {
return try { val payload = "${acc.username}\n${acc.password}"
val file = File(global.fileDir, global.filePaths.accountData) return Util.saveStringToFile(global.filePaths.accountData, payload)
file.writeText("${acc.username}\n${acc.password}\n", Charset.forName("UTF-8"))
true
} catch (e: Exception) {
false
}
} }
suspend fun isValid(acc: Account): Boolean { fun isValid(acc: Account): Boolean {
return withContext(Dispatchers.IO) {
global.username = acc.username global.username = acc.username
global.password = acc.password global.password = acc.password
val token = networkRequestFactory.perform(NetworkRequestType.Token).getString("token") val tokenJSON = networkRequestFactory.perform(NetworkRequestType.Token)
!JSONObject(token).has("error") return if (tokenJSON.has("token")) {
val token = tokenJSON.getString("token")
!token.contains("\"error\"")
} else {
false
} }
} }
} }
@@ -33,9 +33,7 @@ interface BackButtonFactoryInterface {
@Composable @Composable
fun BackButton(onClick: () -> Unit) { fun BackButton(onClick: () -> Unit) {
Box( Box(
modifier = Modifier modifier = Modifier.padding(start = 10.dp)
.fillMaxWidth()
.padding(start = 5.dp)
) { ) {
Button( Button(
onClick = onClick, onClick = onClick,
@@ -65,8 +63,14 @@ object BackButtonFactory: BackButtonFactoryInterface {
@Composable @Composable
override fun get(type: BackButtonType): @Composable () -> Unit { override fun get(type: BackButtonType): @Composable () -> Unit {
return when (type) { return when (type) {
BackButtonType.Menu -> { { BackButton { global.currentScreenElement = ScreenElement.Menu } } } BackButtonType.Menu -> { { BackButton {
BackButtonType.MarksAll -> { { BackButton { global.marksDepthLevel = MarksDepthLevel.All } } } global.currentScreenElement = ScreenElement.Menu
global.currentScreen = Screen.Main
} } }
BackButtonType.MarksAll -> { { BackButton {
global.currentScreenElement = ScreenElement.Marks
global.marksDepthLevel = MarksDepthLevel.All
} } }
BackButtonType.MarksSubject -> { { BackButton { global.marksDepthLevel = MarksDepthLevel.Subject } } } BackButtonType.MarksSubject -> { { BackButton { global.marksDepthLevel = MarksDepthLevel.Subject } } }
} }
} }
@@ -1,26 +1,44 @@
package com.odweta.solon package com.odweta.solon
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.RecomposeScope
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import com.odweta.solon.ui.theme.PastelYellow import com.odweta.solon.ui.theme.PastelYellow
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.concurrent.Callable
enum class Screen { enum class Screen {
Main, Main,
Splash, Splash,
SignIn SignIn,
Settings,
Temp
} }
// default settings (will get overwritten by the saved settings) // default settings (will get overwritten by the saved settings)
@@ -34,7 +52,8 @@ class Setting(auxId: SettingsId, auxName: String, auxChecked: Boolean) {
enum class SettingsId { enum class SettingsId {
Svoboda, Svoboda,
Amoled, Amoled,
RefreshButton RefreshButton,
Animations
} }
class SettingsData { class SettingsData {
@@ -56,7 +75,13 @@ class SettingsData {
true true
) )
override fun toString() = "$useSvobodaMode\n$useAmoledMode\n$showRefreshButton" var useAnimations: Setting = Setting(
SettingsId.Animations,
"Používat animace v aplikaci",
true
)
override fun toString() = "$useSvobodaMode\n$useAmoledMode\n$showRefreshButton\n$useAnimations"
} }
enum class LectureNoteType { enum class LectureNoteType {
@@ -76,23 +101,13 @@ fun String.toLectureNoteType(): LectureNoteType {
} }
} }
class LectureNote(id: Int, note: MutableState<String>, type: MutableState<LectureNoteType>, number: Int, lectureName: String) { data class LectureNote(
var id: Int = 0 var id: Int,
var note: MutableState<String> = var note: MutableState<String> = mutableStateOf(""),
mutableStateOf("") var type: MutableState<LectureNoteType> = mutableStateOf(LectureNoteType.Normal),
var type: MutableState<LectureNoteType> = var number: Int,
mutableStateOf(LectureNoteType.Normal) var lectureName: String
var number: Int = 0 )
var lectureName: String = ""
init {
this.id = id
this.note = note
this.type = type
this.number = number
this.lectureName = lectureName
}
}
class Subject { class Subject {
lateinit var name: String lateinit var name: String
@@ -174,16 +189,20 @@ class Mark {
enum class ScreenElement { enum class ScreenElement {
Timetable, Timetable,
Marks, Marks,
NewMarks,
FinalMarks,
Menu, Menu,
About, About,
Settings SignOut
} }
class Global { class Global {
lateinit var timetableJSON: JSONObject lateinit var timetableJSON: JSONObject
lateinit var marksJSON: JSONObject lateinit var marksJSON: JSONObject
lateinit var finalMarksJSON: JSONObject
lateinit var marks: MutableMap<String, MutableList<Mark>> lateinit var marks: MutableMap<String, MutableList<Mark>>
lateinit var newMarks: MutableMap<String, MutableList<Mark>> lateinit var newMarks: MutableMap<String, MutableList<Mark>>
lateinit var finalMarks: Map<String, Int>
lateinit var settings: SettingsData lateinit var settings: SettingsData
lateinit var fileDir: File lateinit var fileDir: File
@@ -205,11 +224,13 @@ class Global {
var selectedLecture: Int = 0 var selectedLecture: Int = 0
var currentLectureNumber: Int = 0 var currentLectureNumber: Int = 0
var tempScreen by mutableStateOf(Screen.Settings)
var currentScreen by mutableStateOf(Screen.Main) var currentScreen by mutableStateOf(Screen.Main)
var currentScreenElement by mutableStateOf(ScreenElement.Timetable) var currentScreenElement by mutableStateOf(ScreenElement.Timetable)
var status by mutableStateOf("") var status by mutableStateOf("")
var returnToTempScreen by mutableStateOf(false)
var isAPISetUp by mutableStateOf(false) var isAPISetUp by mutableStateOf(false)
var launched by mutableStateOf(false) var launched by mutableStateOf(false)
var refreshed by mutableStateOf(false) var refreshed by mutableStateOf(false)
@@ -218,6 +239,8 @@ class Global {
var subjectToShow by mutableStateOf("") var subjectToShow by mutableStateOf("")
var markToShow by mutableStateOf(Mark()) var markToShow by mutableStateOf(Mark())
var demoMode: Boolean = false
inner class FilePaths { inner class FilePaths {
val accountData: String = "account_data.txt" val accountData: String = "account_data.txt"
val timetable: String = "timetable.json" val timetable: String = "timetable.json"
@@ -225,6 +248,7 @@ class Global {
val marks: String = "marks.json" val marks: String = "marks.json"
val settings: String = "settings.txt" val settings: String = "settings.txt"
val lectureNotes: String = "lecture_notes.csv" val lectureNotes: String = "lecture_notes.csv"
val finalMarks: String = "final_marks.txt"
} }
val filePaths = FilePaths() val filePaths = FilePaths()
@@ -234,6 +258,7 @@ class Global {
val userData = "$apiBase/v1/user" val userData = "$apiBase/v1/user"
val timetable = "$apiBase/v1/timeTable" val timetable = "$apiBase/v1/timeTable"
var marks: String = "$apiBase/v1/students/<student id>/marks/list" var marks: String = "$apiBase/v1/students/<student id>/marks/list"
var finalMarks: String ="$apiBase/v1/students/<student id>/marks/final"
} }
val apiEndPoints = APIEndPoints() val apiEndPoints = APIEndPoints()
@@ -260,3 +285,34 @@ class Global {
val global = Global() val global = Global()
var settings = SettingsData() var settings = SettingsData()
private fun <T> changeAndRevert(variable: T, newValue: T, revert: (T) -> Unit) {
val orig = variable
revert(newValue)
revert(orig)
}
@Composable
fun <T> Animate(targetState: T, content: @Composable (target: T) -> Unit) {
if (settings.useAnimations.checked.value == true) {
// When animations are enabled, apply AnimatedContent
AnimatedContent(
targetState = targetState,
transitionSpec = {
(slideInVertically(initialOffsetY = { it }) + fadeIn()).togetherWith(
slideOutVertically(targetOffsetY = { it }) + fadeOut()
)
},
modifier = Modifier.fillMaxSize(),
label = "animated content"
) { target ->
content(target)
}
} else {
// When animations are disabled, just show content without animation
content(targetState)
}
}
@@ -28,14 +28,24 @@ fun remoteLoad(ctx: Context) {
if (!global.isAPISetUp) { if (!global.isAPISetUp) {
CoroutineScope(Dispatchers.IO) CoroutineScope(Dispatchers.IO)
.launch { .launch {
// setup API
setupAPI() setupAPI()
// fetch remote data
global.timetableJSON = networkRequestFactory.perform(NetworkRequestType.Timetable) global.timetableJSON = networkRequestFactory.perform(NetworkRequestType.Timetable)
global.marksJSON = networkRequestFactory.perform(NetworkRequestType.Marks) 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( launchContinue(
ctx, ctx,
global.timetableJSON, global.timetableJSON,
global.marksJSON global.marksJSON,
global.finalMarksJSON
) )
} }
} else { } else {
@@ -43,7 +53,8 @@ fun remoteLoad(ctx: Context) {
launchContinue( launchContinue(
ctx, ctx,
networkRequestFactory.perform(NetworkRequestType.Timetable), networkRequestFactory.perform(NetworkRequestType.Timetable),
networkRequestFactory.perform(NetworkRequestType.Marks) networkRequestFactory.perform(NetworkRequestType.Marks),
networkRequestFactory.perform(NetworkRequestType.FinalMarks)
) )
} }
} }
@@ -54,39 +65,43 @@ fun localLoad(ctx: Context) {
global.studentId = Parsing.getStudentId() global.studentId = Parsing.getStudentId()
global.studentClass = Parsing.getStudentClass() global.studentClass = Parsing.getStudentClass()
global.apiEndPoints.marks = "${global.apiEndPoints.apiBase}/v1/students/${global.studentId}/marks/list" 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 localTimetable = JSONObject(Util.fileToString(global.filePaths.timetable))
val localMarks = JSONObject(Util.fileToString(global.filePaths.marks)) val localMarks = JSONObject(Util.fileToString(global.filePaths.marks))
val localFinalMarks = JSONObject(Util.fileToString(global.filePaths.finalMarks))
launchContinue( launchContinue(
ctx, ctx,
localTimetable, localTimetable,
localMarks localMarks,
localFinalMarks
) )
} }
fun launchContinue( fun launchContinue(
ctx: Context, ctx: Context,
timetable: JSONObject, timetable: JSONObject,
marks: JSONObject marks: JSONObject,
finalMarks: JSONObject
) { ) {
// parse timetable and marks // parse timetable and marks
val parsedTimetable = Parsing.parseTimetable(timetable) global.timetable = Parsing.parseTimetable(timetable)
global.timetable = parsedTimetable global.marks = Parsing.parseMarks(marks)
val parsedMarks = Parsing.parseMarks(marks)
global.marks = parsedMarks
global.newMarks = Parsing.parseNewMarks(marks) global.newMarks = Parsing.parseNewMarks(marks)
global.finalMarks = Parsing.parseFinalMarks(finalMarks)
// load the settings
val settingsData = Util.fileToString(global.filePaths.settings).toSettingsData()
global.settings = settingsData
// load the lecture notes // load the lecture notes
Util.csvToLectureNotes() //Util.loadLectureNotes()
//Util.csvToLectureNotes()
// load the settings
settings = Util.fileToString(global.filePaths.settings).toSettingsData()
// listen for internet availability in case of disconnection // listen for internet availability in case of disconnection
if (!global.internetAwaitListenerRunning) if (!global.internetAwaitListenerRunning)
setOnInternetAvailListener(ctx) setOnInternetAvailListener(ctx)
// FIXME maybe? causes IndexOutOfBoundsException????? prob not tho
if (!global.launched) { if (!global.launched) {
// show the default (main) screen // show the default (main) screen
global.currentScreen = Screen.Main global.currentScreen = Screen.Main
@@ -8,28 +8,45 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.PointerIcon.Companion.Text
import androidx.compose.ui.unit.dp
import com.odweta.solon.ui.theme.SolonTheme import com.odweta.solon.ui.theme.SolonTheme
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// for seemless <currentScreen> switching
window.statusBarColor = Color.Black.toArgb() window.statusBarColor = Color.Black.toArgb()
window.setBackgroundDrawable(resources.getDrawable(R.drawable.black))
global.fileDir = applicationContext.filesDir global.fileDir = applicationContext.filesDir
launchStart(applicationContext) launchStart(applicationContext)
setContent { setContent {
SolonTheme { SolonTheme {
when (global.currentScreen) { Animate(global.currentScreen) { targetScreen ->
when (targetScreen) {
Screen.Main -> if (global.launched) MainScreen() Screen.Main -> if (global.launched) MainScreen()
Screen.Splash -> SplashScreen() Screen.Splash -> SplashScreen()
Screen.SignIn -> SignInScreen() Screen.SignIn -> SignInScreen()
Screen.Settings -> SettingsScreen()
Screen.Temp -> TempScreen()
}
}
} }
} }
} }
@@ -54,12 +71,16 @@ class MainActivity : ComponentActivity() {
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f)
) { ) {
when (global.currentScreenElement) { Animate(global.currentScreenElement) { targetScreen ->
when (targetScreen) {
ScreenElement.Timetable -> TimetableScreen() ScreenElement.Timetable -> TimetableScreen()
ScreenElement.Marks -> MarksScreen() ScreenElement.Marks -> MarksScreen()
ScreenElement.NewMarks -> NewMarksScreen()
ScreenElement.FinalMarks -> FinalMarksScreen()
ScreenElement.Menu -> MenuScreen() ScreenElement.Menu -> MenuScreen()
ScreenElement.About -> AboutScreen() ScreenElement.About -> AboutScreen()
ScreenElement.Settings -> SettingsScreen() ScreenElement.SignOut -> SignOutScreen()
}
} }
} }
@@ -71,4 +92,3 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
}
@@ -1,5 +1,6 @@
package com.odweta.solon package com.odweta.solon
import android.util.Log
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
@@ -20,29 +21,27 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AutoAwesome import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material.icons.filled.WorkspacePremium
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.zIndex
import com.odweta.solon.ui.theme.PastelGreen import com.odweta.solon.ui.theme.PastelGreen
import com.odweta.solon.ui.theme.PastelGreenTransparent import com.odweta.solon.ui.theme.PastelGreenTransparent
import com.odweta.solon.ui.theme.PastelOrange import com.odweta.solon.ui.theme.PastelOrange
@@ -53,10 +52,10 @@ import com.odweta.solon.ui.theme.PastelRed
import com.odweta.solon.ui.theme.PastelRedTransparent import com.odweta.solon.ui.theme.PastelRedTransparent
import com.odweta.solon.ui.theme.PastelYellow import com.odweta.solon.ui.theme.PastelYellow
import com.odweta.solon.ui.theme.PastelYellowTransparent import com.odweta.solon.ui.theme.PastelYellowTransparent
import java.time.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
var showNewMarksDialog by mutableStateOf(false) var showNewMarksDialog by mutableStateOf(false)
var showFinalMarksDialog by mutableStateOf(false)
enum class MarksDepthLevel { enum class MarksDepthLevel {
All, All,
@@ -66,46 +65,69 @@ enum class MarksDepthLevel {
@Composable @Composable
fun MarksScreen() { fun MarksScreen() {
when (global.marksDepthLevel) { Animate(global.marksDepthLevel) { targetDepthLevel ->
when (targetDepthLevel) {
MarksDepthLevel.All -> MarksAll() MarksDepthLevel.All -> MarksAll()
MarksDepthLevel.Subject -> MarksSubject() MarksDepthLevel.Subject -> MarksSubject()
MarksDepthLevel.Detail -> MarksDetail() MarksDepthLevel.Detail -> MarksDetail()
} }
if (showNewMarksDialog) { }
val scrollStateNewMarks = rememberScrollState() }
Dialog( @Composable
onDismissRequest = { showNewMarksDialog = false }, fun NewMarksScreen() {
properties = DialogProperties( BackHandler { global.currentScreenElement = ScreenElement.Marks }
dismissOnBackPress = true,
dismissOnClickOutside = true val ss = rememberScrollState()
)
) {
Column( Column(
Modifier.verticalScroll(scrollStateNewMarks).padding(vertical = 10.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
backButtonFactory.get(BackButtonType.MarksAll)()
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier
.background(
color = Color.Black,
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.border(
shape = RoundedCornerShape(global.dimens.roundedCorner),
color = PastelYellow,
width = global.dimens.borderWidth
)
.height(70.dp)
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
"Nové známky", "Nové známky",
color = Color.White, color = Color.White,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 30.sp, fontSize = 30.sp,
textAlign = TextAlign.Center, 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)
) )
}
}
}
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
Column(
modifier = Modifier.verticalScroll(ss)
) {
for ((k, v) in global.newMarks) { for ((k, v) in global.newMarks) {
if (v.size > 0) { if (v.isNotEmpty()) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(10.dp) .padding(horizontal = 10.dp)
.padding(bottom = 10.dp)
.border( .border(
shape = RoundedCornerShape(global.dimens.roundedCorner), shape = RoundedCornerShape(global.dimens.roundedCorner),
color = PastelYellow, color = PastelYellow,
@@ -133,7 +155,8 @@ fun MarksScreen() {
Text( Text(
i.theme, i.theme,
color = Color.White, color = Color.White,
modifier = Modifier.padding(end = 5.dp, top = 5.dp) modifier = Modifier
.padding(end = 5.dp, top = 5.dp)
.weight(1f) .weight(1f)
) )
Column( Column(
@@ -146,7 +169,8 @@ fun MarksScreen() {
.background( .background(
color = getAvgColor(i.grade, true), color = getAvgColor(i.grade, true),
shape = RoundedCornerShape(global.dimens.roundedCorner) shape = RoundedCornerShape(global.dimens.roundedCorner)
).padding(5.dp), )
.padding(5.dp),
style = TextStyle( style = TextStyle(
shadow = Shadow( shadow = Shadow(
color = Color.Black, color = Color.Black,
@@ -171,6 +195,67 @@ fun MarksScreen() {
} }
} }
} }
@Composable
fun FinalMarksScreen() {
BackHandler { global.currentScreenElement = ScreenElement.Marks }
val ss = rememberScrollState()
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
backButtonFactory.get(BackButtonType.MarksAll)()
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier
.background(
color = Color.Black,
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.border(
shape = RoundedCornerShape(global.dimens.roundedCorner),
color = PastelYellow,
width = global.dimens.borderWidth
)
.height(70.dp)
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Uzavřené známky",
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 30.sp,
textAlign = TextAlign.Center
)
}
}
}
Spacer(Modifier.height(10.dp))
Column(
modifier = Modifier.verticalScroll(ss)
) {
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))
}
}
}
}
} }
private fun getAvgColor(avg: String, isBorder: Boolean = false): Color { private fun getAvgColor(avg: String, isBorder: Boolean = false): Color {
@@ -221,7 +306,7 @@ fun MarksAll() {
.verticalScroll(scrollState) .verticalScroll(scrollState)
) { ) {
for ((subject, markList) in global.marks) { for ((subject, markList) in global.marks) {
val amoledModifier = if (settings.useAmoledMode.checked.value) { val amoledModifier = if (settings.useAmoledMode.checked.value == true) {
Modifier Modifier
.width(150.dp) .width(150.dp)
.fillMaxHeight() .fillMaxHeight()
@@ -243,7 +328,7 @@ fun MarksAll() {
) )
} }
val amoledModifierBackground = if (settings.useAmoledMode.checked.value) { val amoledModifierBackground = if (settings.useAmoledMode.checked.value == true) {
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.height(70.dp) .height(70.dp)
@@ -256,8 +341,10 @@ fun MarksAll() {
shape = RoundedCornerShape(global.dimens.roundedCorner) shape = RoundedCornerShape(global.dimens.roundedCorner)
) )
.clickable { .clickable {
if (markList[0].grade != "0") {
global.marksDepthLevel = MarksDepthLevel.Subject global.marksDepthLevel = MarksDepthLevel.Subject
global.subjectToShow = subject global.subjectToShow = subject
} // else { isInDemo == true }
} }
} else { } else {
Modifier Modifier
@@ -268,12 +355,14 @@ fun MarksAll() {
shape = RoundedCornerShape(global.dimens.roundedCorner) shape = RoundedCornerShape(global.dimens.roundedCorner)
) )
.clickable { .clickable {
if (markList[0].grade != "0") {
global.marksDepthLevel = MarksDepthLevel.Subject global.marksDepthLevel = MarksDepthLevel.Subject
global.subjectToShow = subject global.subjectToShow = subject
} // else { isInDemo == true }
} }
} }
val amoledSpacerModifier = if (settings.useAmoledMode.checked.value) { val amoledSpacerModifier = if (settings.useAmoledMode.checked.value == true) {
Modifier Modifier
.width(10.dp) .width(10.dp)
.fillMaxHeight() .fillMaxHeight()
@@ -360,22 +449,46 @@ fun MarksAll() {
} }
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
Row(
modifier = Modifier
.align(Alignment.BottomCenter)
) {
FloatingActionButton( FloatingActionButton(
onClick = { showNewMarksDialog = true }, onClick = { global.currentScreenElement = ScreenElement.NewMarks },
modifier = Modifier modifier = Modifier
.align(Alignment.BottomEnd)
.padding(10.dp) .padding(10.dp)
.background(Color.Black, shape = RoundedCornerShape(global.dimens.roundedCorner)), .background(
containerColor = Color.Black, Color.Black,
contentColor = PastelYellow shape = RoundedCornerShape(global.dimens.roundedCorner)
),
contentColor = PastelYellow,
containerColor = Color.Black
) { ) {
Icon( Icon(
modifier = Modifier.padding(start = 4.dp, top = 4.dp),
imageVector = Icons.Default.AutoAwesome, imageVector = Icons.Default.AutoAwesome,
contentDescription = "New marks in subject", contentDescription = "New marks in subject",
tint = PastelYellow // You can customize the icon color tint = PastelYellow // You can customize the icon color
) )
} }
/*FloatingActionButton(
onClick = { global.currentScreenElement = ScreenElement.FinalMarks },
modifier = Modifier
.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
)
}*/
}
} }
} }
@@ -413,7 +526,7 @@ fun MarksSubject() {
shape = RoundedCornerShape(global.dimens.roundedCorner) shape = RoundedCornerShape(global.dimens.roundedCorner)
) )
.background( .background(
color = if (settings.useAmoledMode.checked.value) { color = if (settings.useAmoledMode.checked.value == true) {
Color.Black Color.Black
} else { } else {
getAvgColor( getAvgColor(
@@ -454,7 +567,7 @@ fun MarksSubject() {
shape = RoundedCornerShape(global.dimens.roundedCorner) shape = RoundedCornerShape(global.dimens.roundedCorner)
) )
.background( .background(
color = if (settings.useAmoledMode.checked.value) { color = if (settings.useAmoledMode.checked.value == true) {
Color.Black Color.Black
} else { } else {
PastelYellowTransparent PastelYellowTransparent
@@ -481,7 +594,7 @@ fun MarksSubject() {
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 5.dp) .padding(horizontal = 5.dp)
.background( .background(
color = if (settings.useAmoledMode.checked.value) { color = if (settings.useAmoledMode.checked.value == true) {
Color.Black Color.Black
} else { } else {
getAvgColor(mark.grade) getAvgColor(mark.grade)
@@ -561,7 +674,7 @@ fun MarksDetail() {
shape = RoundedCornerShape(global.dimens.roundedCorner) shape = RoundedCornerShape(global.dimens.roundedCorner)
) )
.background( .background(
if (settings.useAmoledMode.checked.value) { if (settings.useAmoledMode.checked.value == true) {
Color.Black Color.Black
} else { } else {
getAvgColor( getAvgColor(
@@ -597,7 +710,7 @@ fun MarksDetail() {
shape = RoundedCornerShape(global.dimens.roundedCorner) shape = RoundedCornerShape(global.dimens.roundedCorner)
) )
.background( .background(
if (settings.useAmoledMode.checked.value) { if (settings.useAmoledMode.checked.value == true) {
Color.Black Color.Black
} else { } else {
PastelYellowTransparent PastelYellowTransparent
@@ -614,7 +727,7 @@ fun MarksDetail() {
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 5.dp) .padding(horizontal = 5.dp)
.background( .background(
color = if (settings.useAmoledMode.checked.value) { color = if (settings.useAmoledMode.checked.value == true) {
Color.Black Color.Black
} else { } else {
PastelYellowTransparent PastelYellowTransparent
@@ -657,7 +770,7 @@ fun MarksDetail() {
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 5.dp) .padding(horizontal = 5.dp)
.background( .background(
color = if (settings.useAmoledMode.checked.value) { color = if (settings.useAmoledMode.checked.value == true) {
Color.Black Color.Black
} else { } else {
PastelYellowTransparent PastelYellowTransparent
@@ -1,14 +1,30 @@
package com.odweta.solon package com.odweta.solon
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ExitToApp
import androidx.compose.material.icons.filled.AccountBox
import androidx.compose.material.icons.filled.ArrowBackIosNew
import androidx.compose.material.icons.filled.ExitToApp
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -19,8 +35,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.odweta.solon.ui.theme.PastelYellow import com.odweta.solon.ui.theme.PastelYellow
@@ -28,7 +47,7 @@ import kotlin.system.exitProcess
@Composable @Composable
fun MenuScreen() { fun MenuScreen() {
var showDialog by remember { mutableStateOf(false) } val menuSeparatorDimen = 40.dp
Column( Column(
modifier = Modifier modifier = Modifier
@@ -36,87 +55,50 @@ fun MenuScreen() {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
MenuButton({ showDialog = true }, "Odhlásit se") Row {
MenuButton(
Spacer(modifier = Modifier.height(80.dp)) { global.currentScreenElement = ScreenElement.SignOut },
Icons.AutoMirrored.Filled.ExitToApp,
MenuButton({ global.currentScreen = Screen.SignIn }, "Změnit přihlášení") "Odhlásit se"
Spacer(modifier = Modifier.height(80.dp))
MenuButton({ global.currentScreenElement = ScreenElement.About }, "O aplikaci")
Spacer(modifier = Modifier.height(80.dp))
MenuButton({ global.currentScreenElement = ScreenElement.Settings }, "Nastavení")
}
// Dialog implementation
if (showDialog) {
AlertDialog(
onDismissRequest = { showDialog = false }, // Close the dialog on dismiss
title = {
Text(text = "Odhlásit se", color = Color.White)
},
text = {
Text("Opravdu se chcete odhlásit? Tato akce zavře aplikaci.", color = Color.White)
},
containerColor = Color.Black,
modifier = Modifier
.border(
width = global.dimens.borderWidth,
color = PastelYellow,
shape = RoundedCornerShape(global.dimens.roundedCorner)), // Yellow border
confirmButton = {
Button(
onClick = {
showDialog = false
Util.deleteFile(global.filePaths.userData)
Util.deleteFile(global.filePaths.timetable)
Util.deleteFile(global.filePaths.marks)
Util.deleteFile(global.filePaths.accountData)
Util.deleteFile(global.filePaths.lectureNotes)
// do not delete settings
exitProcess(0)
},
colors = ButtonDefaults.buttonColors(
containerColor = Color.Black, // Black background for the button
contentColor = Color.White // Yellow text for the button
),
modifier = Modifier
.border(
width = global.dimens.borderWidth,
color = PastelYellow,
shape = RoundedCornerShape(global.dimens.roundedCorner)) // Yellow border
) {
Text("Ano")
}
},
dismissButton = {
Button(
onClick = {
showDialog = false
},
colors = ButtonDefaults.buttonColors(
containerColor = Color.Black, // Black background for the button
contentColor = Color.White,
// Yellow text for the button
),
modifier = Modifier
.border(
width = global.dimens.borderWidth,
color = PastelYellow,
shape = RoundedCornerShape(global.dimens.roundedCorner)) // Yellow border
) {
Text("Ne")
}
}
) )
Spacer(modifier = Modifier.width(menuSeparatorDimen))
MenuButton(
{
global.noSignInBackHandle = false
global.currentScreen = Screen.SignIn
},
Icons.Default.AccountBox,
"Změnit přihlášení"
)
}
Spacer(modifier = Modifier.height(menuSeparatorDimen))
Row {
MenuButton(
{ global.currentScreenElement = ScreenElement.About },
Icons.Default.Info,
"O aplikaci"
)
Spacer(modifier = Modifier.width(menuSeparatorDimen))
MenuButton(
{ global.currentScreen = Screen.Settings },
Icons.Default.Settings,
"Nastavení"
)
}
} }
} }
@Composable @Composable
fun MenuButton(onClick: () -> Unit, text: String) { fun MenuButton(onClick: () -> Unit, icon: ImageVector, altText: String) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Button( Button(
onClick = onClick, onClick = onClick,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
@@ -129,12 +111,21 @@ fun MenuButton(onClick: () -> Unit, text: String) {
shape = RoundedCornerShape(global.dimens.roundedCorner), shape = RoundedCornerShape(global.dimens.roundedCorner),
color = PastelYellow color = PastelYellow
) )
.width(230.dp) .size(80.dp)
.height(80.dp)
) { ) {
Icon(
imageVector = icon,
contentDescription = altText,
modifier = Modifier.size(40.dp)
)
}
Text( Text(
text = text, text = altText,
fontSize = 23.sp modifier = Modifier.padding(5.dp).width(80.dp),
color = Color.White,
fontSize = 17.sp,
textAlign = TextAlign.Center
) )
} }
} }
@@ -0,0 +1,11 @@
package com.odweta.solon
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
@Composable
fun MessagesScreen() {
Column {
}
}
@@ -38,7 +38,7 @@ fun NavbarScreen() {
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
var spacerModifier = 30.dp var spacerModifier = 30.dp
if (settings.showRefreshButton.checked.value) { if (settings.showRefreshButton.checked.value == true) {
spacerModifier = 15.dp spacerModifier = 15.dp
Button( Button(
onClick = { onClick = {
@@ -11,21 +11,28 @@ import org.json.JSONObject
fun setupAPI() { fun setupAPI() {
global.token = "NETWORK_UNREACHABLE" global.token = "NETWORK_UNREACHABLE"
val token = networkRequestFactory.perform(NetworkRequestType.Token).getString("token")
if (""""error": """ !in token) { val tokenJSON = networkRequestFactory.perform(NetworkRequestType.Token)
if (tokenJSON.has("token")) {
val token = tokenJSON.getString("token")
if ("\"error\": " !in token) {
global.token = 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.userData = JSONObject()
global.studentId = "" global.studentId = ""
return return
} }
global.userData = networkRequestFactory.perform(NetworkRequestType.UserData) global.userData = networkRequestFactory.perform(NetworkRequestType.UserData)
Util.saveStringToFile(global.filePaths.userData, global.userData.toString()) Util.saveStringToFile(global.filePaths.userData, global.userData.toString())
global.studentClass = Parsing.getStudentClass() global.studentClass = Parsing.getStudentClass()
global.studentId = Parsing.getStudentId() global.studentId = Parsing.getStudentId()
global.apiEndPoints.marks = "${global.apiEndPoints.apiBase}/v1/students/${global.studentId}/marks/list" 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 global.isAPISetUp = true
} }
@@ -34,6 +41,7 @@ enum class NetworkRequestType {
Token, Token,
Timetable, Timetable,
Marks, Marks,
FinalMarks,
UserData UserData
} }
@@ -65,6 +73,7 @@ object NetworkRequestFactory: NetworkRequestInterface {
} }
NetworkRequestType.Timetable -> return getAPI(global.apiEndPoints.timetable) NetworkRequestType.Timetable -> return getAPI(global.apiEndPoints.timetable)
NetworkRequestType.Marks -> return getAPI(global.apiEndPoints.marks) NetworkRequestType.Marks -> return getAPI(global.apiEndPoints.marks)
NetworkRequestType.FinalMarks -> return getAPI(global.apiEndPoints.finalMarks)
NetworkRequestType.UserData -> return getAPI(global.apiEndPoints.userData) NetworkRequestType.UserData -> return getAPI(global.apiEndPoints.userData)
} }
} }
@@ -76,7 +85,13 @@ object NetworkRequestFactory: NetworkRequestInterface {
.get() .get()
.build() .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 { private fun post(url: String, headers: Headers, body: FormBody): Response {
@@ -86,11 +101,16 @@ object NetworkRequestFactory: NetworkRequestInterface {
.post(body) .post(body)
.build() .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 { private fun getAPI(url: String): JSONObject {
Log.d("debug TOKEN", global.token)
val response = get(url, headersOf( val response = get(url, headersOf(
"Content-Type", "application/x-www-form-urlencoded", "Content-Type", "application/x-www-form-urlencoded",
"Authorization", "Bearer ${global.token}" "Authorization", "Bearer ${global.token}"
@@ -153,9 +153,12 @@ class Parsing {
subject.name = subj.getJSONObject("subject").getString("abbrev") subject.name = subj.getJSONObject("subject").getString("abbrev")
subject.place = subject.place =
subj.getJSONArray("rooms").getJSONObject(0).getString("abbrev") subj.getJSONArray("rooms").getJSONObject(0).getString("abbrev")
subject.teacher = if (subj.getJSONArray("teachers").length() > 0) { subject.teacher =
subj.getJSONArray("teachers").getJSONObject(0) if (subj.getJSONArray("teachers").length() > 0) {
.getString("fullName") // TODO: keep in mind that fullName might not be a valid field (name and surname are!) subj.getJSONArray("teachers")
.getJSONObject(0).getString("name") + " " +
subj.getJSONArray("teachers")
.getJSONObject(0).getString("surname")
} else { } else {
"" ""
} }
@@ -196,15 +199,13 @@ class Parsing {
val detailHours = subj.getJSONArray("detailHours") val detailHours = subj.getJSONArray("detailHours")
newSubject.number = newSubject.number =
detailHours.getJSONObject(i).get("id").toString() detailHours.getJSONObject(i).getString("id")
newSubject.start = newSubject.start =
detailHours.getJSONObject(i).get("timeFrom") detailHours.getJSONObject(i).getString("timeFrom")
.toString()
newSubject.start = newSubject.start =
newSubject.start.slice(0..newSubject.start.length - 4) newSubject.start.slice(0..newSubject.start.length - 4)
newSubject.end = newSubject.end =
detailHours.getJSONObject(i).get("timeto") detailHours.getJSONObject(i).getString("timeto")
.toString()
newSubject.end = newSubject.end =
newSubject.end.slice(0..newSubject.end.length - 4) newSubject.end.slice(0..newSubject.end.length - 4)
subjects.add(newSubject) subjects.add(newSubject)
@@ -214,7 +215,7 @@ class Parsing {
} }
val sCopy = subjects.toList() val sCopy = subjects.toList()
for (s in 0 until sCopy.size) { for (s in sCopy.indices) {
if ("-" in subjects[s].number) { if ("-" in subjects[s].number) {
subjects[s].number = "1" subjects[s].number = "1"
subjects[s].start = "08:00" subjects[s].start = "08:00"
@@ -223,26 +224,6 @@ class Parsing {
} }
} }
/*
// rust pseudocode
let mut res: Vec<i32> = vec![];
for i in 0..v.len()-1 {
if v[i] == v[i+1]-1 {
res.push(v[i]);
} else {
res.push(v[i]);
while res[res.len()-1]+1 != v[i+1] {
res.push(res[res.len()-1]+1);
}
}
}
res.push(v[v.len()-1]);
res
*/
if (subjects.size > 1) { if (subjects.size > 1) {
for (i in 0..<subjects.size - 1) { for (i in 0..<subjects.size - 1) {
if (subjects[i].number == ((subjects[i + 1].number.toInt() - 1).toString())) { if (subjects[i].number == ((subjects[i + 1].number.toInt() - 1).toString())) {
@@ -322,7 +303,6 @@ class Parsing {
timetable.add(day) timetable.add(day)
} }
Log.d("debug", "timetable: $timetable")
return timetable return timetable
} }
@@ -426,6 +406,14 @@ class Parsing {
return marks return marks
} }
fun parseFinalMarks(json: JSONObject): Map<String, Int> {
return mapOf(
"AJ" to 1,
"B" to 5,
"Pr" to 1
)
}
fun getStudentClass(): String { fun getStudentClass(): String {
return if (!userIsParent()) { return if (!userIsParent()) {
global.userData global.userData
@@ -1,5 +1,6 @@
package com.odweta.solon package com.odweta.solon
import android.util.Log
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -16,18 +17,37 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchColors import androidx.compose.material3.SwitchColors
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.liveData
import com.odweta.solon.ui.theme.PastelYellow import com.odweta.solon.ui.theme.PastelYellow
import kotlinx.coroutines.launch
@Composable @Composable
fun SettingsScreen() { fun SettingsScreen() {
// handle the back gesture (back swipe form side or back button on bottom android navbar) // handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler { global.currentScreenElement = ScreenElement.Menu } BackHandler {
global.currentScreenElement = ScreenElement.Menu
global.currentScreen = Screen.Main
}
// 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?
if (!global.demoMode) {
settings = Util.fileToString(global.filePaths.settings).toSettingsData()
}
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@@ -38,22 +58,22 @@ fun SettingsScreen() {
) { ) {
backButtonFactory.get(BackButtonType.Menu)() backButtonFactory.get(BackButtonType.Menu)()
Spacer(Modifier.height(10.dp))
SettingsTableRow(settings.useSvobodaMode) SettingsTableRow(settings.useSvobodaMode)
Spacer(Modifier.height(10.dp))
SettingsTableRow(settings.useAmoledMode) SettingsTableRow(settings.useAmoledMode)
Spacer(Modifier.height(10.dp)) if (!global.demoMode) {
SettingsTableRow(settings.showRefreshButton) SettingsTableRow(settings.showRefreshButton)
} }
SettingsTableRow(settings.useAnimations)
}
} }
@Composable @Composable
fun SettingsTableRow(setting: Setting) { fun SettingsTableRow(setting: Setting) {
Spacer(Modifier.height(10.dp))
Row( Row(
modifier = Modifier modifier = Modifier
.padding(10.dp) .padding(10.dp)
@@ -71,15 +91,10 @@ fun SettingsTableRow(setting: Setting) {
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Switch( Switch(
checked = setting.checked.value, checked = setting.checked.value == true,
onCheckedChange = { onCheckedChange = {
setting.checked.value = it setting.checked.value = it
when (setting.id) { showTempScreen(Screen.Settings)
SettingsId.Svoboda -> settings.useSvobodaMode = setting
SettingsId.Amoled -> settings.useAmoledMode = setting
SettingsId.RefreshButton -> settings.showRefreshButton = setting
else -> {}
}
Util.saveStringToFile(global.filePaths.settings, settings.toString()) Util.saveStringToFile(global.filePaths.settings, settings.toString())
}, },
colors = SwitchColors( colors = SwitchColors(
@@ -113,6 +128,7 @@ fun String.toSettingsData(): SettingsData {
val useSvobodaMode = settings[0].split("#") val useSvobodaMode = settings[0].split("#")
val useAmoledMode = settings[1].split("#") val useAmoledMode = settings[1].split("#")
val showRefreshButton = settings[2].split("#") val showRefreshButton = settings[2].split("#")
val useAnimations = settings[3].split("#")
auxSettingsData.useSvobodaMode.id = SettingsId.Svoboda auxSettingsData.useSvobodaMode.id = SettingsId.Svoboda
auxSettingsData.useSvobodaMode.name = useSvobodaMode[1] auxSettingsData.useSvobodaMode.name = useSvobodaMode[1]
@@ -126,5 +142,9 @@ fun String.toSettingsData(): SettingsData {
auxSettingsData.showRefreshButton.name = showRefreshButton[1] auxSettingsData.showRefreshButton.name = showRefreshButton[1]
auxSettingsData.showRefreshButton.checked.value = showRefreshButton[2].toBoolean() auxSettingsData.showRefreshButton.checked.value = showRefreshButton[2].toBoolean()
auxSettingsData.useAnimations.id = SettingsId.Animations
auxSettingsData.useAnimations.name = useAnimations[1]
auxSettingsData.useAnimations.checked.value = useAnimations[2].toBoolean()
return auxSettingsData return auxSettingsData
} }
@@ -1,6 +1,7 @@
package com.odweta.solon package com.odweta.solon
import android.content.Context import android.content.Context
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@@ -40,6 +41,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
private var buttonText by mutableStateOf("Přihlásit se") private var buttonText by mutableStateOf("Přihlásit se")
@@ -87,6 +89,10 @@ fun signInButtonCallback(ctx: Context, usernameText: String, passwordText: Strin
} }
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
if (global.username == "demo" && global.password == "Demo1234") {
// start the demo
launchDemo(ctx)
} else {
val valid = Account.isValid(acc) val valid = Account.isValid(acc)
checked = true checked = true
@@ -96,6 +102,7 @@ fun signInButtonCallback(ctx: Context, usernameText: String, passwordText: Strin
global.currentScreen = Screen.Splash global.currentScreen = Screen.Splash
launchStart(ctx) launchStart(ctx)
} else { } else {
withContext(Dispatchers.Main) {
Toast.makeText( Toast.makeText(
ctx, ctx,
"Neplatné přihlašovací údaje nebo nejste připojeni k internetu, zkuste to znovu.", "Neplatné přihlašovací údaje nebo nejste připojeni k internetu, zkuste to znovu.",
@@ -104,6 +111,32 @@ fun signInButtonCallback(ctx: Context, usernameText: String, passwordText: Strin
} }
} }
} }
}
}
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 @Composable
fun SignInScreen() { fun SignInScreen() {
@@ -115,7 +148,6 @@ fun SignInScreen() {
var usernameText by remember { mutableStateOf("") } var usernameText by remember { mutableStateOf("") }
var passwordText by remember { mutableStateOf("") } var passwordText by remember { mutableStateOf("") }
val ctx = LocalContext.current val ctx = LocalContext.current
val coscope = rememberCoroutineScope()
var isPasswordVisible by remember { mutableStateOf(false) } // State for visibility toggle var isPasswordVisible by remember { mutableStateOf(false) } // State for visibility toggle
val visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation() val visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation()
@@ -0,0 +1,107 @@
package com.odweta.solon
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.odweta.solon.ui.theme.PastelYellow
import kotlin.system.exitProcess
@Composable
fun SignOutScreen() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler { global.currentScreenElement = ScreenElement.Menu }
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start
) {
backButtonFactory.get(BackButtonType.Menu)()
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Opravdu se chcete odhlásit? Tato akce zavře aplikaci.",
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 27.sp,
lineHeight = 40.sp,
modifier = Modifier.padding(bottom = 10.dp).padding(horizontal = 10.dp)
)
Row {
SignOutButton(
onClick = {
Util.deleteFile(global.filePaths.userData)
Util.deleteFile(global.filePaths.timetable)
Util.deleteFile(global.filePaths.marks)
Util.deleteFile(global.filePaths.accountData)
Util.deleteFile(global.filePaths.lectureNotes)
// do not delete settings
exitProcess(0)
},
label = "Ano"
)
Spacer(Modifier.width(10.dp))
SignOutButton(
onClick = {
global.currentScreenElement = ScreenElement.Menu
},
label = "Ne"
)
}
}
}
}
@Composable
private fun SignOutButton(onClick: () -> Unit, label: String) {
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Black, // Black background for the button
contentColor = Color.White // Yellow text for the button
),
modifier = Modifier
.border(
width = global.dimens.borderWidth,
color = PastelYellow,
shape = RoundedCornerShape(global.dimens.roundedCorner)
) // Yellow border
) {
Text(label, fontSize = 17.sp)
}
}
@@ -0,0 +1,22 @@
package com.odweta.solon
import androidx.compose.runtime.Composable
// temporary screens to artificially trigger recompositions basically at the root level
@Composable
fun TempScreen() {
when (global.tempScreen) {
Screen.Main -> MainScreen()
Screen.Splash -> SplashScreen()
Screen.SignIn -> SignInScreen()
Screen.Settings -> SettingsScreen()
Screen.Temp -> {}
}
}
fun showTempScreen(screen: Screen) {
global.returnToTempScreen = true
global.tempScreen = screen
global.currentScreen = Screen.Temp
}
@@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -49,6 +50,7 @@ import androidx.compose.ui.focus.focusModifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -64,8 +66,11 @@ import com.odweta.solon.ui.theme.PastelRed
import com.odweta.solon.ui.theme.PastelRedTransparent import com.odweta.solon.ui.theme.PastelRedTransparent
import com.odweta.solon.ui.theme.PastelYellow import com.odweta.solon.ui.theme.PastelYellow
import com.odweta.solon.ui.theme.PastelYellowTransparent import com.odweta.solon.ui.theme.PastelYellowTransparent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
@@ -73,16 +78,65 @@ import java.time.format.DateTimeFormatter
var showDetailDialog by mutableStateOf(false) var showDetailDialog by mutableStateOf(false)
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun TimetableScreen() { fun TimetableScreen() {
val pagerState = rememberPagerState(pageCount = { if (global.timetable.isNotEmpty()) global.timetable.size else 6 }) // FIXME
Column {
//QuickDays(pagerState)
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter contentAlignment = Alignment.BottomCenter
) { ) {
DayPager() DayPager(pagerState)
if (showDetailDialog) DetailDialog() 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)
) {
for ((idx, day) in global.timetable.withIndex()) {
QuickDay(day, Modifier.weight(1f), idx)
}
}
}
@SuppressLint("UnrememberedMutableState") @SuppressLint("UnrememberedMutableState")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -226,13 +280,28 @@ fun DetailDialog() {
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun DayPager() { fun DayPager(pagerState: PagerState) {
val pagerState = rememberPagerState(pageCount = { if (global.timetable.isNotEmpty()) global.timetable.size else 6 }) // FIXME
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { pageIndex -> ) { pageIndex ->
if (global.timetable.isNotEmpty()) {
DayScreen(global.timetable[pageIndex]) 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
)
} }
} }
@@ -257,6 +326,7 @@ private fun getSubjectColor(s: Subject, transparent: Boolean): Color {
} }
} }
} }
fun isTimeBetweenT(givenTime: LocalTime, startTime: LocalTime, endTime: LocalTime): Boolean { fun isTimeBetweenT(givenTime: LocalTime, startTime: LocalTime, endTime: LocalTime): Boolean {
// Check if the given time is between startTime and endTime // Check if the given time is between startTime and endTime
return givenTime.isAfter(startTime) && givenTime.isBefore(endTime) return givenTime.isAfter(startTime) && givenTime.isBefore(endTime)
@@ -293,7 +363,7 @@ fun subjectModifier(d: Day, s: Subject, lectureNumber: Int): Modifier {
global.currentLectureNumber = lectureNumber global.currentLectureNumber = lectureNumber
} }
val m = if (settings.useAmoledMode.checked.value) { val m = if (settings.useAmoledMode.checked.value == true) {
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.height(70.dp) .height(70.dp)
@@ -356,8 +426,10 @@ private fun DayScreen(day: Day) {
) { ) {
for ((subjectIndex, subject) in day.subjects.withIndex()) { for ((subjectIndex, subject) in day.subjects.withIndex()) {
val lectureId = Util.getLectureId(day.date, subjectIndex) 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) { val amoledModifier = if (settings.useAmoledMode.checked.value == true) {
Modifier Modifier
.width(50.dp) .width(50.dp)
.fillMaxHeight() .fillMaxHeight()
@@ -438,8 +510,9 @@ private fun DayScreen(day: Day) {
Text( Text(
text = " supl ", text = " supl ",
color = Color.Black, color = Color.White,
modifier = Modifier.background(color = Color.White) fontStyle = FontStyle.Italic,
modifier = Modifier.background(color = PastelRedTransparent)
) )
} }
} else { } else {
@@ -482,10 +555,9 @@ private fun DayScreen(day: Day) {
.padding(vertical = 5.dp), .padding(vertical = 5.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (settings.useSvobodaMode.checked.value) { if (settings.useSvobodaMode.checked.value == true) {
if ( if (
subject.teacher == "Jindřich Svoboda" && subject.teacher == "Jindřich Svoboda"
global.studentClass == "5. G"
) { ) {
Image( Image(
painter = painterResource(R.drawable.spock), painter = painterResource(R.drawable.spock),
@@ -531,10 +603,10 @@ private fun DayScreen(day: Day) {
Box(modifier = Modifier Box(modifier = Modifier
.fillMaxSize() .fillMaxSize()
.zIndex(99f) .zIndex(99f)
.clickable { /*.clickable {
global.selectedLecture = lectureId global.selectedLecture = lectureId
showDetailDialog = true showDetailDialog = true
}) }*/)
if ( if (
(global.lectureNotes[global.selectedLecture] != null && (global.lectureNotes[global.selectedLecture] != null &&
@@ -1,10 +1,21 @@
package com.odweta.solon package com.odweta.solon
import android.content.Context
import android.util.Log import android.util.Log
import android.widget.ImageView
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Upload
import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -12,16 +23,22 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import com.bumptech.glide.Glide
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.json.JSONObject import org.json.JSONObject
private var showLoadingIcon by mutableStateOf(false)
@Composable @Composable
fun ToolbarScreen() { fun ToolbarScreen() {
// make the font size adaptive relative to the length of the user's name // make the font size adaptive relative to the length of the user's name
@@ -43,12 +60,28 @@ fun ToolbarScreen() {
toolbarTextSizeAdaptive = 10.sp toolbarTextSizeAdaptive = 10.sp
} }
Box(modifier = Modifier.padding(10.dp)) { Animate(global.status) { targetText ->
Row(modifier = Modifier.padding(10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
Text( Text(
text = global.status, text = targetText,
fontSize = toolbarTextSizeAdaptive, fontSize = toolbarTextSizeAdaptive,
color = Color.White color = Color.White
) )
if (settings.useAnimations.checked.value == true) {
AnimatedVisibility(showLoadingIcon) {
AndroidView(
modifier = Modifier.size(50.dp).padding(start = 10.dp),
factory = { context: Context -> ImageView(context) },
update = { imageView ->
Glide.with(imageView.context)
.load(R.drawable.loading)
.into(imageView)
}
)
}
}
}
} }
} }
@@ -80,20 +113,14 @@ object StatusFactory: StatusFactoryInterface {
StatusType.Loading -> { StatusType.Loading -> {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
global.refreshed = false global.refreshed = false
val anim = listOf( showStatus("Načítání nových dat")
"Načítání nových dat", delay(100L)
"Načítání nových dat.", showLoadingIcon = true
"Načítání nových dat..",
"Načítání nových dat...",
"Načítání nových dat..",
"Načítání nových dat."
)
var i = 0
while (!global.refreshed) { while (!global.refreshed) {
if (i == anim.size) i = 0 showStatus("Načítání nových dat")
showStatus(anim[i++])
delay(300)
} }
showLoadingIcon = false
} }
} }
} }
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
cp app/build/outputs/apk/release/com.odweta.solon_$1.apk ../release/