72 Commits
Author SHA1 Message Date
Odweta 3d0c752f0c made new release 2.3.0 2024-12-22 16:18:20 +01:00
Odweta da890ea180 remade new marks into a FAB 2024-12-22 16:16:07 +01:00
Odweta 49a28b0064 optimizations 2024-12-22 13:14:13 +01:00
Odweta fbc528b450 network request factory working 2024-12-22 13:10:55 +01:00
Odweta 99fe3a0243 network request factory working 2024-12-22 13:10:48 +01:00
Odweta e5d807b6d8 removed all redundant bits (variables and stuff) from Global.kt and converted them into the global struct 2024-12-22 12:21:38 +01:00
Odweta 8b7301a30d improved refreshed button 2024-12-22 12:07:51 +01:00
Odweta f0fcad3491 improved refresh button in navbar 2024-12-22 12:05:34 +01:00
Odweta 377156a31c settings work 2024-12-22 11:54:10 +01:00
Odweta 78fca78c75 about works 2024-12-22 11:51:43 +01:00
Odweta 7618535493 menu working 2024-12-22 11:40:39 +01:00
Odweta eb8f4796a9 navbar working 2024-12-22 10:06:56 +01:00
Odweta ac1c1dfa26 refresh button working (not 100% test coverage) 2024-12-22 09:49:21 +01:00
Odweta ce6afefd3f toolbar working (removed the loading 'animation' when fetching new remote data) 2024-12-21 22:59:47 +01:00
Odweta 89dd624b00 started working on the grand refactor (the app launches) 2024-12-20 20:29:19 +01:00
Odweta f9d4ff562d refined the launch proccess and improved the architecture of global variables 2024-12-20 17:31:28 +01:00
Odweta 3240313f75 update deploy script 2024-12-18 14:14:03 +01:00
Odweta 59eab71cc4 made deploy script 2024-12-18 14:10:36 +01:00
Odweta eb289db339 gitignore 2024-12-18 14:09:31 +01:00
Odweta 5b0f016add gitignore update 2024-12-18 11:41:15 +01:00
Odweta f8b05884d3 made new release 2.2.2 (207) 2024-12-18 11:31:59 +01:00
Odweta 1344d57685 gitignore and update fastlane 2024-12-18 11:03:43 +01:00
Odweta cb57affc10 lecture notes now work and also added some cosmetic fixes 2024-12-18 10:00:53 +01:00
Odweta 4f43154a13 made new release 2.2.1 2024-12-16 11:08:15 +01:00
Odweta 24dbb3969c improved some stuff 2024-12-16 10:57:02 +01:00
Odweta c7dacbf1bd settings: optionally tag new mark 2024-12-16 10:19:10 +01:00
Odweta af96914ec7 tagged lectures with notes 2024-12-16 10:09:09 +01:00
Odweta 8ddbdc43f9 improved lecture notes with a type and showing the name and number of the lecture 2024-12-16 09:53:19 +01:00
Odweta 3bb5ce5ca7 fixed incorrect color in MarksDetail regarding the average field 2024-12-15 10:42:14 +01:00
Odweta bda937095c made new release 2.2 (205) 2024-12-15 00:20:17 +01:00
Odweta 67392ccb3d release 2.1 (pull) 2024-12-15 00:09:40 +01:00
Odweta a163d6b285 you can now add notes to every lecture and they will automatically be saved locally 2024-12-15 00:05:44 +01:00
Odweta e67f87864c WIP lecture notes 2024-12-14 20:21:49 +01:00
Odweta 8e9693bfe6 fixed colors in MarksSubject and tagged new marks 2024-12-14 18:44:21 +01:00
Odweta c6646e554f made new release 2.1 (204) 2024-12-08 09:35:46 +01:00
Odweta 98b6b72e81 add generic release app name 2024-12-03 19:19:20 +01:00
Odweta c6a5c50a7d build and sign release 2.1 2024-12-03 16:28:14 +01:00
Odweta 09bddaac3c made new logo 2024-12-02 13:48:35 +01:00
Odweta bb39f10a0b made new logo 2024-12-02 13:48:27 +01:00
Odweta 7e44cb585b sign release again 2024-12-02 11:06:11 +01:00
Odweta 91ba977a86 made release 2.1.1 2024-12-01 15:21:00 +01:00
Odweta 5d9b70f3a1 moved the refresh button to the navbar 2024-12-01 15:17:55 +01:00
Odweta 75a766ca7d signed new release 2.1 (204) 2024-12-01 08:41:55 +01:00
Odweta 23607f93b3 hotfix debug residue 2024-11-30 23:03:12 +01:00
Odweta 887c7d58b5 made new release 2.1 2024-11-30 12:33:08 +01:00
Odweta 4d96707a51 fixed settings file read error 2024-11-30 12:15:36 +01:00
Odweta 5a98379d3f made the AMOLED mode work 2024-11-30 12:06:51 +01:00
Odweta 80e5581f45 made the Svoboda mode work and improved the toolbar ux 2024-11-30 11:38:50 +01:00
Odweta 5200e3ba07 improved menu layout 2024-11-30 11:23:04 +01:00
Odweta f67e28b8f6 made the showRefreshButton setting work 2024-11-30 11:15:08 +01:00
Odweta 597971288e improved the displaying of averages with marks 2024-11-30 11:10:23 +01:00
Odweta 0a4ea395fd added a settings screen skeleton 2024-11-30 09:52:32 +01:00
Odweta 509f7f33a7 added a mark to substituted lectures 2024-11-29 18:41:48 +01:00
Odweta 87470effcd improved the About screen and added version number 2024-11-29 18:33:36 +01:00
Odweta 079872dae2 locked the screen orientation in portrait mode 2024-11-29 10:25:28 +01:00
Odweta 26317052cc added prepending free subjects to timetable when the day does not start with subject number 1 2024-11-26 18:52:03 +01:00
Odweta ed981f89de added a refresh button to the timetable 2024-11-26 18:04:05 +01:00
Odweta fd788195c5 added icon and screenshots 2024-11-20 13:10:18 +01:00
Odweta ed1f67f3e6 signed the apk 2024-11-20 11:20:50 +01:00
Odweta 980e51e8f1 removed dependency info block and made new release 2.0.3 2024-11-19 19:09:21 +01:00
Odweta ebfc3c1c23 still trying to fix it 2024-11-09 10:04:50 +01:00
Odweta 07716e1ae6 fix build error (perhaps) again 2024-11-09 09:57:19 +01:00
Odweta 1049401429 fix build error (perhaps) 2024-11-09 09:50:58 +01:00
Odweta 83a825f4b7 naming output files based on versionName 2024-11-08 23:48:38 +01:00
Odweta 66dc2338fb added fastlane folders 2024-11-08 23:34:36 +01:00
Odweta 7c585709ed versioncode bump thing 2024-11-08 22:54:46 +01:00
Odweta f448420e03 update TODO.md 2024-11-08 19:07:52 +01:00
Odweta ec989d6ae0 add TODO.md 2024-11-08 19:07:18 +01:00
Odweta db91df4d22 bump version to 2.0.2 2024-11-08 19:04:10 +01:00
Odweta e020ae5f17 made the sign in layout responsive to the keyboard 2024-11-08 18:59:25 +01:00
Odweta d3197111c3 quality of life fixes
made the toolbar font size adaptively large relative to the user's name length, shrunk down the height of subjects and marks and made back gestures work properly - not close the app but show the previous screen
2024-11-08 18:09:55 +01:00
Odweta 8a4956e6c6 hotfix app crash, bump to 2.0.1 2024-10-25 16:44:05 +02:00
82 changed files with 2348 additions and 1449 deletions
+1
View File
@@ -13,3 +13,4 @@
.externalNativeBuild .externalNativeBuild
.cxx .cxx
local.properties local.properties
mobile/app/build
+10
View File
@@ -0,0 +1,10 @@
<?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
@@ -0,0 +1,12 @@
<?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
@@ -0,0 +1,17 @@
<?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
-2
View File
@@ -2,7 +2,5 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" /> <mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/help" vcs="Git" />
<mapping directory="$PROJECT_DIR$/mobile/help" vcs="Git" />
</component> </component>
</project> </project>
+2 -2
View File
@@ -1,3 +1,3 @@
![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/-/raw/master/release/solon.apk?ref_type=heads&inline=false) (verze 2.0) # 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)
+4
View File
@@ -0,0 +1,4 @@
- add settings
- color schemes
- modes
- new logo
Executable
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
echo -n "version number: "
read ver
echo -n "glpat: "
read glpat
cd ./mobile
# build
./gradlew assembleRelease
# 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
# add, commit, push
git add ..
git commit -m "made new release $ver"
git push
# create the tag
echo "\n[creating tag v$ver]"
data_str='{ "tag_name": "'$ver'", "ref": "HEAD", "message": "Tagging release v'$ver'" }'
curl --request POST \
--header "PRIVATE-TOKEN: $glpat" \
--header "Content-Type: application/json" \
--data "$data_str" \
"https://gitlab.com/api/v4/projects/47146187/repository/tags"
# make a release
echo "\n[publishing release $ver]"
data_str='{ "name": "v'$ver'", "tag_name": "'$ver'", "ref": "HEAD" }'
curl --request POST \
--header "PRIVATE-TOKEN: $glpat" \
--header 'Content-Type: application/json' \
--data "$data_str" \
"https://gitlab.com/api/v4/projects/47146187/releases"
# attach a file
echo "\n[attaching the release apk]"
asset_url="https://gitlab.com/-/project/47146187"$(curl --request POST \
--header "PRIVATE-TOKEN: $glpat" \
--form "file=@app/build/outputs/apk/release/com.odweta.solon_$ver.apk" \
"https://gitlab.com/api/v4/projects/47146187/uploads" | jq .url -r)
# add the file as a stable release asset
echo "\n[creating release asset]"
curl --request POST \
--header "PRIVATE-TOKEN: $glpat" \
--data name="app-release.apk" \
--data url="$asset_url" \
--data filepath="/app-release.apk" \
"https://gitlab.com/api/v4/projects/47146187/releases/$ver/assets/links"
# cd back
cd -
@@ -0,0 +1 @@
Optimalizovana a zbavena nesmyslu verze skolniho online systemu SkolaOnLine
Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

@@ -0,0 +1 @@
Odlehcena verze aplikace SkolaOnLine
+1
View File
@@ -0,0 +1 @@
Solon
@@ -0,0 +1 @@
A more optimized and debloated version of the SkolaOnLine online school system
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1 @@
A light-weight Android frontend to SkolaOnLine
@@ -0,0 +1 @@
Solon
+12
View File
@@ -62,6 +62,18 @@
<PersistentState> <PersistentState>
<option name="children"> <option name="children">
<map> <map>
<entry key="backgroundImage">
<value>
<PersistentState>
<option name="values">
<map>
<entry key="imagePath" value="$PROJECT_DIR$/app/src/main/res/drawable/black.png" />
<entry key="scalingPercent" value="172" />
</map>
</option>
</PersistentState>
</value>
</entry>
<entry key="foregroundClipArt"> <entry key="foregroundClipArt">
<value> <value>
<PersistentState> <PersistentState>
+6 -3
View File
@@ -4,16 +4,19 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-10-20T07:27:51.115175643Z"> <DropdownSelection timestamp="2024-11-29T17:18:42.354545820Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/home/thinker/.android/avd/Motorola_Moto_e22_API_31.avd" /> <DeviceId pluginId="LocalEmulator" identifier="path=/home/thinker/.config/.android/avd/Medium_Phone_API_35.avd" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>
<DialogSelection /> <DialogSelection />
</SelectionState> </SelectionState>
<SelectionState runConfigName="DefaultPreview"> <SelectionState runConfigName="TimetablePreview">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="RBPreview">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
</SelectionState> </SelectionState>
</selectionStates> </selectionStates>
+10
View File
@@ -3,15 +3,19 @@
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> <inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
@@ -27,21 +31,27 @@
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true"> <inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" /> <option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool> </inspection_tool>
</profile> </profile>
</component> </component>
+1 -2
View File
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" /> <mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$/help" vcs="Git" />
</component> </component>
</project> </project>
+23 -4
View File
@@ -12,12 +12,25 @@ android {
applicationId = "com.odweta.solon" applicationId = "com.odweta.solon"
minSdk = 28 minSdk = 28
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 208
versionName = "2.0" versionName = "2.3.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
applicationVariants.all {
this.outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output ->
val apkName = "com.odweta.solon_" + this.versionName + ".apk"
output.outputFileName = apkName
}
}
buildFeatures {
compose = true
}
buildTypes { buildTypes {
release { release {
isMinifyEnabled = false isMinifyEnabled = false
@@ -27,15 +40,21 @@ android {
) )
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11
} }
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "11"
} }
buildFeatures {
compose = true dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
} }
} }
+4 -1
View File
@@ -18,9 +18,12 @@
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:screenOrientation="portrait"
android:exported="true" android:exported="true"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.Solon"> android:theme="@style/Theme.Solon"
android:windowSoftInputMode="adjustResize"
tools:ignore="DiscouragedApi">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@@ -1,6 +1,8 @@
package com.odweta.solon package com.odweta.solon
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -8,6 +10,7 @@ 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.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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
@@ -24,6 +27,7 @@ import androidx.compose.runtime.Composable
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.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import com.odweta.solon.ui.theme.PastelYellow import com.odweta.solon.ui.theme.PastelYellow
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@@ -32,44 +36,19 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
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 java.time.format.TextStyle
@Composable @Composable
fun AboutScreen(modifier: Modifier = Modifier) { fun AboutScreen() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler { global.currentScreenElement = ScreenElement.Menu }
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxSize(),
verticalArrangement = Arrangement.Top, verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start horizontalAlignment = Alignment.Start
) { ) {
Box( backButtonFactory.get(BackButtonType.Menu)()
modifier = Modifier
.fillMaxWidth()
) {
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(100.dp)
.height(100.dp)
) {
Icon(
imageVector = Icons.Default.ArrowBackIosNew, // Replace with your desired icon
contentDescription = "Back icon",
modifier = Modifier.size(35.dp) // Adjust size if needed
)
}
}
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -100,30 +79,71 @@ fun AboutScreen(modifier: Modifier = Modifier) {
.padding(10.dp) .padding(10.dp)
) )
HorizontalLine()
AboutTableRow(
"Vývoj",
"@Odweta",
"https://www.gitlab.com/Odweta/solon"
)
AboutTableRow(
"Verze aplikace",
Util.getAppVersion(LocalContext.current)?.versionName ?: "N/A"
)
}
}
}
@Composable
fun HorizontalLine() {
Spacer(modifier = Modifier.height(20.dp))
Spacer(modifier = Modifier
.height(2.dp)
.background(color = PastelYellow)
.fillMaxWidth(fraction = 0.8f)
)
Spacer(modifier = Modifier.height(20.dp))
}
@Composable
fun AboutTableRow(key: String, value: String, href: String = "") {
Row( Row(
modifier = Modifier modifier = Modifier
.padding(10.dp) .padding(10.dp)
.background(color = Color.Black, shape = RoundedCornerShape(global.dimens.roundedCorner))
.fillMaxWidth()
) { ) {
val uriHandler = LocalUriHandler.current
Text( Text(
text = "Vývoj: ", text = key,
textAlign = TextAlign.Start, textAlign = TextAlign.Start,
color = Color.White, color = Color.White,
fontSize = 30.sp fontSize = 20.sp
) )
Spacer(modifier = Modifier.weight(1f))
if (href != "") {
val uriHandler = LocalUriHandler.current
Text( Text(
text = "@Odweta", text = value,
color = PastelYellow, color = PastelYellow,
fontSize = 30.sp, fontSize = 20.sp,
textAlign = TextAlign.End, textAlign = TextAlign.End,
textDecoration = TextDecoration.Underline, textDecoration = TextDecoration.Underline,
modifier = Modifier.clickable { modifier = Modifier.clickable {
uriHandler.openUri("https://www.gitlab.com/Odweta/solon") uriHandler.openUri(href)
} }
) )
} else {
Text(
text = value,
color = Color.White,
fontSize = 20.sp,
textAlign = TextAlign.End
)
} }
} }
} }
}
@@ -27,7 +27,7 @@ class Account(val username: String, val password: String, private val error: Int
*/ */
// check if file exists // check if file exists
val file = File(fileDir, accountDataFilePath) val file = File(global.fileDir, global.filePaths.accountData)
return if (!file.exists()) { return if (!file.exists()) {
// it does not exist // it does not exist
@@ -36,15 +36,15 @@ class Account(val username: String, val password: String, private val error: Int
} 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(file)
username = acc.username global.username = acc.username
password = acc.password global.password = acc.password
acc.error == 0 acc.error == 0
} }
} }
fun save(acc: Account): Boolean { fun save(acc: Account): Boolean {
return try { return try {
val file = File(fileDir, accountDataFilePath) val file = File(global.fileDir, global.filePaths.accountData)
file.writeText("${acc.username}\n${acc.password}\n", Charset.forName("UTF-8")) file.writeText("${acc.username}\n${acc.password}\n", Charset.forName("UTF-8"))
true true
@@ -55,10 +55,10 @@ class Account(val username: String, val password: String, private val error: Int
suspend fun isValid(acc: Account): Boolean { suspend fun isValid(acc: Account): Boolean {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
username = acc.username global.username = acc.username
password = acc.password global.password = acc.password
val token = Network.getToken() val token = networkRequestFactory.perform(NetworkRequestType.Token).getString("token")
!JSONObject(token).has("error") !JSONObject(token).has("error")
} }
} }
@@ -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 = global.dimens.borderWidth,
shape = RoundedCornerShape(global.dimens.roundedCorner),
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 { global.currentScreenElement = ScreenElement.Menu } } }
BackButtonType.MarksAll -> { { BackButton { global.marksDepthLevel = MarksDepthLevel.All } } }
BackButtonType.MarksSubject -> { { BackButton { global.marksDepthLevel = MarksDepthLevel.Subject } } }
}
}
}
val backButtonFactory = BackButtonFactory
@@ -0,0 +1,262 @@
package com.odweta.solon
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.odweta.solon.ui.theme.PastelYellow
import org.json.JSONObject
import java.io.File
import java.time.LocalDate
import java.time.LocalDateTime
enum class Screen {
Main,
Splash,
SignIn
}
// default settings (will get overwritten by the saved settings)
class Setting(auxId: SettingsId, auxName: String, auxChecked: Boolean) {
var id: SettingsId = auxId
var name: String = auxName
var checked: MutableState<Boolean> = mutableStateOf(auxChecked)
override fun toString() = "$id#$name#${checked.value}"
}
enum class SettingsId {
Svoboda,
Amoled,
RefreshButton
}
class SettingsData {
var useSvobodaMode: Setting = Setting(
SettingsId.Svoboda,
"Používat Svoboda mód",
false
)
var useAmoledMode: Setting = Setting(
SettingsId.Amoled,
"Používat AMOLED mód",
false
)
var showRefreshButton: Setting = Setting(
SettingsId.RefreshButton,
"Zobrazovat tlačítko obnovení dat",
true
)
override fun toString() = "$useSvobodaMode\n$useAmoledMode\n$showRefreshButton"
}
enum class LectureNoteType {
Normal, // obycejna poznamka
Writing, // pisemny test
Speaking, // ustni zkouseni
Other // cokoliv jineho
}
fun String.toLectureNoteType(): LectureNoteType {
return when(this) {
"Normal" -> LectureNoteType.Normal
"Writing" -> LectureNoteType.Writing
"Speaking" -> LectureNoteType.Speaking
"Other" -> LectureNoteType.Other
else -> LectureNoteType.Normal
}
}
class LectureNote(id: Int, note: MutableState<String>, type: MutableState<LectureNoteType>, number: Int, lectureName: String) {
var id: Int = 0
var note: MutableState<String> =
mutableStateOf("")
var type: MutableState<LectureNoteType> =
mutableStateOf(LectureNoteType.Normal)
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 {
lateinit var name: String
lateinit var place: String
lateinit var teacher: String
lateinit var substitute: String
lateinit var number: String
lateinit var start: String
lateinit var end: String
var free: Boolean = false
companion object {
fun freeSubject(number: String, start: String, end: String): Subject {
val s = Subject()
s.name = "Volno"
s.place = "Volno"
s.teacher = "Volno"
s.substitute = "0"
s.number = number
s.start = start
s.end = end
s.free = true
return s
}
fun empty(): Subject {
val s = Subject()
s.name = "err"
s.place = "error"
s.teacher = "error"
s.substitute = "0"
s.number = "0"
s.start = "00:00"
s.end = "00:00"
s.free = false
return s
}
}
override fun toString(): String {
return "subject ${this.name} at ${this.place} from ${this.start} to ${this.end}; free? ${this.free}"
}
}
class Day {
var name: String = ""
var date: String = ""
var subjects: List<Subject> = mutableListOf()
}
class Mark {
lateinit var grade: String // markText
lateinit var weight: String // weight
lateinit var subject: String // subjectId -> parse that
lateinit var date: String // markDate
lateinit var theme: String // theme
var editDate: LocalDateTime = LocalDateTime.now()
override fun toString(): String {
return "mark: ${this.grade} * ${this.weight}, subject: ${this.subject}, description: ${this.theme}, date: ${this.date}"
}
fun none(): Mark {
val mark = Mark()
mark.subject = "Zatím žádné známky."
mark.grade = "0"
mark.weight = "0"
mark.date = "-"
mark.theme = "-"
return mark
}
}
enum class ScreenElement {
Timetable,
Marks,
Menu,
About,
Settings
}
class Global {
lateinit var timetableJSON: JSONObject
lateinit var marksJSON: JSONObject
lateinit var marks: MutableMap<String, MutableList<Mark>>
lateinit var newMarks: MutableMap<String, MutableList<Mark>>
lateinit var settings: SettingsData
lateinit var fileDir: File
lateinit var token: String
lateinit var studentClass: String
lateinit var studentId: String
var userData: JSONObject = JSONObject()
var timetable: List<Day> = listOf()
var lectureNotes: HashMap<Int, LectureNote> = hashMapOf()
var subjectNameMap: HashMap<String, String> = hashMapOf()
var username: String = ""
var password: String = ""
var noSignInBackHandle: Boolean = false
var internetAwaitListenerRunning: Boolean = false
var selectedLecture: Int = 0
var currentLectureNumber: Int = 0
var currentScreen by mutableStateOf(Screen.Main)
var currentScreenElement by mutableStateOf(ScreenElement.Timetable)
var status by mutableStateOf("")
var isAPISetUp by mutableStateOf(false)
var launched by mutableStateOf(false)
var refreshed by mutableStateOf(false)
var marksDepthLevel by mutableStateOf(MarksDepthLevel.All)
var subjectToShow by mutableStateOf("")
var markToShow by mutableStateOf(Mark())
inner class FilePaths {
val accountData: String = "account_data.txt"
val timetable: String = "timetable.json"
val userData: String = "userdata.json"
val marks: String = "marks.json"
val settings: String = "settings.txt"
val lectureNotes: String = "lecture_notes.csv"
}
val filePaths = FilePaths()
inner class APIEndPoints {
val apiBase = "https://aplikace.skolaonline.cz/solapi/api"
val token = "$apiBase/connect/token"
val userData = "$apiBase/v1/user"
val timetable = "$apiBase/v1/timeTable"
var marks: String = "$apiBase/v1/students/<student id>/marks/list"
}
val apiEndPoints = APIEndPoints()
inner class Dimens {
val borderWidth = 5.dp
val roundedCorner = 8.dp
}
val dimens = Dimens()
inner class Modifiers {
var bar = Modifier
.fillMaxWidth()
.height(100.dp)
.padding(20.dp)
.border(
width = Dimens().borderWidth,
color = PastelYellow,
shape = RoundedCornerShape(Dimens().roundedCorner)
)
}
val modifiers = Modifiers()
}
val global = Global()
var settings = SettingsData()
@@ -1,294 +1,130 @@
package com.odweta.solon package com.odweta.solon
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.util.Log
import android.widget.Toast import androidx.compose.ui.graphics.Color
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.json.JSONObject import org.json.JSONObject
import kotlin.math.cos
private var week: List<Day> = mutableListOf() fun launchStart(ctx: Context) {
private var toastShowing: Boolean = false global.launched = false
private var toastShown: Boolean = false if (Account.haveSaved()) {
private var toast2Shown: Boolean = false if (Util.internetAvail(ctx)) {
global.currentScreen = Screen.Splash
remoteLoad(ctx)
} else {
localLoad(ctx)
}
} else {
global.noSignInBackHandle = true
global.currentScreen = Screen.SignIn
}
}
private lateinit var coscope_: CoroutineScope fun remoteLoad(ctx: Context) {
if (!global.isAPISetUp) {
CoroutineScope(Dispatchers.IO)
.launch {
setupAPI()
global.timetableJSON = networkRequestFactory.perform(NetworkRequestType.Timetable)
global.marksJSON = networkRequestFactory.perform(NetworkRequestType.Marks)
private fun setOnInternetAvailListener(ctx: Context, coscope: CoroutineScope) { launchContinue(
coscope.launch { ctx,
global.timetableJSON,
global.marksJSON
)
}
} else {
CoroutineScope(Dispatchers.IO).launch {
launchContinue(
ctx,
networkRequestFactory.perform(NetworkRequestType.Timetable),
networkRequestFactory.perform(NetworkRequestType.Marks)
)
}
}
}
fun localLoad(ctx: Context) {
global.userData = JSONObject(Util.fileToString(global.filePaths.userData))
global.studentId = Parsing.getStudentId()
global.studentClass = Parsing.getStudentClass()
global.apiEndPoints.marks = "${global.apiEndPoints.apiBase}/v1/students/${global.studentId}/marks/list"
val localTimetable = JSONObject(Util.fileToString(global.filePaths.timetable))
val localMarks = JSONObject(Util.fileToString(global.filePaths.marks))
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
global.newMarks = Parsing.parseNewMarks(marks)
// load the settings
val settingsData = Util.fileToString(global.filePaths.settings).toSettingsData()
global.settings = settingsData
// load the lecture notes
Util.csvToLectureNotes()
// listen for internet availability in case of disconnection
if (!global.internetAwaitListenerRunning)
setOnInternetAvailListener(ctx)
if (!global.launched) {
// show the default (main) screen
global.currentScreen = Screen.Main
// inside show the timetable
global.currentScreenElement = ScreenElement.Timetable
}
global.launched = true
global.refreshed = true
refreshButtonContentColor.value = Color.White
// show the full name of the user in the toolbar as a status
if (Util.internetAvail(ctx)) {
statusFactory.show(StatusType.NameRole)
} else {
statusFactory.show(StatusType.NoInternet)
}
}
private fun setOnInternetAvailListener(ctx: Context) {
global.internetAwaitListenerRunning = true
var internetAvail = Util.internetAvail(ctx)
CoroutineScope(Dispatchers.IO).launch {
while (true) { while (true) {
if (Util.internetAvail(ctx) && !internetAvail) { if (Util.internetAvail(ctx) && !internetAvail) {
internetAvail = Util.internetAvail(ctx) internetAvail = Util.internetAvail(ctx)
coscope.launch { CoroutineScope(Dispatchers.IO).launch {
fetchNewData(ctx, coscope) remoteLoad(ctx)
} }
} else if (!internetAvail) { } else if (!internetAvail) {
internetAvail = Util.internetAvail(ctx) internetAvail = Util.internetAvail(ctx)
toolbarText.value = "Bez připojení k internetu." showStatus("Bez připojení k internetu.")
} else { } else {
internetAvail = Util.internetAvail(ctx) internetAvail = Util.internetAvail(ctx)
} }
delay(1000) // Check for internet availability every second delay(1000) // Check for internet availability every second
} }
} }
} }
private fun fetchNewData(ctx: Context, coscope: CoroutineScope) {
coscope.launch {
done = false
// variables for the loading animation
val loadingArray: Array<String> = arrayOf(
"Načítání nových dat",
"Načítání nových dat.",
"Načítání nových dat..",
"Načítání nových dat..."
)
val delayTime: Long = 300 // delay in ms
// show a loading animation
while (!done && Util.internetAvail(ctx)) {
for (text in loadingArray) {
toolbarText.value = text
delay(delayTime)
}
for (text in loadingArray.reversedArray().slice(1..<loadingArray.size)) {
toolbarText.value = text
delay(delayTime)
}
delay(delayTime)
}
// determine the role of the student
val role = if (Parsing.userIsParent()) {
" Rodič"
} else {
" Student"
}
// display the full name of the user in the toolbar
toolbarText.value =
"${
JSONObject(
Util.fileToString(
userDataFilePath
)
).getString("fullName")}$role"
}
coscope.launch {
launchPhase1(ctx)
}
}
fun launchPhase0(ctx: Context, coscope: CoroutineScope) {
// start a sign in activity
// gather input and put it into global state (if login is needed)
// otherwise show the splash screen and load the app
/* check if there is a saved user account
*
* if yes, use that
* otherwise show the login activity
*/
coscope_ = coscope
fileDir = ctx.filesDir
width = Util.getDisplayWidth(ctx)
height = Util.getDisplayHeight(ctx)
val accSaved: Boolean = Account.haveSaved()
if (!accSaved) {
// show the sign in screen
screenToShow.value = Screen.SignIn
} else {
// show the splash/loading screen
screenToShow.value = Screen.Splash
coscope.launch {
// launch!
launchPhase1(ctx)
}
}
}
private suspend fun launchPhase1(ctx: Context) {
val toast: Toast = Toast.makeText(
ctx,
"Nelze načíst data, zkuste se připojit k internetu.",
Toast.LENGTH_LONG
)
val toast2: Toast = Toast.makeText(
ctx,
"Připojeno k internetu.",
Toast.LENGTH_SHORT
)
// check if the file is available offline,
// if yes, load the data and don't connect to the internet
internetAvail = Util.internetAvail(ctx)
timeTableAvailOffline =
Util.fileExists(timeTableFilePath) && !Util.fileIsEmpty(timeTableFilePath)
marksAvailOffline =
Util.fileExists(marksFilePath) && !Util.fileIsEmpty(marksFilePath)
if (toastShown && internetAvail && !toast2Shown) {
toast2Shown = true
toast2.show()
}
if (!timeTableAvailOffline || internetAvail) {
// set up the API (required for later uses of it)
Network.setupAPI()
if (internetAvail) {
// cancel a toast, if any
if (toastShowing && !toastShown) {
toast.cancel()
}
// get the JSON data
val timeTableJSON = Network.getTimeTableJSON()
// try to save them into a file for later use
timeTableAvailOffline =
Util.saveStringToFile(timeTableFilePath, timeTableJSON)
// parse the data
week = Parsing.parseTimeTableJSON(timeTableJSON)
// fetch and parse marks
marksJSON = Network.getMarksJSON()
marksAvailOffline =
Util.saveStringToFile(marksFilePath, marksJSON)
marks = Parsing.parseMarksJSON(marksJSON)
// continue to launch phase 2
done = true
launchPhase2(ctx)
} else {
if (timeTableAvailOffline) {
// cancel a toast, if any
if (toastShowing && !toastShown) {
toast.cancel()
}
// load the offline data
val timeTableJSON = Util.fileToString(timeTableFilePath)
// parse the data
if (timeTableJSON != "") {
// cancel a toast, if any
if (toastShowing && !toastShown) {
toast.cancel()
}
// parse the data
week = Parsing.parseTimeTableJSON(timeTableJSON)
// continue launching
done = true
launchPhase2(ctx)
} else {
// make a toast or something
toast.show()
// try to reconnect to the internet
launchPhase1(ctx)
}
} else {
// make a toast or something
if (!toastShown) {
toast.show()
toastShowing = true
toastShown = true
}
// try to reconnect to the internet
launchPhase1(ctx)
}
}
} else {
// load offline
// cancel a toast, if any
if (toastShowing && !toastShown) {
toast.cancel()
}
// load the offline data
val timeTableJSON = Util.fileToString(timeTableFilePath)
marksJSON = Util.fileToString(marksFilePath)
if (marksJSON == "") {
marksJSON =
"""{"message": "Zatím žádné známky, zkuste aktualizovat data."}"""
}
// parse the data
if (timeTableJSON != "") {
// cancel a toast, if any
if (toastShowing && !toastShown) {
toast.cancel()
}
// parse the data
week = Parsing.parseTimeTableJSON(timeTableJSON)
marks = Parsing.parseMarksJSON(marksJSON)
// continue launching
done = true
launchPhase2(ctx)
} else {
// make a toast or something
if (!toastShown) {
toast.show()
toastShowing = true
toastShown = true
}
// try to reconnect to the internet
launchPhase1(ctx)
}
}
}
private fun launchPhase2(ctx: Context) {
days = week
for ((i, day) in week.withIndex()) {
dayMap[i] = day
dayList.add(day)
}
loaded = true
coscope_.launch {
setOnInternetAvailListener(ctx, coscope_)
}
val role = if (Parsing.userIsParent()) {
" Rodič"
} else {
" Student"
}
toolbarText.value = "${
JSONObject(
Util.fileToString(
userDataFilePath
)
).getString("fullName")}$role"
// set the main content view
screenToShow.value = Screen.Main
}
@@ -1,7 +1,6 @@
package com.odweta.solon package com.odweta.solon
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -10,33 +9,25 @@ 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.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
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.platform.LocalContext import androidx.compose.ui.graphics.toArgb
import com.odweta.solon.ui.theme.SolonTheme import com.odweta.solon.ui.theme.SolonTheme
import kotlin.math.sign
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
makeStatusBarBlack(window) window.statusBarColor = Color.Black.toArgb()
global.fileDir = applicationContext.filesDir
launchStart(applicationContext)
setContent { setContent {
if (!loaded) {
launchPhase0(LocalContext.current, rememberCoroutineScope())
}
when (signedInAndReadyToLaunch.value) {
true -> launchPhase0(LocalContext.current, rememberCoroutineScope())
else -> {}
}
SolonTheme { SolonTheme {
when (screenToShow.value) { when (global.currentScreen) {
Screen.Main -> MainScreen() Screen.Main -> if (global.launched) MainScreen()
Screen.Splash -> SplashScreen() Screen.Splash -> SplashScreen()
Screen.SignIn -> SignInScreen() Screen.SignIn -> SignInScreen()
} }
@@ -45,14 +36,14 @@ class MainActivity : ComponentActivity() {
} }
@Composable @Composable
fun MainScreen(modifier: Modifier = Modifier) { fun MainScreen() {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(Color.Black) .background(Color.Black)
) { ) {
Box( Box(
modifier = barModifier, modifier = global.modifiers.bar,
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
ToolbarScreen() ToolbarScreen()
@@ -63,16 +54,17 @@ class MainActivity : ComponentActivity() {
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f)
) { ) {
when (isShowing.value) { when (global.currentScreenElement) {
IsShowing.Timetable -> TimetableScreen() ScreenElement.Timetable -> TimetableScreen()
IsShowing.Marks -> MarksScreen() ScreenElement.Marks -> MarksScreen()
IsShowing.Menu -> MenuScreen() ScreenElement.Menu -> MenuScreen()
IsShowing.About -> AboutScreen() ScreenElement.About -> AboutScreen()
ScreenElement.Settings -> SettingsScreen()
} }
} }
Box( Box(
modifier = barModifier, modifier = global.modifiers.bar,
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
NavbarScreen() NavbarScreen()
+383 -178
View File
@@ -1,5 +1,6 @@
package com.odweta.solon package com.odweta.solon
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
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -13,75 +14,191 @@ 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.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape 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.ArrowBackIosNew import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material3.Button import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.ButtonDefaults
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.MutableState import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
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.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.platform.LocalContext
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.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
import com.odweta.solon.ui.theme.PastelOrangeDark
import com.odweta.solon.ui.theme.PastelOrangeDarkTransparent
import com.odweta.solon.ui.theme.PastelOrangeTransparent import com.odweta.solon.ui.theme.PastelOrangeTransparent
import com.odweta.solon.ui.theme.PastelRed 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
enum class MarksShow { var showNewMarksDialog by mutableStateOf(false)
enum class MarksDepthLevel {
All, All,
Subject, Subject,
Detail Detail
} }
var marksShow: MutableState<MarksShow> = mutableStateOf(MarksShow.All)
private var subjectToShow: String = ""
private var markToShow: Mark = Mark()
@Composable @Composable
fun MarksScreen() { fun MarksScreen() {
when (marksShow.value) { when (global.marksDepthLevel) {
MarksShow.All -> MarksAll() MarksDepthLevel.All -> MarksAll()
MarksShow.Subject -> MarksSubject() MarksDepthLevel.Subject -> MarksSubject()
MarksShow.Detail -> MarksDetail() MarksDepthLevel.Detail -> MarksDetail()
}
if (showNewMarksDialog) {
val scrollStateNewMarks = rememberScrollState()
Dialog(
onDismissRequest = { showNewMarksDialog = false },
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)
) {
Column(
Modifier.verticalScroll(scrollStateNewMarks).padding(vertical = 10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
"Nové známky",
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 30.sp,
textAlign = TextAlign.Center,
modifier = Modifier.background(
color = Color.Black,
shape = RoundedCornerShape(global.dimens.roundedCorner)
).border(
shape = RoundedCornerShape(global.dimens.roundedCorner),
color = PastelYellow,
width = global.dimens.borderWidth
).padding(10.dp)
)
Spacer(Modifier.height(10.dp))
for ((k, v) in global.newMarks) {
if (v.size > 0) {
Column(
modifier = Modifier
.padding(10.dp)
.border(
shape = RoundedCornerShape(global.dimens.roundedCorner),
color = PastelYellow,
width = global.dimens.borderWidth
)
.background(
Color.Black,
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
k,
color = Color.White,
modifier = Modifier.padding(10.dp),
fontWeight = FontWeight.Bold,
fontSize = 30.sp
)
for (i in v) {
Row(
Modifier.padding(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
i.theme,
color = Color.White,
modifier = Modifier.padding(end = 5.dp, top = 5.dp)
.weight(1f)
)
Column(
horizontalAlignment = Alignment.End
) {
Text(
i.grade + " * " + i.weight,
color = Color.White,
modifier = Modifier
.background(
color = getAvgColor(i.grade, true),
shape = RoundedCornerShape(global.dimens.roundedCorner)
).padding(5.dp),
style = TextStyle(
shadow = Shadow(
color = Color.Black,
offset = Offset(4f, 4f),
blurRadius = 8f
)
)
)
Text(
i.editDate.format(
DateTimeFormatter.ofPattern("dd. MM. yyyy")
),
color = Color.White
)
}
}
HorizontalLine()
}
}
}
}
}
}
} }
} }
private fun getAvgColor(avg: String, isBorder: Boolean = false): Color { private fun getAvgColor(avg: String, isBorder: Boolean = false): Color {
val aux = avg.replace(",", ".").toFloat().toInt() val aux = avg.replace(",", ".").toFloat()
return if (aux in 1..<2) { return if (aux in 1f..<1.5f) {
if (isBorder) { if (isBorder) {
PastelGreen PastelGreen
} else { } else {
PastelGreenTransparent PastelGreenTransparent
} }
} else if (aux in 2..<3) { } else if (aux in 1.5f..<2.5f) {
if (isBorder) { if (isBorder) {
PastelYellow PastelYellow
} else { } else {
PastelYellowTransparent PastelYellowTransparent
} }
} else if (aux in 3..<4) { } else if (aux in 2.5f..<3.5f) {
if (isBorder) { if (isBorder) {
PastelOrange PastelOrange
} else { } else {
PastelOrangeTransparent PastelOrangeTransparent
} }
} else if (aux in 3.5f..<4.5f) {
if (isBorder) {
PastelOrangeDark
} else {
PastelOrangeDarkTransparent
}
} else { } else {
if (isBorder) { if (isBorder) {
PastelRed PastelRed
@@ -103,7 +220,71 @@ fun MarksAll() {
.padding(horizontal = 5.dp) .padding(horizontal = 5.dp)
.verticalScroll(scrollState) .verticalScroll(scrollState)
) { ) {
for ((subject, markList) in marks) { for ((subject, markList) in global.marks) {
val amoledModifier = if (settings.useAmoledMode.checked.value) {
Modifier
.width(150.dp)
.fillMaxHeight()
.border(
width = global.dimens.borderWidth,
color = getAvgColor(
Util.avg(markList),
true
), // decide on the color (transparent)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
} else {
Modifier
.width(150.dp)
.fillMaxHeight()
.background(
color = getAvgColor(Util.avg(markList)), // decide on the color (not transparent)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
}
val amoledModifierBackground = if (settings.useAmoledMode.checked.value) {
Modifier
.fillMaxWidth()
.height(70.dp)
.border(
width = global.dimens.borderWidth,
color = getAvgColor(
Util.avg(markList),
true
), // decide on the color (transparent)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.clickable {
global.marksDepthLevel = MarksDepthLevel.Subject
global.subjectToShow = subject
}
} else {
Modifier
.fillMaxWidth()
.height(70.dp)
.background(
color = getAvgColor(Util.avg(markList)), // decide on the color (transparent)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.clickable {
global.marksDepthLevel = MarksDepthLevel.Subject
global.subjectToShow = subject
}
}
val amoledSpacerModifier = if (settings.useAmoledMode.checked.value) {
Modifier
.width(10.dp)
.fillMaxHeight()
.background(getAvgColor(Util.avg(markList)))
} else {
Modifier
.width(10.dp)
.fillMaxHeight()
.background(Color.Black)
}
var marksString = "" var marksString = ""
for (m in 0 until markList.size-1) { for (m in 0 until markList.size-1) {
if (markList[m].grade in "0123456789") { if (markList[m].grade in "0123456789") {
@@ -112,65 +293,57 @@ fun MarksAll() {
} }
marksString += markList[markList.size-1].grade marksString += markList[markList.size-1].grade
if (marks.keys.indexOf(subject) > 0) { if (global.marks.keys.indexOf(subject) > 0) {
Box(modifier = Modifier.height(10.dp)) Box(modifier = Modifier.height(10.dp))
} }
Row( Row(
modifier = Modifier modifier = amoledModifierBackground
.fillMaxWidth()
.height(100.dp)
.background(
color = getAvgColor(Util.avg(markList)), // decide on the color (transparent)
shape = RoundedCornerShape(roundedCornerDimen)
)
.clickable {
marksShow.value = MarksShow.Subject
subjectToShow = subject
}
) { ) {
Row( Row(
modifier = Modifier modifier = amoledModifier,
.width(210.dp)
.fillMaxHeight()
.background(
color = getAvgColor(Util.avg(markList)), // decide on the color (not transparent)
shape = RoundedCornerShape(roundedCornerDimen)
),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
Text( Text(
text = subject.uppercase(), text = subject.uppercase(),
fontSize = 35.sp, fontSize = 30.sp,
color = Color.White, color = Color.White,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier modifier = Modifier
.width(100.dp) .width(70.dp)
) )
Box( Box(
modifier = Modifier modifier = amoledSpacerModifier
.width(10.dp)
.fillMaxHeight()
.background(Color.Black)
) )
val currAvg = Util.avg(markList).replace(".", ",")
var textToShow = currAvg
if (currAvg.length == 3 && currAvg[2] == '0') {
textToShow = currAvg[0].toString()
}
Text( Text(
text = Util.avg(markList).replace(".", ","), text = textToShow,
fontSize = 30.sp, fontSize = 28.sp,
color = Color.White, color = Color.White,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier modifier = Modifier
.width(100.dp) .width(70.dp)
) )
} }
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .fillMaxSize()
.fillMaxHeight(), .weight(1f),
contentAlignment = Alignment.TopStart
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 10.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
@@ -182,10 +355,35 @@ fun MarksAll() {
} }
} }
} }
Spacer(Modifier.height(80.dp))
}
Box(Modifier.fillMaxSize()) {
FloatingActionButton(
onClick = { showNewMarksDialog = true },
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(10.dp)
.background(Color.Black, shape = RoundedCornerShape(global.dimens.roundedCorner)),
containerColor = Color.Black,
contentColor = PastelYellow
) {
Icon(
modifier = Modifier.padding(start = 4.dp, top = 4.dp),
imageVector = Icons.Default.AutoAwesome,
contentDescription = "New marks in subject",
tint = PastelYellow // You can customize the icon color
)
}
}
} }
@Composable @Composable
fun MarksSubject() { fun MarksSubject() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler { global.marksDepthLevel = MarksDepthLevel.All }
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Column( Column(
@@ -195,35 +393,56 @@ fun MarksSubject() {
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 5.dp) .padding(horizontal = 5.dp)
) { ) {
Button( Row(
onClick = { modifier = Modifier.fillMaxWidth()
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(100.dp)
.height(100.dp)
) { ) {
Icon( backButtonFactory.get(BackButtonType.MarksAll)()
imageVector = Icons.Default.ArrowBackIosNew, // Replace with your desired icon
contentDescription = "Back icon", Box(modifier = Modifier.weight(1f))
modifier = Modifier.size(35.dp) // Adjust size if needed
Box(
modifier = Modifier
.width(70.dp)
.height(70.dp)
.border(
width = global.dimens.borderWidth,
color = getAvgColor(
Util.avg(global.marks[global.subjectToShow]!!),
true
),
shape = RoundedCornerShape(global.dimens.roundedCorner)
) )
.background(
color = if (settings.useAmoledMode.checked.value) {
Color.Black
} else {
getAvgColor(
Util.avg(global.marks[global.subjectToShow]!!)
)
},
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.padding(vertical = 15.dp),
contentAlignment = Alignment.Center
) {
val currAvg = Util.avg(global.marks[global.subjectToShow]!!).replace(".", ",")
var textToShow = currAvg
if (currAvg.length == 3 && currAvg[2] == '0') {
textToShow = currAvg[0].toString()
}
Text(
text = textToShow,
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 25.sp
) // average
}
} }
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
Box(modifier = Modifier.height(100.dp)) {
Text( Text(
text = subjectNameMap[subjectToShow]!!, text = global.subjectNameMap[global.subjectToShow]!!,
color = Color.White, color = Color.White,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontSize = 30.sp, fontSize = 30.sp,
@@ -232,67 +451,52 @@ fun MarksSubject() {
.border( .border(
width = 5.dp, width = 5.dp,
color = PastelYellow, color = PastelYellow,
shape = RoundedCornerShape(roundedCornerDimen) shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.background(PastelYellowTransparent)
.padding(vertical = 20.dp)
) // subject
}
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(
text = Util.avg(marks[subjectToShow]!!).replace(".", ","),
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 30.sp,
modifier = Modifier
.width(100.dp)
.height(100.dp)
.border(
width = 5.dp,
color = getAvgColor(
Util.avg(marks[subjectToShow]!!),
true
),
shape = RoundedCornerShape(roundedCornerDimen)
) )
.background( .background(
getAvgColor( color = if (settings.useAmoledMode.checked.value) {
Util.avg(marks[subjectToShow]!!) Color.Black
} else {
PastelYellowTransparent
},
shape = RoundedCornerShape(global.dimens.roundedCorner)
) )
) .padding(vertical = 20.dp, horizontal = 10.dp)
.padding(vertical = 32.5.dp) ) // subject
) // average
}
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
Column(modifier = Modifier.verticalScroll(scrollState)) { Column(modifier = Modifier.verticalScroll(scrollState)) {
for ((i, mark) in marks[subjectToShow]!!.withIndex()) { for ((i, mark) in global.marks[global.subjectToShow]!!.withIndex()) {
if (i > 0) { if (i > 0) {
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
} }
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.TopStart
) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 5.dp) .padding(horizontal = 5.dp)
.background( .background(
color = getAvgColor(mark.grade), color = if (settings.useAmoledMode.checked.value) {
shape = RoundedCornerShape(roundedCornerDimen) Color.Black
} else {
getAvgColor(mark.grade)
},
shape = RoundedCornerShape(global.dimens.roundedCorner)
) )
.padding(vertical = 20.dp) .padding(vertical = 10.dp)
.clickable { .clickable {
markToShow = mark global.markToShow = mark
marksShow.value = MarksShow.Detail global.marksDepthLevel = MarksDepthLevel.Detail
} }
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.width(100.dp) .width(70.dp)
.align(Alignment.CenterVertically), .align(Alignment.CenterVertically),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@@ -312,7 +516,7 @@ fun MarksSubject() {
Text( Text(
text = mark.theme, text = mark.theme,
color = Color.White, color = Color.White,
fontSize = 23.sp fontSize = 20.sp
) // theme ) // theme
} }
} }
@@ -320,9 +524,13 @@ fun MarksSubject() {
} }
} }
} }
}
@Composable @Composable
fun MarksDetail() { fun MarksDetail() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler { global.marksDepthLevel = MarksDepthLevel.Subject }
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Column( Column(
@@ -333,36 +541,51 @@ fun MarksDetail() {
.padding(horizontal = 5.dp) .padding(horizontal = 5.dp)
.verticalScroll(scrollState) .verticalScroll(scrollState)
) { ) {
Button( Row(
onClick = { modifier = Modifier.fillMaxWidth()
marksShow.value = MarksShow.Subject
},
colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
containerColor = Color.Black
),
modifier = Modifier
.border(
width = 5.dp,
shape = RoundedCornerShape(roundedCornerDimen),
color = PastelYellow
)
.width(100.dp)
.height(100.dp)
) { ) {
Icon( backButtonFactory.get(BackButtonType.MarksSubject)()
imageVector = Icons.Default.ArrowBackIosNew, // Replace with your desired icon
contentDescription = "Back icon", Box(modifier = Modifier.weight(1f))
modifier = Modifier.size(35.dp) // Adjust size if needed
Box(
modifier = Modifier
.width(70.dp)
.height(70.dp)
.border(
width = global.dimens.borderWidth,
color = getAvgColor(
Util.avg(mutableListOf(global.markToShow)),
true
),
shape = RoundedCornerShape(global.dimens.roundedCorner)
) )
.background(
if (settings.useAmoledMode.checked.value) {
Color.Black
} else {
getAvgColor(
Util.avg(mutableListOf(global.markToShow))
)
}
)
.padding(vertical = 15.dp),
contentAlignment = Alignment.Center
) {
Text(
text = global.markToShow.grade,
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 25.sp
) // average
}
} }
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
Row { Row {
Box(modifier = Modifier.height(100.dp)) {
Text( Text(
text = markToShow.theme, text = global.markToShow.theme,
color = Color.White, color = Color.White,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
fontSize = 30.sp, fontSize = 30.sp,
@@ -371,44 +594,18 @@ fun MarksDetail() {
.border( .border(
width = 5.dp, width = 5.dp,
color = PastelYellow, color = PastelYellow,
shape = RoundedCornerShape(roundedCornerDimen) shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.background(PastelYellowTransparent)
.padding(vertical = 20.dp)
) // description
}
} // description
Row {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(
text = markToShow.grade,
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 35.sp,
modifier = Modifier
.width(100.dp)
.height(100.dp)
.border(
width = 5.dp,
color = getAvgColor(
Util.avg(mutableListOf(markToShow)),
true
),
shape = RoundedCornerShape(roundedCornerDimen)
) )
.background( .background(
getAvgColor( if (settings.useAmoledMode.checked.value) {
Util.avg(mutableListOf(markToShow)) Color.Black
) } else {
) PastelYellowTransparent
.padding(vertical = 32.5.dp)
) // average
} }
} // grade )
.padding(vertical = 20.dp)
) // description
} // description
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
@@ -417,8 +614,12 @@ fun MarksDetail() {
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 5.dp) .padding(horizontal = 5.dp)
.background( .background(
color = PastelYellowTransparent, color = if (settings.useAmoledMode.checked.value) {
shape = RoundedCornerShape(roundedCornerDimen) Color.Black
} else {
PastelYellowTransparent
},
shape = RoundedCornerShape(global.dimens.roundedCorner)
) )
.padding(vertical = 20.dp, horizontal = 10.dp) .padding(vertical = 20.dp, horizontal = 10.dp)
) { ) {
@@ -431,7 +632,7 @@ fun MarksDetail() {
Text( Text(
text = "Váha:", text = "Váha:",
color = Color.White, color = Color.White,
fontSize = 30.sp fontSize = 20.sp
) )
} }
@@ -442,9 +643,9 @@ fun MarksDetail() {
contentAlignment = Alignment.CenterEnd contentAlignment = Alignment.CenterEnd
) { ) {
Text( Text(
text = markToShow.weight.replace(".", ","), text = global.markToShow.weight.replace(".", ","),
color = Color.White, color = Color.White,
fontSize = 23.sp fontSize = 20.sp
) )
} }
} // weight } // weight
@@ -456,8 +657,12 @@ fun MarksDetail() {
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 5.dp) .padding(horizontal = 5.dp)
.background( .background(
color = PastelYellowTransparent, color = if (settings.useAmoledMode.checked.value) {
shape = RoundedCornerShape(roundedCornerDimen) Color.Black
} else {
PastelYellowTransparent
},
shape = RoundedCornerShape(global.dimens.roundedCorner)
) )
.padding(vertical = 20.dp, horizontal = 10.dp) .padding(vertical = 20.dp, horizontal = 10.dp)
) { ) {
@@ -470,7 +675,7 @@ fun MarksDetail() {
Text( Text(
text = "Datum:", text = "Datum:",
color = Color.White, color = Color.White,
fontSize = 30.sp fontSize = 20.sp
) )
} }
@@ -480,7 +685,7 @@ fun MarksDetail() {
.weight(1f), .weight(1f),
contentAlignment = Alignment.CenterEnd contentAlignment = Alignment.CenterEnd
) { ) {
val rawDate: String = markToShow.date.split("T")[0] val rawDate: String = global.markToShow.date.split("T")[0]
val year = rawDate.split("-")[0] val year = rawDate.split("-")[0]
val month = rawDate.split("-")[1] val month = rawDate.split("-")[1]
val day = rawDate.split("-")[2] val day = rawDate.split("-")[2]
@@ -489,7 +694,7 @@ fun MarksDetail() {
Text( Text(
text = date, text = date,
color = Color.White, color = Color.White,
fontSize = 23.sp fontSize = 20.sp
) )
} }
} // date } // date
@@ -27,7 +27,7 @@ import com.odweta.solon.ui.theme.PastelYellow
import kotlin.system.exitProcess import kotlin.system.exitProcess
@Composable @Composable
fun MenuScreen(modifier: Modifier = Modifier) { fun MenuScreen() {
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
Column( Column(
@@ -36,29 +36,20 @@ fun MenuScreen(modifier: Modifier = Modifier) {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
// Button to trigger the dialog MenuButton({ showDialog = true }, "Odhlásit se")
Button(
onClick = { showDialog = true },
colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
containerColor = Color.Black
),
modifier = Modifier
.border(
width = 5.dp,
shape = RoundedCornerShape(roundedCornerDimen),
color = PastelYellow
)
.width(230.dp)
.height(80.dp)
) {
Text(
text = "Odhlásit se",
fontSize = 23.sp
)
}
Spacer(modifier = Modifier.height(100.dp)) Spacer(modifier = Modifier.height(80.dp))
MenuButton({ global.currentScreen = Screen.SignIn }, "Změnit přihlášení")
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 // Dialog implementation
if (showDialog) { if (showDialog) {
@@ -72,16 +63,20 @@ fun MenuScreen(modifier: Modifier = Modifier) {
}, },
containerColor = Color.Black, containerColor = Color.Black,
modifier = Modifier modifier = Modifier
.border(width = 5.dp, color = PastelYellow, shape = RoundedCornerShape( .border(
roundedCornerDimen)), // Yellow border width = global.dimens.borderWidth,
color = PastelYellow,
shape = RoundedCornerShape(global.dimens.roundedCorner)), // Yellow border
confirmButton = { confirmButton = {
Button( Button(
onClick = { onClick = {
showDialog = false showDialog = false
Util.deleteFile(userDataFilePath) Util.deleteFile(global.filePaths.userData)
Util.deleteFile(timeTableFilePath) Util.deleteFile(global.filePaths.timetable)
Util.deleteFile(marksFilePath) Util.deleteFile(global.filePaths.marks)
Util.deleteFile(accountDataFilePath) Util.deleteFile(global.filePaths.accountData)
Util.deleteFile(global.filePaths.lectureNotes)
// do not delete settings
exitProcess(0) exitProcess(0)
}, },
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
@@ -89,8 +84,10 @@ fun MenuScreen(modifier: Modifier = Modifier) {
contentColor = Color.White // Yellow text for the button contentColor = Color.White // Yellow text for the button
), ),
modifier = Modifier modifier = Modifier
.border(width = 5.dp, color = PastelYellow, shape = RoundedCornerShape( .border(
roundedCornerDimen)) // Yellow border width = global.dimens.borderWidth,
color = PastelYellow,
shape = RoundedCornerShape(global.dimens.roundedCorner)) // Yellow border
) { ) {
Text("Ano") Text("Ano")
} }
@@ -106,59 +103,38 @@ fun MenuScreen(modifier: Modifier = Modifier) {
// Yellow text for the button // Yellow text for the button
), ),
modifier = Modifier modifier = Modifier
.border(width = 5.dp, color = PastelYellow, shape = RoundedCornerShape( .border(
roundedCornerDimen)) // Yellow border width = global.dimens.borderWidth,
color = PastelYellow,
shape = RoundedCornerShape(global.dimens.roundedCorner)) // Yellow border
) { ) {
Text("Ne") Text("Ne")
} }
} }
) // TODO: THEME DIALOG )
}
} }
Button(onClick = { @Composable
screenToShow.value = Screen.SignIn fun MenuButton(onClick: () -> Unit, text: String) {
}, Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
contentColor = Color.White, contentColor = Color.White,
containerColor = Color.Black containerColor = Color.Black
), ),
modifier = Modifier modifier = Modifier
.border( .border(
width = 5.dp, width = global.dimens.borderWidth,
shape = RoundedCornerShape(roundedCornerDimen), shape = RoundedCornerShape(global.dimens.roundedCorner),
color = PastelYellow color = PastelYellow
) )
.width(230.dp) .width(230.dp)
.height(80.dp) .height(80.dp)
) { ) {
Text( Text(
text = "Změnit přihlášení", text = text,
fontSize = 23.sp
)
}
Spacer(modifier = Modifier.height(100.dp))
Button(onClick = {
isShowing.value = IsShowing.About
},
colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
containerColor = Color.Black
),
modifier = Modifier
.border(
width = 5.dp,
shape = RoundedCornerShape(roundedCornerDimen),
color = PastelYellow
)
.width(230.dp)
.height(80.dp)
) {
Text(
text = "O aplikaci",
fontSize = 23.sp fontSize = 23.sp
) )
} }
} }
}
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Schedule import androidx.compose.material.icons.filled.Schedule
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
@@ -18,6 +19,7 @@ import androidx.compose.runtime.mutableStateOf
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.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.odweta.solon.ui.theme.PastelYellow import com.odweta.solon.ui.theme.PastelYellow
@@ -25,20 +27,48 @@ import com.odweta.solon.ui.theme.PastelYellow
private var timetableColor: MutableState<Color> = mutableStateOf(PastelYellow) private var timetableColor: MutableState<Color> = mutableStateOf(PastelYellow)
private var marksColor: MutableState<Color> = mutableStateOf(Color.White) private var marksColor: MutableState<Color> = mutableStateOf(Color.White)
private var menuColor: MutableState<Color> = mutableStateOf(Color.White) private var menuColor: MutableState<Color> = mutableStateOf(Color.White)
var refreshButtonContentColor: MutableState<Color> = mutableStateOf(Color.White)
@Composable @Composable
fun NavbarScreen(modifier: Modifier = Modifier) { fun NavbarScreen() {
val ctx = LocalContext.current
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
var spacerModifier = 30.dp
if (settings.showRefreshButton.checked.value) {
spacerModifier = 15.dp
Button(
onClick = {
if (Util.internetAvail(ctx)) {
remoteLoad(ctx)
statusFactory.show(StatusType.Loading)
refreshButtonContentColor.value = PastelYellow
}
},
colors = ButtonDefaults.buttonColors(
contentColor = refreshButtonContentColor.value,
containerColor = Color.Black
)
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = "Refresh Button",
modifier = Modifier.size(25.dp)
)
}
Spacer(modifier = Modifier.width(spacerModifier))
}
Button( Button(
onClick = { onClick = {
isShowing.value = IsShowing.Timetable
timetableColor.value = PastelYellow timetableColor.value = PastelYellow
marksColor.value = Color.White marksColor.value = Color.White
menuColor.value = Color.White menuColor.value = Color.White
global.currentScreenElement = ScreenElement.Timetable
}, },
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
contentColor = timetableColor.value, contentColor = timetableColor.value,
@@ -52,15 +82,15 @@ fun NavbarScreen(modifier: Modifier = Modifier) {
) )
} }
Spacer(modifier = Modifier.width(30.dp)) Spacer(modifier = Modifier.width(spacerModifier))
Button( Button(
onClick = { onClick = {
isShowing.value = IsShowing.Marks
timetableColor.value = Color.White timetableColor.value = Color.White
marksColor.value = PastelYellow marksColor.value = PastelYellow
menuColor.value = Color.White menuColor.value = Color.White
marksShow.value = MarksShow.All global.currentScreenElement = ScreenElement.Marks
global.marksDepthLevel = MarksDepthLevel.All
}, },
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
contentColor = marksColor.value, contentColor = marksColor.value,
@@ -74,14 +104,14 @@ fun NavbarScreen(modifier: Modifier = Modifier) {
) )
} }
Spacer(modifier = Modifier.width(30.dp)) Spacer(modifier = Modifier.width(spacerModifier))
Button( Button(
onClick = { onClick = {
isShowing.value = IsShowing.Menu
timetableColor.value = Color.White timetableColor.value = Color.White
marksColor.value = Color.White marksColor.value = Color.White
menuColor.value = PastelYellow menuColor.value = PastelYellow
global.currentScreenElement = ScreenElement.Menu
}, },
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
contentColor = menuColor.value, contentColor = menuColor.value,
@@ -1,178 +1,106 @@
package com.odweta.solon package com.odweta.solon
import android.util.Log import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.Headers.Companion.headersOf
import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.json.JSONException import okhttp3.Response
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException
class Network { fun setupAPI() {
companion object { global.token = "NETWORK_UNREACHABLE"
suspend fun getToken(): String { val token = networkRequestFactory.perform(NetworkRequestType.Token).getString("token")
return withContext(Dispatchers.IO) { if (""""error": """ !in token) {
val request = Request.Builder() global.token = token
.url(tokenUrl) }
.header("Content-Type", "application/x-www-form-urlencoded") if (global.token == "NETWORK_UNREACHABLE" || """"error": """ in global.token) {
.post( global.userData = JSONObject()
FormBody.Builder() global.studentId = ""
return
}
global.userData = networkRequestFactory.perform(NetworkRequestType.UserData)
Util.saveStringToFile(global.filePaths.userData, global.userData.toString())
global.studentClass = Parsing.getStudentClass()
global.studentId = Parsing.getStudentId()
global.apiEndPoints.marks = "${global.apiEndPoints.apiBase}/v1/students/${global.studentId}/marks/list"
global.isAPISetUp = true
}
enum class NetworkRequestType {
Token,
Timetable,
Marks,
UserData
}
interface NetworkRequestInterface {
fun perform(type: NetworkRequestType): JSONObject
}
object NetworkRequestFactory: NetworkRequestInterface {
private val client: OkHttpClient = OkHttpClient()
private val responseError = JSONObject(mapOf("error" to "error"))
override fun perform(type: NetworkRequestType): JSONObject {
when (type) {
NetworkRequestType.Token -> {
val response = post(global.apiEndPoints.token, headersOf(), FormBody.Builder()
.add("grant_type", "password") .add("grant_type", "password")
.add("username", username) .add("username", global.username)
.add("password", password) .add("password", global.password)
.add("client_id", "test_client") .add("client_id", "test_client")
.add("scope", "openid offline_access profile sol_api") .add("scope", "openid offline_access profile sol_api")
.build() .build()
) )
.build()
try { return if (response.code == 200) {
val resp = client.newCall(request).execute() JSONObject(mapOf("token" to JSONObject(response.body?.string().toString()).getString("access_token")))
val respString = resp.body?.string()
respString.toString()
//val respJson = respString?.let { JSONObject(it) }
//respJson.toString()
} catch (e: JSONException) {
"""{"error": "json"}"""
} catch (e: Exception) {
//e.printStackTrace()
"""{"error": "network"}"""
}
}
}
suspend fun setupAPI() {
return withContext(Dispatchers.IO) {
token = "NETWORK_UNREACHABLE"
token = getToken()
if (""""error": """ !in token) {
token = JSONObject(token).get("access_token").toString()
}
if (token == "NETWORK_UNREACHABLE" || """"error": """ in token) {
userData = ""
studentId = ""
internetAvail = false
return@withContext
}
internetAvail = true
userData = getUserDataJSON()
Util.saveStringToFile(userDataFilePath, userData)
if (!Parsing.userIsParent()) {
studentClass = JSONObject(
Util.fileToString(userDataFilePath)
).getJSONObject("class").get("name").toString()
}
studentId = getStudentId()
//syid = getSYID()
syid = ""
marksUrl = "$apiBaseUrl/v1/students/$studentId/marks/list"//?SemesterId=${syid}"
isAPISetUp = true
}
}
/*private fun getSYID(): String {
val f = Util.fileToString(userDataFilePath)
lateinit var syidRoot: String
lateinit var toAdd: String
if (!Parsing.userIsParent()) {
syidRoot = JSONObject(f).getString("schoolYearId")
toAdd = JSONObject(f).getString("studyYear")
} else { } else {
syidRoot = JSONObject(f) responseError
.getJSONArray("children")
.getJSONObject(0)
.getString("schoolYearId")
toAdd = JSONObject(f)
.getJSONArray("children")
.getJSONObject(0)
.getString("studyYear")
} }
val syidStart = syidRoot[0]
var syidNum = syidRoot.slice(1..<syidRoot.length).toInt()
for (ch in toAdd) {
syidNum += 1
} }
NetworkRequestType.Timetable -> return getAPI(global.apiEndPoints.timetable)
return syidStart.toString() + syidNum.toString() NetworkRequestType.Marks -> return getAPI(global.apiEndPoints.marks)
}*/ NetworkRequestType.UserData -> return getAPI(global.apiEndPoints.userData)
private fun getStudentId(): String {
return if (!Parsing.userIsParent()) {
JSONObject(userData).getString("personID")
} else {
JSONObject(userData).getJSONArray("children")
.getJSONObject(0)
.getString("id")
} }
} }
private suspend fun getUserDataJSON(): String { private fun get(url: String, headers: Headers): Response {
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()
respString
} catch (e: IOException) {
"""{"error": "network"}"""
}
}
}
suspend fun getTimeTableJSON(): String {
return withContext(Dispatchers.IO) {
val userIsParent = Parsing.userIsParent()
var url = timeTableUrl
if (userIsParent) {
url += "?studentId=$studentId"
}
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
.header("Content-Type", "application/x-www-form-urlencoded") .headers(headers)
.header("Authorization", "Bearer $token")
.get() .get()
.build() .build()
try { return client.newCall(request).execute()
val resp = client.newCall(request).execute()
resp.body?.string().toString()
} catch (e: IOException) {
"""{"error": "network"}"""
}
}
} }
suspend fun getMarksJSON(): String { private fun post(url: String, headers: Headers, body: FormBody): Response {
return withContext(Dispatchers.IO) {
val request = Request.Builder() val request = Request.Builder()
.url(marksUrl) .url(url)
.header("Content-Type", "application/x-www-form-urlencoded") .headers(headers)
.header("Authorization", "Bearer $token") .post(body)
.get()
.build() .build()
try { return client.newCall(request).execute()
val resp = client.newCall(request).execute()
resp.body?.string().toString()
} catch (e: IOException) {
"""{"error": "network"}"""
} }
}
private fun getAPI(url: String): JSONObject {
Log.d("debug TOKEN", global.token)
val response = get(url, headersOf(
"Content-Type", "application/x-www-form-urlencoded",
"Authorization", "Bearer ${global.token}"
))
return if (response.code == 200) {
JSONObject(response.body?.string().toString())
} else {
responseError
} }
} }
} }
val networkRequestFactory = NetworkRequestFactory
@@ -1,7 +1,10 @@
package com.odweta.solon package com.odweta.solon
import android.util.Log
import org.json.JSONObject import org.json.JSONObject
import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
class Parsing { class Parsing {
@@ -44,28 +47,26 @@ class Parsing {
} }
fun userIsParent(): Boolean { fun userIsParent(): Boolean {
return if (userData != "") return if (global.userData != JSONObject())
JSONObject(userData).getString("userType") == "parent" global.userData.getString("userType") == "parent"
else false else false
} }
fun parseTimeTableJSON(jsonString: String): List<Day> { fun parseTimetable(json: JSONObject): List<Day> {
val jsonObject = JSONObject(jsonString)
/* /*
* return a List that looks like this: * return a List that looks like this:
* [<name>, <date>, [<subj_name>, <subj_place>, <teacher>, <substitute>, <number>, <start_time>, <end_time>]] * [<name>, <date>, [<subj_name>, <subj_place>, <teacher>, <substitute>, <number>, <start_time>, <end_time>]]
* ["Po", "8.1.", ["D", "207", "Strakova S.", 0, 1, "08:00", "08:45" ]] * ["Po", "8.1.", ["D", "207", "Strakova S.", 0, 1, "08:00", "08:45" ]]
*/ */
val week = mutableListOf<Day>() val timetable = mutableListOf<Day>()
if (jsonObject.has("error")) { if (json.has("error")) {
return week return timetable
} }
// Access the "body" array from the main JSON object // Access the "body" array from the main JSON object
val bodyArray = jsonObject.getJSONArray("days") val bodyArray = json.getJSONArray("days")
// Convert the JSON array to a list of JSON objects // Convert the JSON array to a list of JSON objects
val jsonObjectList = mutableListOf<JSONObject>() val jsonObjectList = mutableListOf<JSONObject>()
@@ -84,8 +85,12 @@ class Parsing {
.split("-") .split("-")
// remove leading zeros // remove leading zeros
while (dayStr[0] == '0') { dayStr = dayStr.slice(1..<dayStr.length) } while (dayStr[0] == '0') {
while (monthStr[0] == '0') { monthStr = monthStr.slice(1..<monthStr.length) } dayStr = dayStr.slice(1..<dayStr.length)
}
while (monthStr[0] == '0') {
monthStr = monthStr.slice(1..<monthStr.length)
}
// format it into a string that is readable // format it into a string that is readable
val date = "$dayStr. $monthStr." val date = "$dayStr. $monthStr."
@@ -126,35 +131,34 @@ class Parsing {
if (type != "SUPLOVANI" && type != "ROZVRH") { if (type != "SUPLOVANI" && type != "ROZVRH") {
when (kind) { when (kind) {
"ZRUSENI_VYUKY_ROZVRH" -> { "ZRUSENI_VYUKY_ROZVRH" -> {
subject.name = subj.get("title").toString() subject.name = subj.getString("title")
subject.place = "" subject.place = ""
subject.teacher = "" subject.teacher = ""
subject.substitute = "2" // 2 means school event subject.substitute = "2" // 2 means school event
val hourspan = subj.getJSONArray("detailHours").length() val hourspan = subj.getJSONArray("detailHours").length()
subject.number = "1 - $hourspan" subject.number = "1 - $hourspan"
subject.start = subj.get("beginTime").toString() subject.start = subj.getString("beginTime")
subject.start = subject.start.slice(0..subject.start.length - 4) subject.start = subject.start.slice(0..subject.start.length - 4)
subject.end = subj.get("endTime").toString() subject.end = subj.getString("endTime")
subject.end = subject.end.slice(0..subject.end.length - 4) subject.end = subject.end.slice(0..subject.end.length - 4)
subjects.add(subject) subjects.add(subject)
} }
"S_NAHRADOU_SKOL_AKCE" -> eventLecture(subject, subj, subjects) "S_NAHRADOU_SKOL_AKCE" -> eventLecture(subject, subj, subjects)
"S_NAHRADOU_VYUKA" -> eventLecture(subject, subj, subjects) "S_NAHRADOU_VYUKA" -> eventLecture(subject, subj, subjects)
} }
} else { } else {
subject.name = subj.getJSONObject("subject").get("abbrev").toString() subject.name = subj.getJSONObject("subject").getString("abbrev")
subject.place = subject.place =
subj.getJSONArray("rooms").getJSONObject(0).get("abbrev").toString() subj.getJSONArray("rooms").getJSONObject(0).getString("abbrev")
subject.teacher = "" + subject.teacher = if (subj.getJSONArray("teachers").length() > 0) {
"${
subj.getJSONArray("teachers").getJSONObject(0).get("name")
} " +
"${
subj.getJSONArray("teachers").getJSONObject(0) subj.getJSONArray("teachers").getJSONObject(0)
.get("surname") .getString("fullName") // TODO: keep in mind that fullName might not be a valid field (name and surname are!)
}" } else {
""
}
subject.substitute = when (type) { subject.substitute = when (type) {
"ROZVRH" -> "0" "ROZVRH" -> "0"
"SUPLOVANI" -> "1" "SUPLOVANI" -> "1"
@@ -269,6 +273,23 @@ class Parsing {
auxSubjects.add(Subject.empty()) auxSubjects.add(Subject.empty())
} }
// prepend free subjects when day does not start with subject number 1
val newSubjects: MutableList<Subject> = mutableListOf()
var n = auxSubjects[0].number.toInt()
while (n > 1) {
newSubjects.add(
Subject.freeSubject(
number = (n - 1).toString(),
start = "00:00",
end = "00:00"
)
)
n -= 1
}
val finalSubjects: MutableList<Subject> = mutableListOf()
finalSubjects.addAll(newSubjects.reversed())
finalSubjects.addAll(auxSubjects)
for ((i, s) in auxSubjects.withIndex()) { for ((i, s) in auxSubjects.withIndex()) {
if (s.free) { if (s.free) {
s.start = auxSubjects[i - 1].end s.start = auxSubjects[i - 1].end
@@ -279,7 +300,7 @@ class Parsing {
val day = Day() val day = Day()
day.name = name day.name = name
day.date = date day.date = date
day.subjects = auxSubjects day.subjects = finalSubjects
// get rid of duplicates // get rid of duplicates
val nextSubjects: MutableList<Subject> = mutableListOf() val nextSubjects: MutableList<Subject> = mutableListOf()
@@ -292,20 +313,21 @@ class Parsing {
prevSubject = s prevSubject = s
} }
nextSubjects[nextSubjects.size-1].start = day.subjects[day.subjects.size-1].start nextSubjects[nextSubjects.size - 1].start =
day.subjects[day.subjects.size - 1].start
nextSubjects[nextSubjects.size - 1].end = day.subjects[day.subjects.size - 1].end nextSubjects[nextSubjects.size - 1].end = day.subjects[day.subjects.size - 1].end
day.subjects = nextSubjects day.subjects = nextSubjects
week.add(day) timetable.add(day)
} }
return week Log.d("debug", "timetable: $timetable")
return timetable
} }
fun parseMarksJSON(jsonString: String): MutableMap<String, MutableList<Mark>> { fun parseMarks(json: JSONObject): MutableMap<String, MutableList<Mark>> {
// no marks // no marks
val json = JSONObject(jsonString)
if (json.has("marks") && json.getJSONArray("marks").length() == 0) { if (json.has("marks") && json.getJSONArray("marks").length() == 0) {
val noMarks = mutableMapOf<String, MutableList<Mark>>() val noMarks = mutableMapOf<String, MutableList<Mark>>()
val list = mutableListOf<Mark>() val list = mutableListOf<Mark>()
@@ -319,9 +341,9 @@ class Parsing {
val marksList = mutableListOf<JSONObject>() val marksList = mutableListOf<JSONObject>()
val marksJSONArray = JSONObject(jsonString).getJSONArray("marks") val marksJSONArray = json.getJSONArray("marks")
val subjectsJSONArray = JSONObject(jsonString).getJSONArray("subjects") val subjectsJSONArray = json.getJSONArray("subjects")
val subjectMap = mutableMapOf<String, String>() val subjectMap = mutableMapOf<String, String>()
for (i in 0..<subjectsJSONArray.length()) { for (i in 0..<subjectsJSONArray.length()) {
@@ -329,7 +351,7 @@ class Parsing {
subjectMap[subj.getString("id")] = subj.getString("abbrev") subjectMap[subj.getString("id")] = subj.getString("abbrev")
// map names to abbrevs // map names to abbrevs
subjectNameMap[subj.getString("abbrev")] = subj.getString("name") global.subjectNameMap[subj.getString("abbrev")] = subj.getString("name")
} }
for (i in 0..<marksJSONArray.length()) { for (i in 0..<marksJSONArray.length()) {
@@ -340,16 +362,6 @@ class Parsing {
marks[subj] = mutableListOf() marks[subj] = mutableListOf()
} }
/*
* class Mark {
* lateinit var grade: String // markText
* lateinit var weight: String // weight
* lateinit var subject: String // subjectId -> parse that
* lateinit var date: String // markDate
* lateinit var theme: String // theme
* }
*/
for (markJSON in marksList) { for (markJSON in marksList) {
val mark = Mark() val mark = Mark()
mark.grade = markJSON.getString("markText") mark.grade = markJSON.getString("markText")
@@ -362,5 +374,80 @@ class Parsing {
return marks return marks
} }
fun parseNewMarks(json: JSONObject): MutableMap<String, MutableList<Mark>> {
// no marks
if (json.has("marks") && json.getJSONArray("marks").length() == 0) {
val noMarks = mutableMapOf<String, MutableList<Mark>>()
val list = mutableListOf<Mark>()
list.add(Mark().none())
noMarks["-"] = list
return noMarks
}
// [subject] = (<editDate>, <mark>)
val marks = mutableMapOf<String, MutableList<Mark>>()
val marksList = mutableListOf<JSONObject>()
val marksJSONArray = json.getJSONArray("marks")
val subjectsJSONArray = json.getJSONArray("subjects")
val subjectMap = mutableMapOf<String, String>()
for (i in 0..<subjectsJSONArray.length()) {
val subj = subjectsJSONArray.getJSONObject(i)
subjectMap[subj.getString("id")] = subj.getString("abbrev")
// map names to abbrevs
global.subjectNameMap[subj.getString("abbrev")] = subj.getString("name")
}
for (i in 0..<marksJSONArray.length()) {
marksList.add(marksJSONArray.getJSONObject(i))
}
for (subj in subjectMap.values) {
marks[subj] = mutableListOf()
}
for (markJSON in marksList) {
val mark = Mark()
mark.grade = markJSON.getString("markText")
mark.weight = markJSON.getString("weight")
mark.subject = subjectMap[markJSON.getString("subjectId")].toString()
mark.date = markJSON.getString("markDate")
mark.theme = markJSON.getString("theme")
mark.editDate = LocalDateTime.parse(markJSON.getString("editDate"), DateTimeFormatter.ISO_LOCAL_DATE_TIME)
if (isTimeBetweenD(mark.editDate, LocalDateTime.now().minusWeeks(1), LocalDateTime.now())) marks[mark.subject]?.add(mark)
}
return marks
}
fun getStudentClass(): String {
return if (!userIsParent()) {
global.userData
.getJSONObject("class")
.getString("name")
} else {
// if the user is a parent, the class will be of their first child
global.userData
.getJSONArray("children")
.getJSONObject(0)
.getString("className")
}
}
fun getStudentId(): String {
return if (!userIsParent()) {
global.userData.getString("personID")
} else {
global.userData.getJSONArray("children")
.getJSONObject(0)
.getString("id")
}
}
} }
} }
@@ -0,0 +1,130 @@
package com.odweta.solon
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchColors
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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.odweta.solon.ui.theme.PastelYellow
@Composable
fun SettingsScreen() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler { global.currentScreenElement = ScreenElement.Menu }
val scrollState = rememberScrollState()
Column(modifier = Modifier
.fillMaxSize()
.background(color = Color.Black)
.verticalScroll(scrollState)
) {
backButtonFactory.get(BackButtonType.Menu)()
Spacer(Modifier.height(10.dp))
SettingsTableRow(settings.useSvobodaMode)
Spacer(Modifier.height(10.dp))
SettingsTableRow(settings.useAmoledMode)
Spacer(Modifier.height(10.dp))
SettingsTableRow(settings.showRefreshButton)
}
}
@Composable
fun SettingsTableRow(setting: Setting) {
Row(
modifier = Modifier
.padding(10.dp)
.background(color = Color.Black, shape = RoundedCornerShape(global.dimens.roundedCorner))
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = setting.name,
textAlign = TextAlign.Start,
color = Color.White,
fontSize = 20.sp
)
Spacer(modifier = Modifier.weight(1f))
Switch(
checked = setting.checked.value,
onCheckedChange = {
setting.checked.value = it
when (setting.id) {
SettingsId.Svoboda -> settings.useSvobodaMode = setting
SettingsId.Amoled -> settings.useAmoledMode = setting
SettingsId.RefreshButton -> settings.showRefreshButton = setting
else -> {}
}
Util.saveStringToFile(global.filePaths.settings, settings.toString())
},
colors = SwitchColors(
uncheckedThumbColor = Color.Black,
uncheckedTrackColor = Color.DarkGray,
uncheckedBorderColor = Color.DarkGray,
checkedThumbColor = Color.Black,
checkedTrackColor = PastelYellow,
checkedBorderColor = PastelYellow,
checkedIconColor = Color.White,
uncheckedIconColor = Color.White,
disabledCheckedThumbColor = Color.Black,
disabledCheckedTrackColor = Color.Black,
disabledCheckedBorderColor = Color.Black,
disabledCheckedIconColor = Color.Black,
disabledUncheckedThumbColor = Color.Black,
disabledUncheckedTrackColor = Color.Black,
disabledUncheckedBorderColor = Color.Black,
disabledUncheckedIconColor = Color.Black
)
)
}
}
fun String.toSettingsData(): SettingsData {
val settings = this.split("\n")
if (settings.size <= 1) {
return SettingsData()
}
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
}
@@ -2,20 +2,18 @@ package com.odweta.solon
import android.content.Context import android.content.Context
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
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.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
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.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.filled.VisibilityOff
@@ -27,8 +25,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.Alignment
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
@@ -40,20 +36,24 @@ import com.odweta.solon.ui.theme.PastelYellow
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import kotlinx.coroutines.Dispatchers
private var buttonText: MutableState<String> = mutableStateOf("Přihlásit se") private var buttonText by mutableStateOf("Přihlásit se")
private fun changeButtonText(s: String) { private fun changeButtonText(s: String) {
buttonText.value = s buttonText = s
} }
fun signInButtonCallback(ctx: Context, coscope: CoroutineScope, buttonText: String, usernameText: String, passwordText: String) { fun signInButtonCallback(ctx: Context, usernameText: String, passwordText: String) {
username = usernameText global.username = usernameText
password = passwordText global.password = passwordText
val acc = Account(username, password, 0) val acc = Account(global.username, global.password, 0)
var checked = false var checked = false
coscope.launch { CoroutineScope(Dispatchers.IO).launch {
while (!checked) { while (!checked) {
delay(300) delay(300)
changeButtonText("Přihlašování") changeButtonText("Přihlašování")
@@ -86,16 +86,15 @@ fun signInButtonCallback(ctx: Context, coscope: CoroutineScope, buttonText: Stri
changeButtonText("Přihlásit se") changeButtonText("Přihlásit se")
} }
coscope.launch { CoroutineScope(Dispatchers.IO).launch {
signedInAndReadyToLaunch.value = false
val valid = Account.isValid(acc) val valid = Account.isValid(acc)
checked = true checked = true
if (valid) { if (valid) {
Account.save(acc) Account.save(acc)
signedInAndReadyToLaunch.value = true global.currentScreen = Screen.Splash
screenToShow.value = Screen.Splash launchStart(ctx)
} else { } else {
Toast.makeText( Toast.makeText(
ctx, ctx,
@@ -106,41 +105,48 @@ fun signInButtonCallback(ctx: Context, coscope: CoroutineScope, buttonText: Stri
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SignInScreen() { fun SignInScreen() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
if (!global.noSignInBackHandle) {
BackHandler { global.currentScreen = Screen.Main }
}
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() 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()
val scrollState = rememberScrollState()
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(Color.Black) .background(Color.Black)
.verticalScroll(scrollState), .imePadding(),
horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center,
verticalArrangement = Arrangement.Center horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
modifier = Modifier
.wrapContentSize(),
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Image( Image(
painter = painterResource(R.drawable.solon_just_yellow), painter = painterResource(R.drawable.solon_just_yellow),
contentDescription = "Solon logo", contentDescription = "Solon logo",
modifier = Modifier.size(230.dp) modifier = Modifier.size(200.dp)
) )
TextField( TextField(
value = usernameText, value = usernameText,
onValueChange = { input -> usernameText = input }, onValueChange = { input -> usernameText = input },
modifier = Modifier modifier = Modifier
.border(width = 5.dp, color = PastelYellow, shape = RoundedCornerShape( .border(
roundedCornerDimen)) width = 5.dp, color = PastelYellow, shape = RoundedCornerShape(
global.dimens.roundedCorner
)
)
.width(230.dp), .width(230.dp),
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
focusedTextColor = Color.White, // White text focusedTextColor = Color.White, // White text
@@ -165,7 +171,7 @@ fun SignInScreen() {
width = 5.dp, width = 5.dp,
color = PastelYellow, color = PastelYellow,
shape = RoundedCornerShape( shape = RoundedCornerShape(
roundedCornerDimen global.dimens.roundedCorner
) )
), ),
colors = TextFieldDefaults.colors( colors = TextFieldDefaults.colors(
@@ -193,7 +199,7 @@ fun SignInScreen() {
Button( Button(
onClick = { onClick = {
signInButtonCallback(ctx, coscope, buttonText.value, usernameText, passwordText) signInButtonCallback(ctx, usernameText, passwordText)
}, },
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
contentColor = Color.White, contentColor = Color.White,
@@ -202,13 +208,13 @@ fun SignInScreen() {
modifier = Modifier modifier = Modifier
.border( .border(
width = 5.dp, width = 5.dp,
shape = RoundedCornerShape(roundedCornerDimen), shape = RoundedCornerShape(global.dimens.roundedCorner),
color = PastelYellow color = PastelYellow
) )
.width(230.dp) .width(230.dp)
) { ) {
Text( Text(
text = buttonText.value, text = buttonText,
fontSize = 23.sp fontSize = 23.sp
) )
} }
@@ -40,12 +40,7 @@ fun SplashScreen() {
AndroidView( AndroidView(
modifier = Modifier.size(200.dp), modifier = Modifier.size(200.dp),
factory = { context: Context -> factory = { context: Context -> ImageView(context) },
ImageView(context).apply {
// Set a placeholder if needed
// setImageResource(R.drawable.placeholder)
}
},
update = { imageView -> update = { imageView ->
Glide.with(imageView.context) Glide.with(imageView.context)
.load(R.drawable.loading) .load(R.drawable.loading)
@@ -1,36 +1,63 @@
package com.odweta.solon package com.odweta.solon
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.text.format.Time import android.util.Log
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
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.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
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.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
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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.focus.focusModifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
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.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.DialogProperties
import androidx.compose.ui.window.PopupProperties
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.PastelRed import com.odweta.solon.ui.theme.PastelRed
@@ -38,20 +65,174 @@ 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.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@OptIn(ExperimentalFoundationApi::class) var showDetailDialog by mutableStateOf(false)
@Composable @Composable
fun TimetableScreen() { fun TimetableScreen() {
val pagerState = rememberPagerState( pageCount = { days.size } ) Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
DayPager()
if (showDetailDialog) DetailDialog()
}
}
@SuppressLint("UnrememberedMutableState")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailDialog() {
Dialog(
onDismissRequest = { showDetailDialog = false }, // Close the dialog on dismiss
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
),
content = {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.background(
color = Color.Black,
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "" +
global.lectureNotes[global.selectedLecture]?.number.toString() +
" " +
global.lectureNotes[global.selectedLecture]?.lectureName.toString(),
color = Color.White,
textAlign = TextAlign.Left,
modifier = Modifier.padding(top = 10.dp)
)
///////////////////////////////////////////////////////////
var isExpanded by remember { mutableStateOf(false) }
Button(
modifier = Modifier.background(
color = Color.Black,
shape = RoundedCornerShape(global.dimens.roundedCorner)
),
onClick = { isExpanded = true },
colors = ButtonColors(
containerColor = Color.Black,
contentColor = Color.White,
disabledContainerColor = Color.Black,
disabledContentColor = Color.White
)
) {
val type = when (global.lectureNotes[global.selectedLecture]?.type?.value) {
LectureNoteType.Normal -> "Normální"
LectureNoteType.Writing -> "Písemný test"
LectureNoteType.Speaking -> "Ústní zkoušení"
LectureNoteType.Other -> "Jiné"
else -> "Jiné"
}
Text("Typ poznámky: $type")
}
DropdownMenu(
modifier = Modifier.background(color = Color.Black),
onDismissRequest = { isExpanded = false },
expanded = isExpanded,
properties = PopupProperties(
excludeFromSystemGesture = false,
dismissOnClickOutside = true,
dismissOnBackPress = true,
focusable = true
),
) {
DropdownMenuItem(
enabled = true,
onClick = {
isExpanded = false
global.lectureNotes[global.selectedLecture]?.type?.value = LectureNoteType.Normal
Util.saveLectureNotes()
},
text = { Text("Normální poznámka", color = Color.White) }
)
DropdownMenuItem(
enabled = true,
onClick = {
isExpanded = false
global.lectureNotes[global.selectedLecture]?.type?.value = LectureNoteType.Writing
Util.saveLectureNotes()
},
text = { Text("Písemný test", color = Color.White) }
)
DropdownMenuItem(
enabled = true,
onClick = {
isExpanded = false
global.lectureNotes[global.selectedLecture]?.type?.value = LectureNoteType.Speaking
Util.saveLectureNotes()
},
text = { Text("Ústní zkoušení", color = Color.White) }
)
DropdownMenuItem(
enabled = true,
onClick = {
isExpanded = false
global.lectureNotes[global.selectedLecture]?.type?.value = LectureNoteType.Other
Util.saveLectureNotes()
},
text = { Text("Jiné", color = Color.White) }
)
}
////////////////////////////////////////////////
TextField(
value = global.lectureNotes[global.selectedLecture]?.note?.value ?: "",
onValueChange = { newText: String ->
global.lectureNotes[global.selectedLecture]?.note?.value = newText
Util.saveLectureNotes()
},
singleLine = true,
placeholder = { Text("Sem můžete napsat poznámky k\u00A0této hodině.") }, // &nbsp;
colors = TextFieldDefaults.textFieldColors(
focusedTextColor = Color.White,
containerColor = Color.Black,
selectionColors = TextSelectionColors(
handleColor = PastelYellow,
backgroundColor = PastelYellowTransparent
),
cursorColor = PastelYellow,
focusedIndicatorColor = PastelYellow,
unfocusedIndicatorColor = PastelYellowTransparent
)
)
}
}
}
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DayPager() {
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 ->
DayScreen(days[pageIndex]) DayScreen(global.timetable[pageIndex])
} }
} }
@@ -76,8 +257,12 @@ private fun getSubjectColor(s: Subject, transparent: Boolean): Color {
} }
} }
} }
fun isTimeBetweenT(givenTime: LocalTime, startTime: LocalTime, endTime: LocalTime): Boolean {
// Check if the given time is between startTime and endTime
return givenTime.isAfter(startTime) && givenTime.isBefore(endTime)
}
fun isTimeBetween(givenTime: LocalTime, startTime: LocalTime, endTime: LocalTime): Boolean { fun isTimeBetweenD(givenTime: LocalDateTime, startTime: LocalDateTime, endTime: LocalDateTime): 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)
} }
@@ -85,43 +270,60 @@ fun isTimeBetween(givenTime: LocalTime, startTime: LocalTime, endTime: LocalTime
@SuppressLint("ModifierFactoryExtensionFunction") @SuppressLint("ModifierFactoryExtensionFunction")
@Composable @Composable
fun subjectModifier(d: Day, s: Subject, lectureNumber: Int): Modifier { fun subjectModifier(d: Day, s: Subject, lectureNumber: Int): Modifier {
var day = d.date.split(".")[0].replace(" ", "")
var month = d.date.split(".")[1].replace(" ", "")
if (day.length < 2) {
day = "0$day"
}
if (month.length < 2) {
month = "0$month"
}
val dayFormatter = DateTimeFormatter.ofPattern("dd. MM. yyyy") val dayFormatter = DateTimeFormatter.ofPattern("dd. MM. yyyy")
val hourFormatter = DateTimeFormatter.ofPattern("HH:mm:ss") val hourFormatter = DateTimeFormatter.ofPattern("HH:mm:ss")
val dateParsed = LocalDate.parse(d.date + " " + LocalDate.now().year, dayFormatter) val dateParsed = LocalDate.parse("$day. $month." + " " + LocalDate.now().year, dayFormatter)
val startTimeParsed = LocalTime.parse(s.start + ":00", hourFormatter) val startTimeParsed = LocalTime.parse(s.start + ":00", hourFormatter)
val endTimeParsed = LocalTime.parse(s.end + ":00", hourFormatter) val endTimeParsed = LocalTime.parse(s.end + ":00", hourFormatter)
val todayDate = LocalDate.now() val todayDate = LocalDate.now()
val now = LocalTime.now() val now = LocalTime.now()
if (dateParsed == todayDate && isTimeBetween(now, startTimeParsed, endTimeParsed)) { if (dateParsed == todayDate && isTimeBetweenT(now, startTimeParsed, endTimeParsed)) {
currentLectureNumber = lectureNumber global.currentLectureNumber = lectureNumber
} }
return if (lectureNumber == currentLectureNumber && dateParsed == todayDate) { val m = if (settings.useAmoledMode.checked.value) {
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.height(100.dp) .height(70.dp)
.background(
color = getSubjectColor(s, true), // decide on the color (transparent)
shape = RoundedCornerShape(roundedCornerDimen)
)
.border( .border(
width = 5.dp, width = global.dimens.borderWidth,
color = getSubjectColor(s, false), color = getSubjectColor(s, false), // decide on the color (transparent)
shape = RoundedCornerShape(roundedCornerDimen) shape = RoundedCornerShape(global.dimens.roundedCorner)
) )
} else { } else {
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.height(100.dp) .height(70.dp)
.background( .background(
color = getSubjectColor(s, true), // decide on the color (transparent) color = getSubjectColor(s, true), // decide on the color (transparent)
shape = RoundedCornerShape(roundedCornerDimen) shape = RoundedCornerShape(global.dimens.roundedCorner)
) )
} }
return if (lectureNumber == global.currentLectureNumber && dateParsed == todayDate) {
m.border(
width = global.dimens.borderWidth,
color = getSubjectColor(s, false),
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
} else {
m
}
} }
@SuppressLint("UnrememberedMutableState")
@Composable @Composable
private fun DayScreen(day: Day) { private fun DayScreen(day: Day) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
@@ -138,7 +340,7 @@ private fun DayScreen(day: Day) {
Text( Text(
text = "${day.name} ${day.date}", text = "${day.name} ${day.date}",
color = Color.White, color = Color.White,
fontSize = 30.sp, fontSize = 26.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontStyle = FontStyle.Italic fontStyle = FontStyle.Italic
) )
@@ -152,12 +354,43 @@ private fun DayScreen(day: Day) {
.padding(horizontal = 5.dp) .padding(horizontal = 5.dp)
.verticalScroll(scrollState) .verticalScroll(scrollState)
) { ) {
for (subject in day.subjects) { for ((subjectIndex, subject) in day.subjects.withIndex()) {
val lectureId = Util.getLectureId(day.date, subjectIndex)
val amoledModifier = if (settings.useAmoledMode.checked.value) {
Modifier
.width(50.dp)
.fillMaxHeight()
.border(
width = global.dimens.borderWidth,
color = getSubjectColor(
subject,
false
),
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
} else {
Modifier
.width(50.dp)
.fillMaxHeight()
.background(
color = getSubjectColor(
subject,
true
),
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
}
if (day.subjects.indexOf(subject) > 0) { if (day.subjects.indexOf(subject) > 0) {
Box(modifier = Modifier.height(10.dp)) { Box(modifier = Modifier.height(10.dp)) {
if (subject.name == day.subjects[day.subjects.indexOf(subject) - 1].name) { if (subject.name == day.subjects[day.subjects.indexOf(subject) - 1].name) {
val drawable =
if (subject.free) { R.drawable.squiggly_line_green }
else { R.drawable.squiggly_line_yellow }
Image( Image(
painter = painterResource(R.drawable.squiggly_line_yellow), painter = painterResource(drawable),
contentDescription = "Spojovač stejných předmětů", contentDescription = "Spojovač stejných předmětů",
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) )
@@ -165,21 +398,14 @@ private fun DayScreen(day: Day) {
} }
} }
Row( Box (
modifier = subjectModifier(day, subject, subject.number.toInt()) modifier = subjectModifier(day, subject, subject.number.toInt()),
contentAlignment = Alignment.TopStart
) { ) {
Row {
if (subject.substitute != "2") { if (subject.substitute != "2") {
Box( Box(
modifier = Modifier modifier = amoledModifier,
.width(50.dp)
.fillMaxHeight()
.background(
color = getSubjectColor(
subject,
true
), // decide on the color (not transparent)
shape = RoundedCornerShape(roundedCornerDimen)
),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
@@ -199,12 +425,31 @@ private fun DayScreen(day: Day) {
horizontalAlignment = Alignment.Start horizontalAlignment = Alignment.Start
) { ) {
if (!subject.free) { if (!subject.free) {
if (subject.substitute == "1") {
Row {
Text( Text(
text = subject.name.uppercase(), text = subject.name.uppercase(),
color = Color.White, color = Color.White,
fontSize = 25.sp, fontSize = 25.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Spacer(modifier = Modifier.width(10.dp))
Text(
text = " supl ",
color = Color.Black,
modifier = Modifier.background(color = Color.White)
)
}
} else {
Text(
text = subject.name.uppercase(),
color = Color.White,
fontSize = 25.sp,
fontWeight = FontWeight.Bold
)
}
} }
Text( Text(
@@ -237,10 +482,10 @@ private fun DayScreen(day: Day) {
.padding(vertical = 5.dp), .padding(vertical = 5.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (!Parsing.userIsParent()) { if (settings.useSvobodaMode.checked.value) {
if ( if (
subject.teacher == "Jindřich Svoboda" && subject.teacher == "Jindřich Svoboda" &&
studentClass == "5. G" global.studentClass == "5. G"
) { ) {
Image( Image(
painter = painterResource(R.drawable.spock), painter = painterResource(R.drawable.spock),
@@ -255,17 +500,23 @@ private fun DayScreen(day: Day) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxHeight() .fillMaxHeight()
.padding(end = 20.dp), .padding(end = 10.dp),
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.End horizontalAlignment = Alignment.End
) { ) {
// show only for non prepended free lectures
if ((subject.free && subject.start != "00:00" && subject.end != "00:00") || !subject.free) {
Text( Text(
text = "${subject.start}${subject.end}", text = "${subject.start}${subject.end}",
color = Color.White, color = Color.White,
fontSize = 21.sp fontSize = 21.sp
) )
}
if (!subject.free) { if (!subject.free) {
Box (
contentAlignment = Alignment.BottomEnd
) {
Text( Text(
text = subject.teacher, text = subject.teacher,
color = Color.White, color = Color.White,
@@ -276,6 +527,35 @@ private fun DayScreen(day: Day) {
} }
} }
} }
Box(modifier = Modifier
.fillMaxSize()
.zIndex(99f)
.clickable {
global.selectedLecture = lectureId
showDetailDialog = true
})
if (
(global.lectureNotes[global.selectedLecture] != null &&
global.lectureNotes[global.selectedLecture]
?.note?.value != "") ||
(global.lectureNotes[global.selectedLecture] != null &&
global.lectureNotes[global.selectedLecture]
?.type?.value != LectureNoteType.Normal) &&
(global.lectureNotes[global.selectedLecture] != null &&
global.lectureNotes[global.selectedLecture]
?.type?.value != LectureNoteType.Other)
) {
Icon(
modifier = Modifier.padding(start = 3.dp, top = 1.dp),
imageVector = Icons.Default.EditNote,
contentDescription = "Lecture has note",
tint = Color.White // You can customize the icon color
)
}
}
}
} }
} }
} }
@@ -1,15 +1,107 @@
package com.odweta.solon package com.odweta.solon
import android.util.Log
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.json.JSONObject
@Composable @Composable
fun ToolbarScreen() { fun ToolbarScreen() {
// make the font size adaptive relative to the length of the user's name
var toolbarTextSizeAdaptive by remember { mutableStateOf(25.sp) }
if (global.status.length > 28) {
toolbarTextSizeAdaptive = 20.sp
}
if (global.status.length > 32) {
toolbarTextSizeAdaptive = 16.sp
}
if (global.status.length > 40) {
toolbarTextSizeAdaptive = 12.sp
}
if (global.status.length > 45) {
toolbarTextSizeAdaptive = 10.sp
}
Box(modifier = Modifier.padding(10.dp)) {
Text( Text(
text = toolbarText.value, text = global.status,
fontSize = 25.sp, fontSize = toolbarTextSizeAdaptive,
color = Color.White color = Color.White
) )
} }
}
enum class StatusType {
NameRole,
NoInternet,
Loading
}
private interface StatusFactoryInterface {
fun show(type: StatusType)
}
object StatusFactory: StatusFactoryInterface {
override fun show(type: StatusType) {
when (type) {
StatusType.NameRole -> {
val role = if (Parsing.userIsParent()) {
" Rodič"
} else {
" Student"
}
showStatus("${global.userData.getString("fullName")}$role")
}
StatusType.NoInternet -> {
showStatus("Bez připojení k internetu.")
}
StatusType.Loading -> {
CoroutineScope(Dispatchers.IO).launch {
global.refreshed = false
val anim = listOf(
"Načítání nových dat",
"Načítání nových dat.",
"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) {
if (i == anim.size) i = 0
showStatus(anim[i++])
delay(300)
}
}
}
}
}
}
val statusFactory = StatusFactory
fun showStatus(text: String) {
global.status = text
}
@@ -1,56 +1,23 @@
package com.odweta.solon package com.odweta.solon
import android.animation.Animator
import android.animation.AnimatorInflater
import android.animation.AnimatorListenerAdapter
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.app.ActivityManager
import android.content.Context import android.content.Context
import android.content.res.ColorStateList import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.Handler import android.os.Build
import android.util.DisplayMetrics import androidx.compose.runtime.mutableStateOf
import android.view.WindowManager import androidx.core.content.pm.PackageInfoCompat
import android.view.animation.AnimationUtils
import android.widget.Button
import androidx.compose.ui.graphics.Color
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import com.odweta.solon.ui.theme.PastelGreenTransparent
import com.odweta.solon.ui.theme.PastelOrangeTransparent
import com.odweta.solon.ui.theme.PastelRedTransparent
import com.odweta.solon.ui.theme.PastelYellowTransparent
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.nio.charset.Charset import java.nio.charset.Charset
import java.security.MessageDigest
import kotlin.math.abs
class Util { class Util {
companion object { companion object {
private fun getDisplayMetrics(context: Context): DisplayMetrics {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics)
return displayMetrics
}
fun getDisplayHeight(context: Context): Int {
return getDisplayMetrics(context).heightPixels
}
fun getDisplayWidth(context: Context): Int {
return getDisplayMetrics(context).widthPixels
}
fun saveStringToFile(filePath: String, s: String): Boolean { fun saveStringToFile(filePath: String, s: String): Boolean {
// get a file object with the specified path // get a file object with the specified path
val f = File(fileDir, filePath) val f = File(global.fileDir, filePath)
// if it does not exist, create it // if it does not exist, create it
if (!f.exists()) { if (!f.exists()) {
@@ -72,7 +39,7 @@ class Util {
} }
fun fileToString(filePath: String): String { fun fileToString(filePath: String): String {
val f = File(fileDir, filePath) val f = File(global.fileDir, filePath)
return if (f.exists()) { return if (f.exists()) {
if (f.canRead()) { if (f.canRead()) {
@@ -90,12 +57,12 @@ class Util {
} }
fun fileExists(filePath: String): Boolean { fun fileExists(filePath: String): Boolean {
val f = File(fileDir, filePath) val f = File(global.fileDir, filePath)
return f.exists() return f.exists()
} }
fun fileIsEmpty(filePath: String): Boolean { fun fileIsEmpty(filePath: String): Boolean {
val f = File(fileDir, filePath) val f = File(global.fileDir, filePath)
val content = f.readText() val content = f.readText()
return content == "" return content == ""
} }
@@ -115,7 +82,7 @@ class Util {
} }
fun deleteFile(path: String): Boolean { fun deleteFile(path: String): Boolean {
val f = File(fileDir, path) val f = File(global.fileDir, path)
if (f.exists()) { if (f.exists()) {
try { try {
@@ -129,23 +96,6 @@ class Util {
} }
} }
fun isAppInForeground(context: Context, packageName: String): Boolean {
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val runningAppProcesses = activityManager.runningAppProcesses ?: return false
for (processInfo in runningAppProcesses) {
if (processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
processInfo.processName == packageName) {
return true
}
}
return false
}
fun pixelsToDp(pixels: Int, resources: Resources): Int {
return (pixels * resources.displayMetrics.density).toInt()
}
fun avg(marks: MutableList<Mark>): String { fun avg(marks: MutableList<Mark>): String {
var sum = 0f var sum = 0f
var len = 0 var len = 0
@@ -166,7 +116,7 @@ class Util {
weights.add(mark.weight.toFloat()) weights.add(mark.weight.toFloat())
} }
return String.format("%.2f", (sum / sum(weights))).replace(',', '.') return "${Math.round(sum / sum(weights) * 100f) / 100f}".replace(",", ".")
} }
private fun sum(nums: List<Float>): Float { private fun sum(nums: List<Float>): Float {
@@ -178,47 +128,95 @@ class Util {
return sum return sum
} }
/*fun getAvgBgColor(ctx: Context, avg: String): Color { data class AppVersion(
var color: Color = PastelOrangeTransparent val versionName: String,
val versionNumber: Long,
)
if (avg.toFloat() in 1f..<2f) { fun getAppVersion(
color = PastelGreenTransparent context: Context,
} else if (avg.toFloat() in 2f..<3f) { ): AppVersion? {
color = PastelYellowTransparent return try {
} else if (avg.toFloat() in 3f..<4f) { val packageManager = context.packageManager
color = PastelOrangeTransparent val packageName = context.packageName
} else if (avg.toFloat() in 4f..5f) { val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
color = PastelRedTransparent packageManager.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(0)
)
} else {
packageManager.getPackageInfo(packageName, 0)
}
AppVersion(
versionName = packageInfo.versionName.toString(),
versionNumber = PackageInfoCompat.getLongVersionCode(packageInfo),
)
} catch (e: Exception) {
null
}
} }
return color fun getLectureId(date: String, subjectIndex: Int): Int {
} val concat = "$date[$subjectIndex]"
fun getAvgStrokeColor(ctx: Context, avg: String): Int { val bytes = concat.toByteArray()
var color: Int = R.color.pastel_orange val md5AsBytes = MessageDigest.getInstance("MD5").digest(bytes)
return abs(md5AsBytes.take(4).fold(0) { acc, byte ->
if (avg.toFloat() in 1f..<2f) { (acc shl 8) or (byte.toInt() and 0xff)
color = R.color.pastel_green })
} else if (avg.toFloat() in 2f..<3f) {
color = R.color.pastel_yellow
} else if (avg.toFloat() in 3f..<4f) {
color = R.color.pastel_orange
} else if (avg.toFloat() in 4f..5f) {
color = R.color.pastel_red
} }
return ContextCompat.getColor( fun csvToLectureNotes() {
ctx, val lines = fileToString(global.filePaths.lectureNotes).lines()
color 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)
}
}
}
fun lectureNotesToCsvString(hashMap: HashMap<Int, LectureNote>): String {
var result = ""
for ((key, lectureNote) in hashMap) {
result +=
"$key," +
"${lectureNote.type.value}," +
"${lectureNote.note.value}\n"
}
return result
}
fun loadLectureNotes() {
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
) )
} }
}
fun getAvgDrawable(ctx: Context, avg: String): Drawable { csvToLectureNotes()
val d = GradientDrawable() }
d.setColor(getAvgBgColor(ctx, avg))
d.setStroke(10, getAvgStrokeColor(ctx, avg))
d.cornerRadius = 8f
return d fun saveLectureNotes() {
}*/ val toSave = lectureNotesToCsvString(global.lectureNotes)
saveStringToFile(global.filePaths.lectureNotes, toSave)
}
} }
} }
@@ -19,11 +19,13 @@ val White = Color(0xFFFFFFFF)
val Transparent = Color(0x00000000) val Transparent = Color(0x00000000)
val PastelRed = Color(0xFFFF6666) val PastelRed = Color(0xFFFF6666)
val PastelOrangeDark = Color(0xFFDC6421)
val PastelGreen = Color(0xFF00CC99) val PastelGreen = Color(0xFF00CC99)
val PastelOrange = Color(0xFFFF9933) val PastelOrange = Color(0xFFFF9933)
val PastelYellow = Color(0xFFFFCC33) val PastelYellow = Color(0xFFFFCC33)
val PastelRedTransparent = Color(0x50FF6666) val PastelRedTransparent = Color(0x50FF6666)
val PastelOrangeDarkTransparent = Color(0x50DC6421)
val PastelGreenTransparent = Color(0x5000CC99) val PastelGreenTransparent = Color(0x5000CC99)
val PastelOrangeTransparent = Color(0x50FF9933) val PastelOrangeTransparent = Color(0x50FF9933)
val PastelYellowTransparent = Color(0x50FFCC33) val PastelYellowTransparent = Color(0x50FFCC33)
@@ -1,191 +0,0 @@
package com.odweta.solon
import android.view.Window
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
import com.odweta.solon.ui.theme.PastelYellow
import com.odweta.solon.ui.theme.PastelYellowTransparent
import okhttp3.OkHttpClient
import java.io.File
// Global values (dimens, modifiers, ...)
val roundedCornerDimen = 8.dp
val borderWidthDimen = 5.dp
val barModifier = Modifier
.fillMaxWidth()
.height(100.dp)
.padding(20.dp)
//.background(PastelYellowTransparent)
.border(
width = borderWidthDimen,
color = PastelYellow,
shape = RoundedCornerShape(roundedCornerDimen)
)
fun makeStatusBarBlack(window: Window) {
window.statusBarColor = Color.Black.toArgb()
}
const val accountDataFilePath: String = "account_data.txt"
const val timeTableFilePath: String = "timetable.json"
const val userDataFilePath: String = "userdata.json"
const val marksFilePath: String = "marks.json"
lateinit var fileDir: File
var width: Int = 0
var height: Int = 0
var internetAvail: Boolean = false
var loaded: Boolean = false
var studentClass = "1. A"
var currentLectureNumber = 0
enum class IsShowing {
Timetable,
Marks,
Menu,
About
}
enum class Screen {
Main,
Splash,
SignIn
}
var screenToShow: MutableState<Screen> = mutableStateOf(Screen.Main)
var signedInAndReadyToLaunch: MutableState<Boolean> = mutableStateOf(false)
var isShowing: MutableState<IsShowing> = mutableStateOf(IsShowing.Timetable)
var toolbarText: MutableState<String> = mutableStateOf("")
var done: Boolean = false
var timeTableAvailOffline: Boolean = false
var marksAvailOffline: Boolean = false
val subjectNameMap = mutableMapOf<String, String>()
val dayMap = mutableMapOf<Int, Day>()
val dayList = mutableListOf<Day>()
var days = listOf<Day>()
var marksJSON: String = ""
var marks: MutableMap<String, MutableList<Mark>> = mutableMapOf()
var username: String = ""
var password: String = ""
val client = OkHttpClient()
var resetNav: Boolean = false
var isAPISetUp: Boolean = false
var selectedDay: Int = 0
var dayEndString: String = ""
var isChecked: Boolean = true
const val apiBaseUrl = "https://aplikace.skolaonline.cz/solapi/api"
const val tokenUrl = "$apiBaseUrl/connect/token"
var token: String = ""
const val userDataUrl = "$apiBaseUrl/v1/user"
var userData: String = ""
var studentId: String = ""
const val timeTableUrl = "$apiBaseUrl/v1/timeTable"
var syid = "" // school year id
var marksUrl = "$apiBaseUrl/v1/students/$studentId/marks/list?SemesterId=$syid"
class Subject {
lateinit var name: String
lateinit var place: String
lateinit var teacher: String
lateinit var substitute: String
lateinit var number: String
lateinit var start: String
lateinit var end: String
var free: Boolean = false
companion object {
fun freeSubject(number: String, start: String, end: String): Subject {
val s = Subject()
s.name = "Volno"
s.place = "Volno"
s.teacher = "Volno"
s.substitute = "0"
s.number = number
s.start = start
s.end = end
s.free = true
return s
}
fun empty(): Subject {
val s = Subject()
s.name = "err"
s.place = "error"
s.teacher = "error"
s.substitute = "0"
s.number = "0"
s.start = "00:00"
s.end = "00:00"
s.free = false
return s
}
}
override fun toString(): String {
return "subject ${this.name} at ${this.place} from ${this.start} to ${this.end}; free? ${this.free}"
}
}
class Day {
var name: String = ""
var date: String = ""
var subjects: List<Subject> = mutableListOf()
}
class Mark {
lateinit var grade: String // markText
lateinit var weight: String // weight
lateinit var subject: String // subjectId -> parse that
lateinit var date: String // markDate
lateinit var theme: String // theme
override fun toString(): String {
return "mark: ${this.grade} * ${this.weight}, subject: ${this.subject}, description: ${this.theme}, date: ${this.date}"
}
fun none(): Mark {
val mark = Mark()
mark.subject = "Zatím žádné známky."
mark.grade = "0"
mark.weight = "0"
mark.date = "-"
mark.theme = "-"
return mark
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

@@ -1,53 +1,74 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector <vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="108dp" android:height="108dp"
android:width="108dp" android:width="108dp"
android:viewportHeight="108" android:viewportHeight="108"
android:viewportWidth="108"> android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Background Color --> <path android:fillColor="#3DDC84"
<path android:fillColor="#000000"
android:pathData="M0,0h108v108h-108z"/> android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
<!-- Vertical Grid Lines --> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:pathData="M9,0L9,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#000000" <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeWidth="0.8"/> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:pathData="M19,0L19,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#000000" <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeWidth="0.8"/> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:pathData="M29,0L29,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#000000" <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeWidth="0.8"/> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<!-- Repeat the lines for remaining grid --> <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<!-- Horizontal Grid Lines --> <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
<path android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M0,9L108,9" <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8"/> <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
<path android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M0,19L108,19" <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8"/> <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
<path android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M0,29L108,29" <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8"/> <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
<!-- Repeat the lines for remaining grid --> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
<!-- Additional Grid Lines within the Central Area --> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:pathData="M19,29L89,29" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#000000" <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeWidth="0.8"/> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:pathData="M19,39L89,39" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#000000" <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeWidth="0.8"/> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<!-- Repeat similar paths for the remaining inner lines --> <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector> </vector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/launcher_background"/> <background android:drawable="@mipmap/launcher_background"/>
<foreground android:drawable="@mipmap/launcher_foreground"/> <foreground android:drawable="@mipmap/launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/launcher_background"/> <background android:drawable="@mipmap/launcher_background"/>
<foreground android:drawable="@mipmap/launcher_foreground"/> <foreground android:drawable="@mipmap/launcher_foreground"/>
</adaptive-icon> </adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 746 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 650 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 912 B

After

Width:  |  Height:  |  Size: 590 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

@@ -5,7 +5,7 @@
--> -->
<data-extraction-rules> <data-extraction-rules>
<cloud-backup> <cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up. <!-- xTODO: Use <include> and <exclude> to control what is backed up.
<include .../> <include .../>
<exclude .../> <exclude .../>
--> -->
+1
View File
@@ -21,3 +21,4 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
sdk.dir=~/Android/Sdk
+1 -1
View File
@@ -1,6 +1,6 @@
[versions] [versions]
accompanistCoil = "0.30.1" accompanistCoil = "0.30.1"
agp = "8.7.1" agp = "8.7.2"
coilCompose = "2.4.0" coilCompose = "2.4.0"
glide = "4.16.0" glide = "4.16.0"
kotlin = "2.0.0" kotlin = "2.0.0"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
cp app/build/outputs/apk/release/com.odweta.solon_$1.apk ../release/
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 15 KiB