37 Commits
Author SHA1 Message Date
Odweta 1d885ade70 messages 2025-01-13 20:46:05 +01:00
Odweta 8615eee3a7 created TempScreen for on-demand recompositions of the whole app.
enhanced UI
2025-01-09 10:40:20 +01:00
Odweta e3bb1b9cfa update README to latest verName 2025-01-08 17:19:09 +01:00
Odweta 0109c7aa09 Merge branch 'patch-1' into 'master'
Edit README.md

See merge request Odweta/solon!4
2025-01-08 16:16:44 +00:00
Poussinou 7800eeb61b Edit README.md 2025-01-08 16:16:44 +00:00
Odweta 4b7fbfd766 optimized suboptimal code 2025-01-08 17:02:20 +01:00
Odweta 66f3ad791f made new release 2.4 2025-01-03 21:27:19 +01:00
Odweta 910e968653 made new release 2.5 2025-01-03 11:44:28 +01:00
Odweta 31197830d0 tried to make changes to some live data, did not go well so rollback 2025-01-03 11:42:43 +01:00
Odweta a619c8b92e the same 2025-01-03 10:23:52 +01:00
Odweta 04d8527434 sign out ui improvements 2025-01-03 10:22:57 +01:00
Odweta bea50f7fe7 buttons now have icons and text instead of just text 2025-01-03 10:21:33 +01:00
Odweta e9a422eda0 ui improvements, now even marks are animated 2025-01-03 10:06:15 +01:00
Odweta cc86373989 update deploy.sh 2024-12-31 17:34:24 +01:00
Odweta 8b98d92c2b fix deploy 2024-12-31 17:32:27 +01:00
Odweta b10438c109 made new release 2024-12-31 17:28:44 +01:00
Odweta 69f48750f1 update deploy script 2024-12-31 17:27:59 +01:00
Odweta 81fb398ae2 added animations, changed signOut screen 2024-12-31 17:22:09 +01:00
Odweta 1516a00841 fixed app crashes and some other improvements 2024-12-30 17:31:27 +01:00
Odweta 6dae99d311 updated deploying 2024-12-22 20:28:50 +01:00
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
39 changed files with 1674 additions and 1753 deletions
+2 -15
View File
@@ -1,16 +1,3 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
mobile/app/build
mobile/local.properties
mobile/.gradle
-10
View File
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-239e49af:192a4be9597:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>
-17
View File
@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+4 -1
View File
@@ -1,3 +1,6 @@
![solon-logo](solon_logo.png)
# Zorientujte se rychleji v této odlehčené verzi Školy OnLine
# Odkaz na stažení aplikace pro Android [zde](https://gitlab.com/Odweta/solon/-/releases/2.2.2/downloads/app-release.apk) (verze 2.2.2)
# Odkaz na stažení aplikace pro Android [zde](https://gitlab.com/Odweta/solon/-/releases/2.4/downloads/app-release.apk) (verze 2.4)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Nyní na F-Droidu"
height="80">](https://f-droid.org/packages/com.odweta.solon/)
-4
View File
@@ -1,4 +0,0 @@
- add settings
- color schemes
- modes
- new logo
+33 -15
View File
@@ -1,26 +1,39 @@
#!/bin/bash
echo -n "version number: "
read ver
echo -n "glpat: "
echo -n "[glpat]: "
read glpat
echo -n "[keystore password]: "
read ks_pass
cd ./mobile
ver=$(./gradlew printVersionName -q)
# add, commit, push
echo
echo "[add, commit, push]"
echo
git add ..
git commit -m "made new release $ver"
git push -f
# build
echo
echo "[building]"
echo
./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
echo
echo "[signing]"
echo
~/Android/Sdk/build-tools/35.0.0/apksigner sign --alignment-preserved --ks ~/Documents/android-keystore.jks --ks-pass pass:$ks_pass app/build/outputs/apk/release/com.odweta.solon_$ver.apk
# create the tag
echo "\n[creating tag v$ver]"
echo
echo "[creating tag v$ver]"
echo
data_str='{ "tag_name": "'$ver'", "ref": "HEAD", "message": "Tagging release v'$ver'" }'
curl --request POST \
--header "PRIVATE-TOKEN: $glpat" \
@@ -29,7 +42,9 @@ curl --request POST \
"https://gitlab.com/api/v4/projects/47146187/repository/tags"
# make a release
echo "\n[publishing release $ver]"
echo
echo "[publishing release $ver]"
echo
data_str='{ "name": "v'$ver'", "tag_name": "'$ver'", "ref": "HEAD" }'
curl --request POST \
--header "PRIVATE-TOKEN: $glpat" \
@@ -38,20 +53,23 @@ curl --request POST \
"https://gitlab.com/api/v4/projects/47146187/releases"
# attach a file
echo "\n[attaching the release apk]"
echo
echo "[attaching the release apk]"
echo
asset_url="https://gitlab.com/-/project/47146187"$(curl --request POST \
--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]"
echo
echo "[creating release asset]"
echo
curl --request POST \
--header "PRIVATE-TOKEN: $glpat" \
--data name="app-release.apk" \
--data url="$asset_url" \
--data filepath="/app-release.apk" \
--data "assets[links][][link_type]=package" \
"https://gitlab.com/api/v4/projects/47146187/releases/$ver/assets/links"
# cd back
-340
View File
@@ -1,340 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DeviceStreaming">
<option name="deviceSelectionList">
<list>
<PersistentDeviceSelectionData>
<option name="api" value="27" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="F01L" />
<option name="id" value="F01L" />
<option name="manufacturer" value="FUJITSU" />
<option name="name" value="F-01L" />
<option name="screenDensity" value="360" />
<option name="screenX" value="720" />
<option name="screenY" value="1280" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="OPPO" />
<option name="codename" value="OP573DL1" />
<option name="id" value="OP573DL1" />
<option name="manufacturer" value="OPPO" />
<option name="name" value="CPH2557" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="28" />
<option name="brand" value="DOCOMO" />
<option name="codename" value="SH-01L" />
<option name="id" value="SH-01L" />
<option name="manufacturer" value="SHARP" />
<option name="name" value="AQUOS sense2 SH-01L" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="Lenovo" />
<option name="codename" value="TB370FU" />
<option name="id" value="TB370FU" />
<option name="manufacturer" value="Lenovo" />
<option name="name" value="Tab P12" />
<option name="screenDensity" value="340" />
<option name="screenX" value="1840" />
<option name="screenY" value="2944" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="samsung" />
<option name="codename" value="a51" />
<option name="id" value="a51" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy A51" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="akita" />
<option name="id" value="akita" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="b0q" />
<option name="id" value="b0q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S22 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="32" />
<option name="brand" value="google" />
<option name="codename" value="bluejay" />
<option name="id" value="bluejay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="caiman" />
<option name="id" value="caiman" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro" />
<option name="screenDensity" value="360" />
<option name="screenX" value="960" />
<option name="screenY" value="2142" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="comet" />
<option name="id" value="comet" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro Fold" />
<option name="screenDensity" value="390" />
<option name="screenX" value="2076" />
<option name="screenY" value="2152" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="29" />
<option name="brand" value="samsung" />
<option name="codename" value="crownqlteue" />
<option name="id" value="crownqlteue" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Note9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2220" />
<option name="screenY" value="1080" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="dm3q" />
<option name="id" value="dm3q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S23 Ultra" />
<option name="screenDensity" value="600" />
<option name="screenX" value="1440" />
<option name="screenY" value="3088" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="e1q" />
<option name="id" value="e1q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy S24" />
<option name="screenDensity" value="480" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="felix" />
<option name="id" value="felix" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="felix_camera" />
<option name="id" value="felix_camera" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Fold (Camera-enabled)" />
<option name="screenDensity" value="420" />
<option name="screenX" value="2208" />
<option name="screenY" value="1840" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="samsung" />
<option name="codename" value="gts8uwifi" />
<option name="id" value="gts8uwifi" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Tab S8 Ultra" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1848" />
<option name="screenY" value="2960" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="husky" />
<option name="id" value="husky" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8 Pro" />
<option name="screenDensity" value="390" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="motorola" />
<option name="codename" value="java" />
<option name="id" value="java" />
<option name="manufacturer" value="Motorola" />
<option name="name" value="G20" />
<option name="screenDensity" value="280" />
<option name="screenX" value="720" />
<option name="screenY" value="1600" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="komodo" />
<option name="id" value="komodo" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9 Pro XL" />
<option name="screenDensity" value="360" />
<option name="screenX" value="1008" />
<option name="screenY" value="2244" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="lynx" />
<option name="id" value="lynx" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7a" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="31" />
<option name="brand" value="google" />
<option name="codename" value="oriole" />
<option name="id" value="oriole" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="panther" />
<option name="id" value="panther" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 7" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q5q" />
<option name="id" value="q5q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold5" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1812" />
<option name="screenY" value="2176" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="samsung" />
<option name="codename" value="q6q" />
<option name="id" value="q6q" />
<option name="manufacturer" value="Samsung" />
<option name="name" value="Galaxy Z Fold6" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1856" />
<option name="screenY" value="2160" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="r11" />
<option name="id" value="r11" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Watch" />
<option name="screenDensity" value="320" />
<option name="screenX" value="384" />
<option name="screenY" value="384" />
<option name="type" value="WEAR_OS" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="30" />
<option name="brand" value="google" />
<option name="codename" value="redfin" />
<option name="id" value="redfin" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 5" />
<option name="screenDensity" value="440" />
<option name="screenX" value="1080" />
<option name="screenY" value="2340" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="shiba" />
<option name="id" value="shiba" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 8" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2400" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="33" />
<option name="brand" value="google" />
<option name="codename" value="tangorpro" />
<option name="id" value="tangorpro" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel Tablet" />
<option name="screenDensity" value="320" />
<option name="screenX" value="1600" />
<option name="screenY" value="2560" />
</PersistentDeviceSelectionData>
<PersistentDeviceSelectionData>
<option name="api" value="34" />
<option name="brand" value="google" />
<option name="codename" value="tokay" />
<option name="id" value="tokay" />
<option name="manufacturer" value="Google" />
<option name="name" value="Pixel 9" />
<option name="screenDensity" value="420" />
<option name="screenX" value="1080" />
<option name="screenY" value="2424" />
</PersistentDeviceSelectionData>
</list>
</option>
</component>
</project>
+123
View File
@@ -0,0 +1,123 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
-6
View File
@@ -13,12 +13,6 @@
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="TimetablePreview">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
<SelectionState runConfigName="RBPreview">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+22 -2
View File
@@ -4,6 +4,26 @@ plugins {
alias(libs.plugins.kotlin.compose)
}
tasks.register("printVersionName") {
doLast {
println(android.defaultConfig.versionName)
}
}
tasks.register("deploy") {
dependsOn("assembleDebug")
dependsOn("installDebug")
doLast {
exec {
commandLine("adb", "shell", "am", "start", "-n", "com.odweta.solon/.MainActivity")
}
}
}
tasks.register("b") {
dependsOn("assembleDebug")
}
android {
namespace = "com.odweta.solon"
compileSdk = 34
@@ -12,8 +32,8 @@ android {
applicationId = "com.odweta.solon"
minSdk = 28
targetSdk = 34
versionCode = 207
versionName = "2.2.2"
versionCode = 211
versionName = "2.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Binary file not shown.
Binary file not shown.
-37
View File
@@ -1,37 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.odweta.solon",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "2.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 28
}
@@ -38,11 +38,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun AboutScreen(modifier: Modifier = Modifier) {
fun AboutScreen() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler {
isShowing.value = IsShowing.Menu
}
BackHandler { global.currentScreenElement = ScreenElement.Menu }
Column(
modifier = Modifier
@@ -50,35 +48,7 @@ fun AboutScreen(modifier: Modifier = Modifier) {
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(start = 5.dp)
) {
Button(
onClick = {
isShowing.value = IsShowing.Menu
},
colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
containerColor = Color.Black
),
modifier = Modifier
.border(
width = 5.dp,
shape = RoundedCornerShape(roundedCornerDimen),
color = PastelYellow
)
.width(70.dp)
.height(70.dp)
) {
Icon(
imageVector = Icons.Default.ArrowBackIosNew,
contentDescription = "Back icon",
modifier = Modifier.size(40.dp)
)
}
}
backButtonFactory.get(BackButtonType.Menu)()
Column(
modifier = Modifier.fillMaxWidth(),
@@ -126,7 +96,7 @@ fun AboutScreen(modifier: Modifier = Modifier) {
}
@Composable
fun HorizontalLine(modifier: Modifier = Modifier) {
fun HorizontalLine() {
Spacer(modifier = Modifier.height(20.dp))
Spacer(modifier = Modifier
@@ -143,7 +113,7 @@ fun AboutTableRow(key: String, value: String, href: String = "") {
Row(
modifier = Modifier
.padding(10.dp)
.background(color = Color.Black, shape = RoundedCornerShape(roundedCornerDimen))
.background(color = Color.Black, shape = RoundedCornerShape(global.dimens.roundedCorner))
.fillMaxWidth()
) {
Text(
@@ -1,5 +1,6 @@
package com.odweta.solon
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
@@ -8,17 +9,10 @@ import java.nio.charset.Charset
class Account(val username: String, val password: String, private val error: Int) {
companion object {
private fun parseAccountDataFile(file: File): Account {
val lines: List<String> = file.readLines()
if (lines.size >= 2) {
if (lines[0] != "" && lines[1] != "") {
private fun parseAccountDataFile(): Account {
val lines: List<String> = File(global.fileDir, global.filePaths.accountData).readLines()
return Account(lines[0], lines[1], 0)
}
}
return Account("", "", 1)
}
fun haveSaved(): Boolean {
/* check if a file exists
@@ -27,40 +21,39 @@ class Account(val username: String, val password: String, private val error: Int
*/
// check if file exists
val file = File(fileDir, accountDataFilePath)
return if (!file.exists()) {
return if (
!Util.fileExists(global.filePaths.accountData) ||
Util.fileIsEmpty(global.filePaths.accountData)
) {
// it does not exist
file.createNewFile()
File(global.fileDir, global.filePaths.accountData).createNewFile()
false
} else {
// it exists, so it shall be parsed
val acc: Account = parseAccountDataFile(file)
username = acc.username
password = acc.password
val acc: Account = parseAccountDataFile()
global.username = acc.username
global.password = acc.password
acc.error == 0
}
}
fun save(acc: Account): Boolean {
return try {
val file = File(fileDir, accountDataFilePath)
file.writeText("${acc.username}\n${acc.password}\n", Charset.forName("UTF-8"))
val payload = "${acc.username}\n${acc.password}"
return Util.saveStringToFile(global.filePaths.accountData, payload)
}
true
} catch (e: Exception) {
fun isValid(acc: Account): Boolean {
global.username = acc.username
global.password = acc.password
val tokenJSON = networkRequestFactory.perform(NetworkRequestType.Token)
return if (tokenJSON.has("token")) {
val token = tokenJSON.getString("token")
!token.contains("\"error\"")
} else {
false
}
}
suspend fun isValid(acc: Account): Boolean {
return withContext(Dispatchers.IO) {
username = acc.username
password = acc.password
val token = Network.getToken()
!JSONObject(token).has("error")
}
}
}
}
@@ -0,0 +1,79 @@
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.padding(start = 10.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
global.currentScreen = Screen.Main
} } }
BackButtonType.MarksAll -> { { BackButton {
global.currentScreenElement = ScreenElement.Marks
global.marksDepthLevel = MarksDepthLevel.All
} } }
BackButtonType.MarksSubject -> { { BackButton { global.marksDepthLevel = MarksDepthLevel.Subject } } }
}
}
}
val backButtonFactory = BackButtonFactory
@@ -1,81 +1,46 @@
package com.odweta.solon
import android.view.Window
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.fillMaxSize
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.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.RecomposeScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import com.odweta.solon.ui.theme.PastelYellow
import okhttp3.OkHttpClient
import org.json.JSONObject
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"
const val settingsDataFilePath: String = "settings.txt"
const val lectureNotesFilePath: String = "lecture_notes.csv"
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,
Settings
}
import java.time.LocalDateTime
import java.util.concurrent.Callable
enum class Screen {
Main,
Splash,
SignIn
SignIn,
Settings,
Temp
}
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("")
// default settings (will get overwritten by the saved settings)
class Setting(auxId: SettingsId, auxName: String, auxChecked: Boolean) {
var id: SettingsId = auxId
@@ -88,8 +53,7 @@ enum class SettingsId {
Svoboda,
Amoled,
RefreshButton,
TagNewMarks,
DeleteLocalData
Animations
}
class SettingsData {
@@ -111,42 +75,15 @@ class SettingsData {
true
)
var tagNewMarks: Setting = Setting(
SettingsId.TagNewMarks,
"Označit nové známky",
var useAnimations: Setting = Setting(
SettingsId.Animations,
"Používat animace v aplikaci",
true
)
override fun toString() = "$useSvobodaMode\n$useAmoledMode\n$showRefreshButton\n$tagNewMarks"
override fun toString() = "$useSvobodaMode\n$useAmoledMode\n$showRefreshButton\n$useAnimations"
}
var settings : SettingsData = SettingsData()
var done: Boolean = false
var internetAwaitListenerRunning: 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
enum class LectureNoteType {
Normal, // obycejna poznamka
Writing, // pisemny test
@@ -164,47 +101,13 @@ fun String.toLectureNoteType(): LectureNoteType {
}
}
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
}
}
var lectureNotes: HashMap<Int, LectureNote> = hashMapOf()
var selectedLecture: Int = 0
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"
data class LectureNote(
var id: Int,
var note: MutableState<String> = mutableStateOf(""),
var type: MutableState<LectureNoteType> = mutableStateOf(LectureNoteType.Normal),
var number: Int,
var lectureName: String
)
class Subject {
lateinit var name: String
@@ -265,6 +168,7 @@ class Mark {
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}"
@@ -281,3 +185,128 @@ class Mark {
return mark
}
}
enum class ScreenElement {
Timetable,
Marks,
NewMarks,
FinalMarks,
Menu,
About,
SignOut
}
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 tempScreen by mutableStateOf(Screen.Settings)
var currentScreen by mutableStateOf(Screen.Main)
var currentScreenElement by mutableStateOf(ScreenElement.Timetable)
var status by mutableStateOf("")
var returnToTempScreen by mutableStateOf(false)
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()
private fun <T> changeAndRevert(variable: T, newValue: T, revert: (T) -> Unit) {
val orig = variable
revert(newValue)
revert(orig)
}
@Composable
fun <T> Animate(targetState: T, content: @Composable (target: T) -> Unit) {
if (settings.useAnimations.checked.value == true) {
// When animations are enabled, apply AnimatedContent
AnimatedContent(
targetState = targetState,
transitionSpec = {
(slideInVertically(initialOffsetY = { it }) + fadeIn()).togetherWith(
slideOutVertically(targetOffsetY = { it }) + fadeOut()
)
},
modifier = Modifier.fillMaxSize(),
label = "animated content"
) { target ->
content(target)
}
} else {
// When animations are disabled, just show content without animation
content(targetState)
}
}
@@ -1,308 +1,137 @@
package com.odweta.solon
import android.content.Context
import android.widget.Toast
import android.util.Log
import androidx.compose.ui.graphics.Color
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.json.JSONObject
private var week: List<Day> = mutableListOf()
private var toastShowing: Boolean = false
private var toastShown: Boolean = false
private var toast2Shown: Boolean = false
fun launchStart(ctx: Context) {
global.launched = false
if (Account.haveSaved()) {
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 {
// setup API
setupAPI()
private fun setOnInternetAvailListener(ctx: Context, coscope: CoroutineScope) {
internetAwaitListenerRunning = true
coscope.launch {
// fetch remote data
global.timetableJSON = networkRequestFactory.perform(NetworkRequestType.Timetable)
global.marksJSON = networkRequestFactory.perform(NetworkRequestType.Marks)
// save to local for later local load
Util.saveStringToFile(global.filePaths.timetable, global.timetableJSON.toString())
Util.saveStringToFile(global.filePaths.marks, global.marksJSON.toString())
launchContinue(
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
// global.settings = Util.fileToString(global.filePaths.settings).toSettingsData()
// load the lecture notes
Util.loadLectureNotes()
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) {
if (Util.internetAvail(ctx) && !internetAvail) {
internetAvail = Util.internetAvail(ctx)
coscope.launch {
fetchNewData(ctx, coscope)
CoroutineScope(Dispatchers.IO).launch {
remoteLoad(ctx)
}
} else if (!internetAvail) {
internetAvail = Util.internetAvail(ctx)
toolbarText.value = "Bez připojení k internetu."
showStatus("Bez připojení k internetu.")
} else {
internetAvail = Util.internetAvail(ctx)
}
delay(1000) // Check for internet availability every second
}
}
}
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...",
"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) {
if (done) break
toolbarText.value = text
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) {
// TODO: CONVERT TO SNACKBARS
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 || !marksAvailOffline || internetAvail) {
// set up the API (required for later uses of it)
if (!isAPISetUp) {
Network.setupAPI()
}
if (internetAvail) {
// cancel a toast, if any
if (toastShowing && !toastShown) {
toast.cancel()
}
// get the JSON data
var timeTableJSON = Network.getTimeTableJSON()
if (timeTableJSON == """{"error": "network"}""") {
timeTableJSON = Util.fileToString(timeTableFilePath)
}
// fetch and parse marks
marksJSON = Network.getMarksJSON()
if (marksJSON == """{"error": "network"}""") {
marksJSON = Util.fileToString(marksFilePath)
}
timeTableAvailOffline =
Util.saveStringToFile(timeTableFilePath, timeTableJSON)
marksAvailOffline =
Util.saveStringToFile(marksFilePath, marksJSON)
// parse the data
week = Parsing.parseTimeTableJSON(timeTableJSON)
marks = Parsing.parseMarksJSON(marksJSON)
// set the settings
if (Util.fileExists(settingsDataFilePath) && !Util.fileIsEmpty(settingsDataFilePath)) {
settings = Util.stringToSettingsData(Util.fileToString(settingsDataFilePath))
} // else load the default settings
// 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)
}
Util.loadLectureNotes()
loaded = true
coscope_.launch {
if (!internetAwaitListenerRunning) {
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
if (settings.showRefreshButton.checked.value) {
refreshButtonContentColor.value = Color.White
}
}
@@ -1,7 +1,6 @@
package com.odweta.solon
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
@@ -9,51 +8,59 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.core.view.WindowCompat
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.PointerIcon.Companion.Text
import androidx.compose.ui.unit.dp
import com.odweta.solon.ui.theme.SolonTheme
import kotlin.math.sign
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
makeStatusBarBlack(window)
// for seemless <currentScreen> switching
window.statusBarColor = Color.Black.toArgb()
window.setBackgroundDrawable(resources.getDrawable(R.drawable.black))
global.fileDir = applicationContext.filesDir
launchStart(applicationContext)
setContent {
if (!loaded) {
launchPhase0(LocalContext.current, rememberCoroutineScope())
}
when (signedInAndReadyToLaunch.value) {
true -> launchPhase0(LocalContext.current, rememberCoroutineScope())
else -> {}
}
SolonTheme {
when (screenToShow.value) {
Screen.Main -> MainScreen()
Animate(global.currentScreen) { targetScreen ->
when (targetScreen) {
Screen.Main -> if (global.launched) MainScreen()
Screen.Splash -> SplashScreen()
Screen.SignIn -> SignInScreen()
Screen.Settings -> SettingsScreen()
Screen.Temp -> TempScreen()
}
}
}
}
}
}
@Composable
fun MainScreen(modifier: Modifier = Modifier) {
fun MainScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
) {
Box(
modifier = barModifier,
modifier = global.modifiers.bar,
contentAlignment = Alignment.Center
) {
ToolbarScreen()
@@ -64,21 +71,24 @@ class MainActivity : ComponentActivity() {
.fillMaxWidth()
.weight(1f)
) {
when (isShowing.value) {
IsShowing.Timetable -> TimetableScreen()
IsShowing.Marks -> MarksScreen()
IsShowing.Menu -> MenuScreen()
IsShowing.About -> AboutScreen()
IsShowing.Settings -> SettingsScreen()
Animate(global.currentScreenElement) { targetScreen ->
when (targetScreen) {
ScreenElement.Timetable -> TimetableScreen()
ScreenElement.Marks -> MarksScreen()
ScreenElement.NewMarks -> NewMarksScreen()
ScreenElement.FinalMarks -> FinalMarksScreen()
ScreenElement.Menu -> MenuScreen()
ScreenElement.About -> AboutScreen()
ScreenElement.SignOut -> SignOutScreen()
}
}
}
Box(
modifier = barModifier,
modifier = global.modifiers.bar,
contentAlignment = Alignment.Center
) {
NavbarScreen()
}
}
}
}
+311 -149
View File
@@ -14,28 +14,32 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBackIosNew
import androidx.compose.material.icons.filled.AutoAwesome
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material.icons.filled.WorkspacePremium
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.text.TextStyle
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.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.odweta.solon.ui.theme.PastelGreen
import com.odweta.solon.ui.theme.PastelGreenTransparent
import com.odweta.solon.ui.theme.PastelOrange
@@ -46,25 +50,207 @@ import com.odweta.solon.ui.theme.PastelRed
import com.odweta.solon.ui.theme.PastelRedTransparent
import com.odweta.solon.ui.theme.PastelYellow
import com.odweta.solon.ui.theme.PastelYellowTransparent
import java.time.LocalDate
import java.time.format.DateTimeFormatter
enum class MarksShow {
var showNewMarksDialog by mutableStateOf(false)
var showFinalMarksDialog by mutableStateOf(false)
enum class MarksDepthLevel {
All,
Subject,
Detail
}
var marksShow: MutableState<MarksShow> = mutableStateOf(MarksShow.All)
private var subjectToShow: String = ""
private var markToShow: Mark = Mark()
@Composable
fun MarksScreen() {
when (marksShow.value) {
MarksShow.All -> MarksAll()
MarksShow.Subject -> MarksSubject()
MarksShow.Detail -> MarksDetail()
Animate(global.marksDepthLevel) { targetDepthLevel ->
when (targetDepthLevel) {
MarksDepthLevel.All -> MarksAll()
MarksDepthLevel.Subject -> MarksSubject()
MarksDepthLevel.Detail -> MarksDetail()
}
}
}
@Composable
fun NewMarksScreen() {
BackHandler { global.currentScreenElement = ScreenElement.Marks }
val ss = rememberScrollState()
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
backButtonFactory.get(BackButtonType.MarksAll)()
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier
.background(
color = Color.Black,
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.border(
shape = RoundedCornerShape(global.dimens.roundedCorner),
color = PastelYellow,
width = global.dimens.borderWidth
)
.height(70.dp)
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Nové známky",
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 30.sp,
textAlign = TextAlign.Center
)
}
}
}
Spacer(Modifier.height(10.dp))
Column(
modifier = Modifier.verticalScroll(ss)
) {
for ((k, v) in global.newMarks) {
if (v.size > 0) {
Column(
modifier = Modifier
.padding(horizontal = 10.dp)
.padding(bottom = 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()
}
}
}
}
}
}
}
@Composable
fun FinalMarksScreen() {
BackHandler { global.currentScreenElement = ScreenElement.Marks }
val ss = rememberScrollState()
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
backButtonFactory.get(BackButtonType.MarksAll)()
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center
) {
Row(
modifier = Modifier
.background(
color = Color.Black,
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.border(
shape = RoundedCornerShape(global.dimens.roundedCorner),
color = PastelYellow,
width = global.dimens.borderWidth
)
.height(70.dp)
.padding(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
"Uzavřené známky",
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 30.sp,
textAlign = TextAlign.Center
)
}
}
}
Spacer(Modifier.height(10.dp))
Column(
modifier = Modifier.verticalScroll(ss)
) {
Text("test", color = Color.White)
Text("test", color = Color.White)
Text("test", color = Color.White)
Text("test", color = Color.White)
Text("test", color = Color.White)
Text("test", color = Color.White)
Text("test", color = Color.White)
Text("test", color = Color.White)
}
}
}
@@ -115,15 +301,18 @@ fun MarksAll() {
.padding(horizontal = 5.dp)
.verticalScroll(scrollState)
) {
for ((subject, markList) in marks) {
val amoledModifier = if (settings.useAmoledMode.checked.value) {
for ((subject, markList) in global.marks) {
val amoledModifier = if (settings.useAmoledMode.checked.value == true) {
Modifier
.width(150.dp)
.fillMaxHeight()
.border(
width = borderWidthDimen,
color = getAvgColor(Util.avg(markList), true), // decide on the color (transparent)
shape = RoundedCornerShape(roundedCornerDimen)
width = global.dimens.borderWidth,
color = getAvgColor(
Util.avg(markList),
true
), // decide on the color (transparent)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
} else {
Modifier
@@ -131,22 +320,25 @@ fun MarksAll() {
.fillMaxHeight()
.background(
color = getAvgColor(Util.avg(markList)), // decide on the color (not transparent)
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
}
val amoledModifierBackground = if (settings.useAmoledMode.checked.value) {
val amoledModifierBackground = if (settings.useAmoledMode.checked.value == true) {
Modifier
.fillMaxWidth()
.height(70.dp)
.border(
width = borderWidthDimen,
color = getAvgColor(Util.avg(markList), true), // decide on the color (transparent)
shape = RoundedCornerShape(roundedCornerDimen)
width = global.dimens.borderWidth,
color = getAvgColor(
Util.avg(markList),
true
), // decide on the color (transparent)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.clickable {
marksShow.value = MarksShow.Subject
subjectToShow = subject
global.marksDepthLevel = MarksDepthLevel.Subject
global.subjectToShow = subject
}
} else {
Modifier
@@ -154,15 +346,15 @@ fun MarksAll() {
.height(70.dp)
.background(
color = getAvgColor(Util.avg(markList)), // decide on the color (transparent)
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.clickable {
marksShow.value = MarksShow.Subject
subjectToShow = subject
global.marksDepthLevel = MarksDepthLevel.Subject
global.subjectToShow = subject
}
}
val amoledSpacerModifier = if (settings.useAmoledMode.checked.value) {
val amoledSpacerModifier = if (settings.useAmoledMode.checked.value == true) {
Modifier
.width(10.dp)
.fillMaxHeight()
@@ -182,20 +374,10 @@ fun MarksAll() {
}
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))
}
// only add if mark date is not older than one week
var subjectContainsNewMark = false
for (mark in markList) {
if (LocalDate.parse(mark.date, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
.isAfter(LocalDate.now().minusWeeks(1))
) {
subjectContainsNewMark = true
}
}
Row(
modifier = amoledModifierBackground
) {
@@ -234,18 +416,11 @@ fun MarksAll() {
}
Box(
modifier = Modifier.fillMaxSize().weight(1f),
modifier = Modifier
.fillMaxSize()
.weight(1f),
contentAlignment = Alignment.TopStart
) {
if (subjectContainsNewMark && settings.tagNewMarks.checked.value) {
Icon(
modifier = Modifier.padding(start = 4.dp, top = 4.dp),
imageVector = Icons.Default.AutoAwesome,
contentDescription = "New marks in subject",
tint = Color.White // You can customize the icon color
)
}
Box(
modifier = Modifier
.fillMaxSize()
@@ -261,15 +436,58 @@ fun MarksAll() {
}
}
}
Spacer(Modifier.height(80.dp))
}
Box(Modifier.fillMaxSize()) {
Row(
modifier = Modifier
.align(Alignment.BottomCenter)
) {
FloatingActionButton(
onClick = { global.currentScreenElement = ScreenElement.NewMarks },
modifier = Modifier
.padding(10.dp)
.background(
Color.Black,
shape = RoundedCornerShape(global.dimens.roundedCorner)
),
contentColor = PastelYellow,
containerColor = Color.Black
) {
Icon(
imageVector = Icons.Default.AutoAwesome,
contentDescription = "New marks in subject",
tint = PastelYellow // You can customize the icon color
)
}
FloatingActionButton(
onClick = { global.currentScreenElement = ScreenElement.FinalMarks },
modifier = Modifier
.padding(10.dp)
.background(
Color.Black,
shape = RoundedCornerShape(global.dimens.roundedCorner)
),
contentColor = PastelYellow,
containerColor = Color.Black
) {
Icon(
imageVector = Icons.Default.WorkspacePremium,
contentDescription = "Final marks",
tint = PastelYellow // You can customize the icon color
)
}
}
}
}
@Composable
fun MarksSubject() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler {
marksShow.value = MarksShow.All
}
BackHandler { global.marksDepthLevel = MarksDepthLevel.All }
val scrollState = rememberScrollState()
@@ -283,29 +501,7 @@ fun MarksSubject() {
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(
onClick = {
marksShow.value = MarksShow.All
},
colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
containerColor = Color.Black
),
modifier = Modifier
.border(
width = 5.dp,
shape = RoundedCornerShape(roundedCornerDimen),
color = PastelYellow
)
.width(70.dp)
.height(70.dp)
) {
Icon(
imageVector = Icons.Default.ArrowBackIosNew,
contentDescription = "Back icon",
modifier = Modifier.size(40.dp)
)
}
backButtonFactory.get(BackButtonType.MarksAll)()
Box(modifier = Modifier.weight(1f))
@@ -314,26 +510,27 @@ fun MarksSubject() {
.width(70.dp)
.height(70.dp)
.border(
width = 5.dp,
width = global.dimens.borderWidth,
color = getAvgColor(
Util.avg(marks[subjectToShow]!!),
Util.avg(global.marks[global.subjectToShow]!!),
true
),
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.background(color = if (settings.useAmoledMode.checked.value) {
.background(
color = if (settings.useAmoledMode.checked.value == true) {
Color.Black
} else {
getAvgColor(
Util.avg(marks[subjectToShow]!!)
Util.avg(global.marks[global.subjectToShow]!!)
)
},
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.padding(vertical = 15.dp),
contentAlignment = Alignment.Center
) {
val currAvg = Util.avg(marks[subjectToShow]!!).replace(".", ",")
val currAvg = Util.avg(global.marks[global.subjectToShow]!!).replace(".", ",")
var textToShow = currAvg
if (currAvg.length == 3 && currAvg[2] == '0') {
textToShow = currAvg[0].toString()
@@ -350,7 +547,7 @@ fun MarksSubject() {
Spacer(modifier = Modifier.height(20.dp))
Text(
text = subjectNameMap[subjectToShow]!!,
text = global.subjectNameMap[global.subjectToShow]!!,
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 30.sp,
@@ -359,14 +556,15 @@ fun MarksSubject() {
.border(
width = 5.dp,
color = PastelYellow,
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.background(color = if (settings.useAmoledMode.checked.value) {
.background(
color = if (settings.useAmoledMode.checked.value == true) {
Color.Black
} else {
PastelYellowTransparent
},
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.padding(vertical = 20.dp, horizontal = 10.dp)
) // subject
@@ -374,7 +572,7 @@ fun MarksSubject() {
Spacer(modifier = Modifier.height(20.dp))
Column(modifier = Modifier.verticalScroll(scrollState)) {
for ((i, mark) in marks[subjectToShow]!!.withIndex()) {
for ((i, mark) in global.marks[global.subjectToShow]!!.withIndex()) {
if (i > 0) {
Spacer(modifier = Modifier.height(10.dp))
}
@@ -387,17 +585,18 @@ fun MarksSubject() {
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 5.dp)
.background(color = if (settings.useAmoledMode.checked.value) {
.background(
color = if (settings.useAmoledMode.checked.value == true) {
Color.Black
} else {
getAvgColor(mark.grade)
},
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.padding(vertical = 10.dp)
.clickable {
markToShow = mark
marksShow.value = MarksShow.Detail
global.markToShow = mark
global.marksDepthLevel = MarksDepthLevel.Detail
}
) {
Box(
@@ -426,20 +625,6 @@ fun MarksSubject() {
) // theme
}
}
// only add if mark date is not older than one week
if ((LocalDate
.parse(mark.date, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
.isAfter(LocalDate.now().minusWeeks(1))
) && settings.tagNewMarks.checked.value
) {
Icon(
modifier = Modifier.padding(start = 7.dp, top = 4.dp),
imageVector = Icons.Default.AutoAwesome,
contentDescription = "A new mark",
tint = Color.White // You can customize the icon color
)
}
}
}
}
@@ -449,9 +634,7 @@ fun MarksSubject() {
@Composable
fun MarksDetail() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler {
marksShow.value = MarksShow.Subject
}
BackHandler { global.marksDepthLevel = MarksDepthLevel.Subject }
val scrollState = rememberScrollState()
@@ -466,29 +649,7 @@ fun MarksDetail() {
Row(
modifier = Modifier.fillMaxWidth()
) {
Button(
onClick = {
marksShow.value = MarksShow.Subject
},
colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
containerColor = Color.Black
),
modifier = Modifier
.border(
width = borderWidthDimen,
shape = RoundedCornerShape(roundedCornerDimen),
color = PastelYellow
)
.width(70.dp)
.height(70.dp)
) {
Icon(
imageVector = Icons.Default.ArrowBackIosNew,
contentDescription = "Back icon",
modifier = Modifier.size(40.dp)
)
}
backButtonFactory.get(BackButtonType.MarksSubject)()
Box(modifier = Modifier.weight(1f))
@@ -497,19 +658,19 @@ fun MarksDetail() {
.width(70.dp)
.height(70.dp)
.border(
width = borderWidthDimen,
width = global.dimens.borderWidth,
color = getAvgColor(
Util.avg(mutableListOf(markToShow)),
Util.avg(mutableListOf(global.markToShow)),
true
),
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.background(
if (settings.useAmoledMode.checked.value) {
if (settings.useAmoledMode.checked.value == true) {
Color.Black
} else {
getAvgColor(
Util.avg(mutableListOf(markToShow))
Util.avg(mutableListOf(global.markToShow))
)
}
)
@@ -517,7 +678,7 @@ fun MarksDetail() {
contentAlignment = Alignment.Center
) {
Text(
text = markToShow.grade,
text = global.markToShow.grade,
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 25.sp
@@ -529,7 +690,7 @@ fun MarksDetail() {
Row {
Text(
text = markToShow.theme,
text = global.markToShow.theme,
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 30.sp,
@@ -538,9 +699,10 @@ fun MarksDetail() {
.border(
width = 5.dp,
color = PastelYellow,
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.background(if (settings.useAmoledMode.checked.value) {
.background(
if (settings.useAmoledMode.checked.value == true) {
Color.Black
} else {
PastelYellowTransparent
@@ -557,12 +719,12 @@ fun MarksDetail() {
.fillMaxWidth()
.padding(horizontal = 5.dp)
.background(
color = if (settings.useAmoledMode.checked.value) {
color = if (settings.useAmoledMode.checked.value == true) {
Color.Black
} else {
PastelYellowTransparent
},
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.padding(vertical = 20.dp, horizontal = 10.dp)
) {
@@ -586,7 +748,7 @@ fun MarksDetail() {
contentAlignment = Alignment.CenterEnd
) {
Text(
text = markToShow.weight.replace(".", ","),
text = global.markToShow.weight.replace(".", ","),
color = Color.White,
fontSize = 20.sp
)
@@ -600,12 +762,12 @@ fun MarksDetail() {
.fillMaxWidth()
.padding(horizontal = 5.dp)
.background(
color = if (settings.useAmoledMode.checked.value) {
color = if (settings.useAmoledMode.checked.value == true) {
Color.Black
} else {
PastelYellowTransparent
},
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
.padding(vertical = 20.dp, horizontal = 10.dp)
) {
@@ -628,7 +790,7 @@ fun MarksDetail() {
.weight(1f),
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 month = rawDate.split("-")[1]
val day = rawDate.split("-")[2]
@@ -1,14 +1,30 @@
package com.odweta.solon
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.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.automirrored.filled.ExitToApp
import androidx.compose.material.icons.filled.AccountBox
import androidx.compose.material.icons.filled.ArrowBackIosNew
import androidx.compose.material.icons.filled.ExitToApp
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Text
import androidx.compose.material3.Button
import androidx.compose.runtime.Composable
@@ -19,16 +35,19 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.odweta.solon.ui.theme.PastelYellow
import kotlin.system.exitProcess
@Composable
fun MenuScreen(modifier: Modifier = Modifier) {
var showDialog by remember { mutableStateOf(false) }
fun MenuScreen() {
val menuSeparatorDimen = 40.dp
Column(
modifier = Modifier
@@ -36,79 +55,50 @@ fun MenuScreen(modifier: Modifier = Modifier) {
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
MenuButton({ showDialog = true }, "Odhlásit se")
Spacer(modifier = Modifier.height(80.dp))
MenuButton({ screenToShow.value = Screen.SignIn }, "Změnit přihlášení")
Spacer(modifier = Modifier.height(80.dp))
MenuButton({ isShowing.value = IsShowing.About }, "O aplikaci")
Spacer(modifier = Modifier.height(80.dp))
MenuButton({ isShowing.value = IsShowing.Settings }, "Nastavení")
}
// Dialog implementation
if (showDialog) {
AlertDialog(
onDismissRequest = { showDialog = false }, // Close the dialog on dismiss
title = {
Text(text = "Odhlásit se", color = Color.White)
},
text = {
Text("Opravdu se chcete odhlásit? Tato akce zavře aplikaci.", color = Color.White)
},
containerColor = Color.Black,
modifier = Modifier
.border(width = 5.dp, color = PastelYellow, shape = RoundedCornerShape(
roundedCornerDimen)), // Yellow border
confirmButton = {
Button(
onClick = {
showDialog = false
Util.deleteFile(userDataFilePath)
Util.deleteFile(timeTableFilePath)
Util.deleteFile(marksFilePath)
Util.deleteFile(accountDataFilePath)
exitProcess(0)
},
colors = ButtonDefaults.buttonColors(
containerColor = Color.Black, // Black background for the button
contentColor = Color.White // Yellow text for the button
),
modifier = Modifier
.border(width = 5.dp, color = PastelYellow, shape = RoundedCornerShape(
roundedCornerDimen)) // Yellow border
) {
Text("Ano")
}
},
dismissButton = {
Button(
onClick = {
showDialog = false
},
colors = ButtonDefaults.buttonColors(
containerColor = Color.Black, // Black background for the button
contentColor = Color.White,
// Yellow text for the button
),
modifier = Modifier
.border(width = 5.dp, color = PastelYellow, shape = RoundedCornerShape(
roundedCornerDimen)) // Yellow border
) {
Text("Ne")
}
}
Row {
MenuButton(
{ global.currentScreenElement = ScreenElement.SignOut },
Icons.AutoMirrored.Filled.ExitToApp,
"Odhlásit se"
)
Spacer(modifier = Modifier.width(menuSeparatorDimen))
MenuButton(
{
global.noSignInBackHandle = false
global.currentScreen = Screen.SignIn
},
Icons.Default.AccountBox,
"Změnit přihlášení"
)
}
Spacer(modifier = Modifier.height(menuSeparatorDimen))
Row {
MenuButton(
{ global.currentScreenElement = ScreenElement.About },
Icons.Default.Info,
"O aplikaci"
)
Spacer(modifier = Modifier.width(menuSeparatorDimen))
MenuButton(
{ global.currentScreen = Screen.Settings },
Icons.Default.Settings,
"Nastavení"
)
}
}
}
@Composable
fun MenuButton(onClick: () -> Unit, text: String) {
fun MenuButton(onClick: () -> Unit, icon: ImageVector, altText: String) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(
@@ -117,16 +107,25 @@ fun MenuButton(onClick: () -> Unit, text: String) {
),
modifier = Modifier
.border(
width = 5.dp,
shape = RoundedCornerShape(roundedCornerDimen),
width = global.dimens.borderWidth,
shape = RoundedCornerShape(global.dimens.roundedCorner),
color = PastelYellow
)
.width(230.dp)
.height(80.dp)
.size(80.dp)
) {
Icon(
imageVector = icon,
contentDescription = altText,
modifier = Modifier.size(40.dp)
)
}
Text(
text = text,
fontSize = 23.sp
text = altText,
modifier = Modifier.padding(5.dp).width(80.dp),
color = Color.White,
fontSize = 17.sp,
textAlign = TextAlign.Center
)
}
}
@@ -0,0 +1,11 @@
package com.odweta.solon
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
@Composable
fun MessagesScreen() {
Column {
}
}
@@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
@@ -17,8 +16,6 @@ import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -26,7 +23,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.odweta.solon.ui.theme.PastelYellow
import kotlinx.coroutines.CoroutineScope
private var timetableColor: MutableState<Color> = mutableStateOf(PastelYellow)
private var marksColor: MutableState<Color> = mutableStateOf(Color.White)
@@ -34,21 +30,23 @@ private var menuColor: MutableState<Color> = mutableStateOf(Color.White)
var refreshButtonContentColor: MutableState<Color> = mutableStateOf(Color.White)
@Composable
fun NavbarScreen(modifier: Modifier = Modifier) {
fun NavbarScreen() {
val ctx = LocalContext.current
val coscope = rememberCoroutineScope()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
var spacerModifier = 30.dp
if (settings.showRefreshButton.checked.value) {
if (settings.showRefreshButton.checked.value == true) {
spacerModifier = 15.dp
Button(
onClick = {
fetchNewData(ctx, coscope)
if (Util.internetAvail(ctx)) {
remoteLoad(ctx)
statusFactory.show(StatusType.Loading)
refreshButtonContentColor.value = PastelYellow
}
},
colors = ButtonDefaults.buttonColors(
contentColor = refreshButtonContentColor.value,
@@ -67,10 +65,10 @@ fun NavbarScreen(modifier: Modifier = Modifier) {
Button(
onClick = {
isShowing.value = IsShowing.Timetable
timetableColor.value = PastelYellow
marksColor.value = Color.White
menuColor.value = Color.White
global.currentScreenElement = ScreenElement.Timetable
},
colors = ButtonDefaults.buttonColors(
contentColor = timetableColor.value,
@@ -88,11 +86,11 @@ fun NavbarScreen(modifier: Modifier = Modifier) {
Button(
onClick = {
isShowing.value = IsShowing.Marks
timetableColor.value = Color.White
marksColor.value = PastelYellow
menuColor.value = Color.White
marksShow.value = MarksShow.All
global.currentScreenElement = ScreenElement.Marks
global.marksDepthLevel = MarksDepthLevel.All
},
colors = ButtonDefaults.buttonColors(
contentColor = marksColor.value,
@@ -110,10 +108,10 @@ fun NavbarScreen(modifier: Modifier = Modifier) {
Button(
onClick = {
isShowing.value = IsShowing.Menu
timetableColor.value = Color.White
marksColor.value = Color.White
menuColor.value = PastelYellow
global.currentScreenElement = ScreenElement.Menu
},
colors = ButtonDefaults.buttonColors(
contentColor = menuColor.value,
@@ -1,183 +1,111 @@
package com.odweta.solon
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.Headers.Companion.headersOf
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONException
import okhttp3.Response
import org.json.JSONObject
import java.io.IOException
class Network {
companion object {
suspend fun getToken(): String {
return withContext(Dispatchers.IO) {
val request = Request.Builder()
.url(tokenUrl)
.header("Content-Type", "application/x-www-form-urlencoded")
.post(
FormBody.Builder()
fun setupAPI() {
global.token = "NETWORK_UNREACHABLE"
val tokenJSON = networkRequestFactory.perform(NetworkRequestType.Token)
if (tokenJSON.has("token")) {
val token = tokenJSON.getString("token")
if ("\"error\": " !in token) {
global.token = token
}
}
if (global.token == "NETWORK_UNREACHABLE" || "\"error\":" in global.token) {
global.userData = JSONObject()
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("username", username)
.add("password", password)
.add("username", global.username)
.add("password", global.password)
.add("client_id", "test_client")
.add("scope", "openid offline_access profile sol_api")
.build()
)
.build()
try {
val resp = client.newCall(request).execute()
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)
studentClass = if (!Parsing.userIsParent()) {
JSONObject(
Util.fileToString(userDataFilePath)
).getJSONObject("class").get("name").toString()
return if (response.code == 200) {
JSONObject(mapOf("token" to JSONObject(response.body?.string().toString()).getString("access_token")))
} else {
// if the user is a parent, the class will be of their first child
JSONObject(
Util.fileToString(userDataFilePath)
).getJSONArray("children").getJSONObject(0).getString("className")
responseError
}
studentId = getStudentId()
//syid = getSYID()
syid = ""
marksUrl = "$apiBaseUrl/v1/students/$studentId/marks/list"//?SemesterId=${syid}"
isAPISetUp = true
}
NetworkRequestType.Timetable -> return getAPI(global.apiEndPoints.timetable)
NetworkRequestType.Marks -> return getAPI(global.apiEndPoints.marks)
NetworkRequestType.UserData -> return getAPI(global.apiEndPoints.userData)
}
}
/*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 {
syidRoot = JSONObject(f)
.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
}
return syidStart.toString() + syidNum.toString()
}*/
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 {
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"
}
private fun get(url: String, headers: Headers): Response {
val request = Request.Builder()
.url(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Bearer $token")
.headers(headers)
.get()
.build()
try {
val resp = client.newCall(request).execute()
resp.body?.string().toString()
} catch (e: IOException) {
"""{"error": "network"}"""
}
}
return client.newCall(request).execute()
}
suspend fun getMarksJSON(): String {
return withContext(Dispatchers.IO) {
private fun post(url: String, headers: Headers, body: FormBody): Response {
val request = Request.Builder()
.url(marksUrl)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Bearer $token")
.get()
.url(url)
.headers(headers)
.post(body)
.build()
try {
val resp = client.newCall(request).execute()
resp.body?.string().toString()
} catch (e: IOException) {
"""{"error": "network"}"""
return client.newCall(request).execute()
}
}
private fun getAPI(url: String): JSONObject {
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
import android.util.Log
import org.json.JSONObject
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter
class Parsing {
@@ -44,28 +47,26 @@ class Parsing {
}
fun userIsParent(): Boolean {
return if (userData != "")
JSONObject(userData).getString("userType") == "parent"
return if (global.userData != JSONObject())
global.userData.getString("userType") == "parent"
else false
}
fun parseTimeTableJSON(jsonString: String): List<Day> {
val jsonObject = JSONObject(jsonString)
fun parseTimetable(json: JSONObject): List<Day> {
/*
* return a List that looks like this:
* [<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" ]]
*/
val week = mutableListOf<Day>()
val timetable = mutableListOf<Day>()
if (jsonObject.has("error")) {
return week
if (json.has("error")) {
return timetable
}
// 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
val jsonObjectList = mutableListOf<JSONObject>()
@@ -84,8 +85,12 @@ class Parsing {
.split("-")
// remove leading zeros
while (dayStr[0] == '0') { dayStr = dayStr.slice(1..<dayStr.length) }
while (monthStr[0] == '0') { monthStr = monthStr.slice(1..<monthStr.length) }
while (dayStr[0] == '0') {
dayStr = dayStr.slice(1..<dayStr.length)
}
while (monthStr[0] == '0') {
monthStr = monthStr.slice(1..<monthStr.length)
}
// format it into a string that is readable
val date = "$dayStr. $monthStr."
@@ -126,35 +131,37 @@ class Parsing {
if (type != "SUPLOVANI" && type != "ROZVRH") {
when (kind) {
"ZRUSENI_VYUKY_ROZVRH" -> {
subject.name = subj.get("title").toString()
subject.name = subj.getString("title")
subject.place = ""
subject.teacher = ""
subject.substitute = "2" // 2 means school event
val hourspan = subj.getJSONArray("detailHours").length()
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.end = subj.get("endTime").toString()
subject.end = subj.getString("endTime")
subject.end = subject.end.slice(0..subject.end.length - 4)
subjects.add(subject)
}
"S_NAHRADOU_SKOL_AKCE" -> eventLecture(subject, subj, subjects)
"S_NAHRADOU_VYUKA" -> eventLecture(subject, subj, subjects)
}
} else {
subject.name = subj.getJSONObject("subject").get("abbrev").toString()
subject.name = subj.getJSONObject("subject").getString("abbrev")
subject.place =
subj.getJSONArray("rooms").getJSONObject(0).get("abbrev").toString()
subject.teacher = "" +
"${
subj.getJSONArray("teachers").getJSONObject(0).get("name")
} " +
"${
subj.getJSONArray("teachers").getJSONObject(0)
.get("surname")
}"
subj.getJSONArray("rooms").getJSONObject(0).getString("abbrev")
subject.teacher =
if (subj.getJSONArray("teachers").length() > 0) {
subj.getJSONArray("teachers")
.getJSONObject(0).getString("name") + " " +
subj.getJSONArray("teachers")
.getJSONObject(0).getString("surname")
} else {
""
}
subject.substitute = when (type) {
"ROZVRH" -> "0"
"SUPLOVANI" -> "1"
@@ -192,15 +199,13 @@ class Parsing {
val detailHours = subj.getJSONArray("detailHours")
newSubject.number =
detailHours.getJSONObject(i).get("id").toString()
detailHours.getJSONObject(i).getString("id")
newSubject.start =
detailHours.getJSONObject(i).get("timeFrom")
.toString()
detailHours.getJSONObject(i).getString("timeFrom")
newSubject.start =
newSubject.start.slice(0..newSubject.start.length - 4)
newSubject.end =
detailHours.getJSONObject(i).get("timeto")
.toString()
detailHours.getJSONObject(i).getString("timeto")
newSubject.end =
newSubject.end.slice(0..newSubject.end.length - 4)
subjects.add(newSubject)
@@ -210,7 +215,7 @@ class Parsing {
}
val sCopy = subjects.toList()
for (s in 0 until sCopy.size) {
for (s in sCopy.indices) {
if ("-" in subjects[s].number) {
subjects[s].number = "1"
subjects[s].start = "08:00"
@@ -219,26 +224,6 @@ class Parsing {
}
}
/*
// rust pseudocode
let mut res: Vec<i32> = vec![];
for i in 0..v.len()-1 {
if v[i] == v[i+1]-1 {
res.push(v[i]);
} else {
res.push(v[i]);
while res[res.len()-1]+1 != v[i+1] {
res.push(res[res.len()-1]+1);
}
}
}
res.push(v[v.len()-1]);
res
*/
if (subjects.size > 1) {
for (i in 0..<subjects.size - 1) {
if (subjects[i].number == ((subjects[i + 1].number.toInt() - 1).toString())) {
@@ -309,20 +294,20 @@ class Parsing {
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
day.subjects = nextSubjects
week.add(day)
timetable.add(day)
}
return week
return timetable
}
fun parseMarksJSON(jsonString: String): MutableMap<String, MutableList<Mark>> {
fun parseMarks(json: JSONObject): MutableMap<String, MutableList<Mark>> {
// no marks
val json = JSONObject(jsonString)
if (json.has("marks") && json.getJSONArray("marks").length() == 0) {
val noMarks = mutableMapOf<String, MutableList<Mark>>()
val list = mutableListOf<Mark>()
@@ -336,9 +321,9 @@ class Parsing {
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>()
for (i in 0..<subjectsJSONArray.length()) {
@@ -346,7 +331,7 @@ class Parsing {
subjectMap[subj.getString("id")] = subj.getString("abbrev")
// 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()) {
@@ -357,16 +342,6 @@ class Parsing {
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) {
val mark = Mark()
mark.grade = markJSON.getString("markText")
@@ -379,5 +354,90 @@ class Parsing {
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))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
marksList.add(marksJSONArray.getJSONObject(i))
}
for (subj in subjectMap.values) {
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")
}
}
}
}
@@ -1,9 +1,8 @@
package com.odweta.solon
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -11,35 +10,43 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBackIosNew
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchColors
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.currentRecomposeScope
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.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 androidx.lifecycle.liveData
import com.odweta.solon.ui.theme.PastelYellow
import kotlinx.coroutines.launch
@Composable
fun SettingsScreen(modifier: Modifier = Modifier) {
fun SettingsScreen() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler {
isShowing.value = IsShowing.Menu
global.currentScreenElement = ScreenElement.Menu
global.currentScreen = Screen.Main
}
// load the settings dat every time the settings screen is opened,
// maybe i can add a flag that determines if the settings are up to date?
settings = Util.fileToString(global.filePaths.settings).toSettingsData()
val scrollState = rememberScrollState()
Column(modifier = Modifier
@@ -47,60 +54,26 @@ fun SettingsScreen(modifier: Modifier = Modifier) {
.background(color = Color.Black)
.verticalScroll(scrollState)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(start = 5.dp)
) {
Button(
onClick = {
isShowing.value = IsShowing.Menu
},
colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
containerColor = Color.Black
),
modifier = Modifier
.border(
width = 5.dp,
shape = RoundedCornerShape(roundedCornerDimen),
color = PastelYellow
)
.width(70.dp)
.height(70.dp)
) {
Icon(
imageVector = Icons.Default.ArrowBackIosNew,
contentDescription = "Back icon",
modifier = Modifier.size(40.dp)
)
}
} // back button
Spacer(Modifier.height(10.dp))
backButtonFactory.get(BackButtonType.Menu)()
SettingsTableRow(settings.useSvobodaMode)
Spacer(Modifier.height(10.dp))
SettingsTableRow(settings.useAmoledMode)
Spacer(Modifier.height(10.dp))
SettingsTableRow(settings.showRefreshButton)
Spacer(Modifier.height(10.dp))
SettingsTableRow(settings.tagNewMarks)
SettingsTableRow(settings.useAnimations)
}
}
@Composable
fun SettingsTableRow(setting: Setting) {
Spacer(Modifier.height(10.dp))
Row(
modifier = Modifier
.padding(10.dp)
.background(color = Color.Black, shape = RoundedCornerShape(roundedCornerDimen))
.background(color = Color.Black, shape = RoundedCornerShape(global.dimens.roundedCorner))
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
@@ -114,17 +87,11 @@ fun SettingsTableRow(setting: Setting) {
Spacer(modifier = Modifier.weight(1f))
Switch(
checked = setting.checked.value,
checked = setting.checked.value == true,
onCheckedChange = {
setting.checked.value = it
when (setting.id) {
SettingsId.Svoboda -> settings.useSvobodaMode = setting
SettingsId.Amoled -> settings.useAmoledMode = setting
SettingsId.RefreshButton -> settings.showRefreshButton = setting
SettingsId.TagNewMarks -> settings.tagNewMarks = setting
else -> {}
}
Util.saveStringToFile(settingsDataFilePath, settings.toString())
showTempScreen(Screen.Settings)
Util.saveStringToFile(global.filePaths.settings, settings.toString())
},
colors = SwitchColors(
uncheckedThumbColor = Color.Black,
@@ -147,3 +114,33 @@ fun SettingsTableRow(setting: Setting) {
)
}
}
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("#")
val useAnimations = settings[3].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()
auxSettingsData.useAnimations.id = SettingsId.Animations
auxSettingsData.useAnimations.name = useAnimations[1]
auxSettingsData.useAnimations.checked.value = useAnimations[2].toBoolean()
return auxSettingsData
}
@@ -1,6 +1,7 @@
package com.odweta.solon
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
@@ -39,20 +40,22 @@ import kotlinx.coroutines.launch
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
private var buttonText: MutableState<String> = mutableStateOf("Přihlásit se")
private var buttonText by mutableStateOf("Přihlásit se")
private fun changeButtonText(s: String) {
buttonText.value = s
buttonText = s
}
fun signInButtonCallback(ctx: Context, coscope: CoroutineScope, buttonText: String, usernameText: String, passwordText: String) {
username = usernameText
password = passwordText
val acc = Account(username, password, 0)
fun signInButtonCallback(ctx: Context, usernameText: String, passwordText: String) {
global.username = usernameText
global.password = passwordText
val acc = Account(global.username, global.password, 0)
var checked = false
coscope.launch {
CoroutineScope(Dispatchers.IO).launch {
while (!checked) {
delay(300)
changeButtonText("Přihlašování")
@@ -85,18 +88,17 @@ fun signInButtonCallback(ctx: Context, coscope: CoroutineScope, buttonText: Stri
changeButtonText("Přihlásit se")
}
coscope.launch {
signedInAndReadyToLaunch.value = false
CoroutineScope(Dispatchers.IO).launch {
val valid = Account.isValid(acc)
checked = true
if (valid) {
Account.save(acc)
signedInAndReadyToLaunch.value = true
screenToShow.value = Screen.Splash
fetchNewData(ctx, coscope)
global.currentScreen = Screen.Splash
launchStart(ctx)
} else {
withContext(Dispatchers.Main) {
Toast.makeText(
ctx,
"Neplatné přihlašovací údaje nebo nejste připojeni k internetu, zkuste to znovu.",
@@ -105,18 +107,18 @@ fun signInButtonCallback(ctx: Context, coscope: CoroutineScope, buttonText: Stri
}
}
}
}
@Composable
fun SignInScreen() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler {
screenToShow.value = Screen.Main
if (!global.noSignInBackHandle) {
BackHandler { global.currentScreen = Screen.Main }
}
var usernameText by remember { mutableStateOf("") }
var passwordText by remember { mutableStateOf("") }
val ctx = LocalContext.current
val coscope = rememberCoroutineScope()
var isPasswordVisible by remember { mutableStateOf(false) } // State for visibility toggle
val visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation()
@@ -145,7 +147,7 @@ fun SignInScreen() {
modifier = Modifier
.border(
width = 5.dp, color = PastelYellow, shape = RoundedCornerShape(
roundedCornerDimen
global.dimens.roundedCorner
)
)
.width(230.dp),
@@ -172,7 +174,7 @@ fun SignInScreen() {
width = 5.dp,
color = PastelYellow,
shape = RoundedCornerShape(
roundedCornerDimen
global.dimens.roundedCorner
)
),
colors = TextFieldDefaults.colors(
@@ -200,7 +202,7 @@ fun SignInScreen() {
Button(
onClick = {
signInButtonCallback(ctx, coscope, buttonText.value, usernameText, passwordText)
signInButtonCallback(ctx, usernameText, passwordText)
},
colors = ButtonDefaults.buttonColors(
contentColor = Color.White,
@@ -209,13 +211,13 @@ fun SignInScreen() {
modifier = Modifier
.border(
width = 5.dp,
shape = RoundedCornerShape(roundedCornerDimen),
shape = RoundedCornerShape(global.dimens.roundedCorner),
color = PastelYellow
)
.width(230.dp)
) {
Text(
text = buttonText.value,
text = buttonText,
fontSize = 23.sp
)
}
@@ -0,0 +1,107 @@
package com.odweta.solon
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.odweta.solon.ui.theme.PastelYellow
import kotlin.system.exitProcess
@Composable
fun SignOutScreen() {
// handle the back gesture (back swipe form side or back button on bottom android navbar)
BackHandler { global.currentScreenElement = ScreenElement.Menu }
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start
) {
backButtonFactory.get(BackButtonType.Menu)()
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Opravdu se chcete odhlásit? Tato akce zavře aplikaci.",
color = Color.White,
textAlign = TextAlign.Center,
fontSize = 27.sp,
lineHeight = 40.sp,
modifier = Modifier.padding(bottom = 10.dp).padding(horizontal = 10.dp)
)
Row {
SignOutButton(
onClick = {
Util.deleteFile(global.filePaths.userData)
Util.deleteFile(global.filePaths.timetable)
Util.deleteFile(global.filePaths.marks)
Util.deleteFile(global.filePaths.accountData)
Util.deleteFile(global.filePaths.lectureNotes)
// do not delete settings
exitProcess(0)
},
label = "Ano"
)
Spacer(Modifier.width(10.dp))
SignOutButton(
onClick = {
global.currentScreenElement = ScreenElement.Menu
},
label = "Ne"
)
}
}
}
}
@Composable
private fun SignOutButton(onClick: () -> Unit, label: String) {
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Black, // Black background for the button
contentColor = Color.White // Yellow text for the button
),
modifier = Modifier
.border(
width = global.dimens.borderWidth,
color = PastelYellow,
shape = RoundedCornerShape(global.dimens.roundedCorner)
) // Yellow border
) {
Text(label, fontSize = 17.sp)
}
}
@@ -40,12 +40,7 @@ fun SplashScreen() {
AndroidView(
modifier = Modifier.size(200.dp),
factory = { context: Context ->
ImageView(context).apply {
// Set a placeholder if needed
// setImageResource(R.drawable.placeholder)
}
},
factory = { context: Context -> ImageView(context) },
update = { imageView ->
Glide.with(imageView.context)
.load(R.drawable.loading)
@@ -0,0 +1,22 @@
package com.odweta.solon
import androidx.compose.runtime.Composable
// temporary screens to artificially trigger recompositions basically at the root level
@Composable
fun TempScreen() {
when (global.tempScreen) {
Screen.Main -> MainScreen()
Screen.Splash -> SplashScreen()
Screen.SignIn -> SignInScreen()
Screen.Settings -> SettingsScreen()
Screen.Temp -> {}
}
}
fun showTempScreen(screen: Screen) {
global.returnToTempScreen = true
global.tempScreen = screen
global.currentScreen = Screen.Temp
}
@@ -1,6 +1,7 @@
package com.odweta.solon
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -48,6 +49,7 @@ import androidx.compose.ui.focus.focusModifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@@ -66,22 +68,20 @@ import com.odweta.solon.ui.theme.PastelYellowTransparent
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter
var showDetailDialog = mutableStateOf(false)
var showDetailDialog by mutableStateOf(false)
@Composable
fun TimetableScreen() {
Box(
modifier = Modifier
.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomCenter
) {
DayPager()
if (showDetailDialog.value) {
DetailDialog()
}
if (showDetailDialog) DetailDialog()
}
}
@@ -90,7 +90,7 @@ fun TimetableScreen() {
@Composable
fun DetailDialog() {
Dialog(
onDismissRequest = { showDetailDialog.value = false }, // Close the dialog on dismiss
onDismissRequest = { showDetailDialog = false }, // Close the dialog on dismiss
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
@@ -102,7 +102,7 @@ fun DetailDialog() {
.padding(horizontal = 20.dp)
.background(
color = Color.Black,
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
) {
Column(
@@ -110,9 +110,9 @@ fun DetailDialog() {
) {
Text(
text = "" +
lectureNotes[selectedLecture]?.number.toString() +
global.lectureNotes[global.selectedLecture]?.number.toString() +
" " +
lectureNotes[selectedLecture]?.lectureName.toString(),
global.lectureNotes[global.selectedLecture]?.lectureName.toString(),
color = Color.White,
textAlign = TextAlign.Left,
modifier = Modifier.padding(top = 10.dp)
@@ -125,7 +125,7 @@ fun DetailDialog() {
Button(
modifier = Modifier.background(
color = Color.Black,
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
),
onClick = { isExpanded = true },
colors = ButtonColors(
@@ -135,7 +135,7 @@ fun DetailDialog() {
disabledContentColor = Color.White
)
) {
val type = when (lectureNotes[selectedLecture]?.type?.value) {
val type = when (global.lectureNotes[global.selectedLecture]?.type?.value) {
LectureNoteType.Normal -> "Normální"
LectureNoteType.Writing -> "Písemný test"
LectureNoteType.Speaking -> "Ústní zkoušení"
@@ -160,7 +160,7 @@ fun DetailDialog() {
enabled = true,
onClick = {
isExpanded = false
lectureNotes[selectedLecture]?.type?.value = LectureNoteType.Normal
global.lectureNotes[global.selectedLecture]?.type?.value = LectureNoteType.Normal
Util.saveLectureNotes()
},
text = { Text("Normální poznámka", color = Color.White) }
@@ -170,7 +170,7 @@ fun DetailDialog() {
enabled = true,
onClick = {
isExpanded = false
lectureNotes[selectedLecture]?.type?.value = LectureNoteType.Writing
global.lectureNotes[global.selectedLecture]?.type?.value = LectureNoteType.Writing
Util.saveLectureNotes()
},
text = { Text("Písemný test", color = Color.White) }
@@ -180,7 +180,7 @@ fun DetailDialog() {
enabled = true,
onClick = {
isExpanded = false
lectureNotes[selectedLecture]?.type?.value = LectureNoteType.Speaking
global.lectureNotes[global.selectedLecture]?.type?.value = LectureNoteType.Speaking
Util.saveLectureNotes()
},
text = { Text("Ústní zkoušení", color = Color.White) }
@@ -190,7 +190,7 @@ fun DetailDialog() {
enabled = true,
onClick = {
isExpanded = false
lectureNotes[selectedLecture]?.type?.value = LectureNoteType.Other
global.lectureNotes[global.selectedLecture]?.type?.value = LectureNoteType.Other
Util.saveLectureNotes()
},
text = { Text("Jiné", color = Color.White) }
@@ -200,9 +200,9 @@ fun DetailDialog() {
////////////////////////////////////////////////
TextField(
value = lectureNotes[selectedLecture]?.note?.value ?: "",
value = global.lectureNotes[global.selectedLecture]?.note?.value ?: "",
onValueChange = { newText: String ->
lectureNotes[selectedLecture]?.note?.value = newText
global.lectureNotes[global.selectedLecture]?.note?.value = newText
Util.saveLectureNotes()
},
singleLine = true,
@@ -228,12 +228,12 @@ fun DetailDialog() {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DayPager() {
val pagerState = rememberPagerState(pageCount = { days.size })
val pagerState = rememberPagerState(pageCount = { if (global.timetable.isNotEmpty()) global.timetable.size else 6 }) // FIXME
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize()
) { pageIndex ->
DayScreen(days[pageIndex])
DayScreen(global.timetable[pageIndex])
}
}
@@ -258,8 +258,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
return givenTime.isAfter(startTime) && givenTime.isBefore(endTime)
}
@@ -286,18 +290,18 @@ fun subjectModifier(d: Day, s: Subject, lectureNumber: Int): Modifier {
val todayDate = LocalDate.now()
val now = LocalTime.now()
if (dateParsed == todayDate && isTimeBetween(now, startTimeParsed, endTimeParsed)) {
currentLectureNumber = lectureNumber
if (dateParsed == todayDate && isTimeBetweenT(now, startTimeParsed, endTimeParsed)) {
global.currentLectureNumber = lectureNumber
}
val m = if (settings.useAmoledMode.checked.value) {
val m = if (settings.useAmoledMode.checked.value == true) {
Modifier
.fillMaxWidth()
.height(70.dp)
.border(
width = borderWidthDimen,
width = global.dimens.borderWidth,
color = getSubjectColor(s, false), // decide on the color (transparent)
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
} else {
Modifier
@@ -305,15 +309,15 @@ fun subjectModifier(d: Day, s: Subject, lectureNumber: Int): Modifier {
.height(70.dp)
.background(
color = getSubjectColor(s, true), // decide on the color (transparent)
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
}
return if (lectureNumber == currentLectureNumber && dateParsed == todayDate) {
return if (lectureNumber == global.currentLectureNumber && dateParsed == todayDate) {
m.border(
width = borderWidthDimen,
width = global.dimens.borderWidth,
color = getSubjectColor(s, false),
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
} else {
m
@@ -353,18 +357,20 @@ private fun DayScreen(day: Day) {
) {
for ((subjectIndex, subject) in day.subjects.withIndex()) {
val lectureId = Util.getLectureId(day.date, subjectIndex)
global.lectureNotes[lectureId]?.number = subject.number.toInt()
global.lectureNotes[lectureId]?.lectureName = subject.name
val amoledModifier = if (settings.useAmoledMode.checked.value) {
val amoledModifier = if (settings.useAmoledMode.checked.value == true) {
Modifier
.width(50.dp)
.fillMaxHeight()
.border(
width = borderWidthDimen,
width = global.dimens.borderWidth,
color = getSubjectColor(
subject,
false
),
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
} else {
Modifier
@@ -375,7 +381,7 @@ private fun DayScreen(day: Day) {
subject,
true
),
shape = RoundedCornerShape(roundedCornerDimen)
shape = RoundedCornerShape(global.dimens.roundedCorner)
)
}
@@ -435,8 +441,9 @@ private fun DayScreen(day: Day) {
Text(
text = " supl ",
color = Color.Black,
modifier = Modifier.background(color = Color.White)
color = Color.White,
fontStyle = FontStyle.Italic,
modifier = Modifier.background(color = PastelRedTransparent)
)
}
} else {
@@ -479,10 +486,9 @@ private fun DayScreen(day: Day) {
.padding(vertical = 5.dp),
contentAlignment = Alignment.Center
) {
if (settings.useSvobodaMode.checked.value) {
if (settings.useSvobodaMode.checked.value == true) {
if (
subject.teacher == "Jindřich Svoboda" &&
studentClass == "5. G"
subject.teacher == "Jindřich Svoboda"
) {
Image(
painter = painterResource(R.drawable.spock),
@@ -529,14 +535,20 @@ private fun DayScreen(day: Day) {
.fillMaxSize()
.zIndex(99f)
.clickable {
selectedLecture = lectureId
showDetailDialog.value = true
global.selectedLecture = lectureId
showDetailDialog = true
})
if (
(lectureNotes[lectureId] != null && lectureNotes[lectureId]?.note?.value != "") ||
(lectureNotes[lectureId] != null && lectureNotes[lectureId]?.type?.value != LectureNoteType.Normal) &&
(lectureNotes[lectureId] != null && lectureNotes[lectureId]?.type?.value != LectureNoteType.Other)
(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),
@@ -1,46 +1,134 @@
package com.odweta.solon
import android.content.Context
import android.util.Log
import android.widget.ImageView
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Upload
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import com.bumptech.glide.Glide
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.json.JSONObject
private var showLoadingIcon by mutableStateOf(false)
@Composable
fun ToolbarScreen() {
// make the font size adaptive relative to the length of the user's name
var toolbarTextSizeAdaptive by remember { mutableStateOf(25.sp) }
if (toolbarText.value.length > 28) {
if (global.status.length > 28) {
toolbarTextSizeAdaptive = 20.sp
}
if (toolbarText.value.length > 32) {
if (global.status.length > 32) {
toolbarTextSizeAdaptive = 16.sp
}
if (toolbarText.value.length > 40) {
if (global.status.length > 40) {
toolbarTextSizeAdaptive = 12.sp
}
if (toolbarText.value.length > 45) {
if (global.status.length > 45) {
toolbarTextSizeAdaptive = 10.sp
}
Box(modifier = Modifier.padding(10.dp)) {
Animate(global.status) { targetText ->
Row(modifier = Modifier.padding(10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
Text(
text = toolbarText.value,
text = targetText,
fontSize = toolbarTextSizeAdaptive,
color = Color.White
)
if (settings.useAnimations.checked.value == true) {
AnimatedVisibility(showLoadingIcon) {
AndroidView(
modifier = Modifier.size(50.dp).padding(start = 10.dp),
factory = { context: Context -> ImageView(context) },
update = { imageView ->
Glide.with(imageView.context)
.load(R.drawable.loading)
.into(imageView)
}
)
}
}
}
}
}
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
showStatus("Načítání nových dat")
delay(100L)
showLoadingIcon = true
while (!global.refreshed) {
showStatus("Načítání nových dat")
}
showLoadingIcon = false
}
}
}
}
}
val statusFactory = StatusFactory
fun showStatus(text: String) {
global.status = text
}
+14 -114
View File
@@ -5,8 +5,6 @@ import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.util.DisplayMetrics
import android.view.WindowManager
import androidx.compose.runtime.mutableStateOf
import androidx.core.content.pm.PackageInfoCompat
import java.io.File
@@ -17,26 +15,9 @@ import kotlin.math.abs
class Util {
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 {
// 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 (!f.exists()) {
@@ -58,7 +39,7 @@ class Util {
}
fun fileToString(filePath: String): String {
val f = File(fileDir, filePath)
val f = File(global.fileDir, filePath)
return if (f.exists()) {
if (f.canRead()) {
@@ -76,12 +57,12 @@ class Util {
}
fun fileExists(filePath: String): Boolean {
val f = File(fileDir, filePath)
val f = File(global.fileDir, filePath)
return f.exists()
}
fun fileIsEmpty(filePath: String): Boolean {
val f = File(fileDir, filePath)
val f = File(global.fileDir, filePath)
val content = f.readText()
return content == ""
}
@@ -101,7 +82,7 @@ class Util {
}
fun deleteFile(path: String): Boolean {
val f = File(fileDir, path)
val f = File(global.fileDir, path)
if (f.exists()) {
try {
@@ -115,21 +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 avg(marks: MutableList<Mark>): String {
var sum = 0f
var len = 0
@@ -151,7 +117,6 @@ class Util {
}
return "${Math.round(sum / sum(weights) * 100f) / 100f}".replace(",", ".")
//return String.format("%.2f", (sum / sum(weights))).replace(',', '.')
}
private fun sum(nums: List<Float>): Float {
@@ -163,49 +128,6 @@ class Util {
return sum
}
/*fun getAvgBgColor(ctx: Context, avg: String): Color {
var color: Color = PastelOrangeTransparent
if (avg.toFloat() in 1f..<2f) {
color = PastelGreenTransparent
} else if (avg.toFloat() in 2f..<3f) {
color = PastelYellowTransparent
} else if (avg.toFloat() in 3f..<4f) {
color = PastelOrangeTransparent
} else if (avg.toFloat() in 4f..5f) {
color = PastelRedTransparent
}
return color
}
fun getAvgStrokeColor(ctx: Context, avg: String): Int {
var color: Int = R.color.pastel_orange
if (avg.toFloat() in 1f..<2f) {
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(
ctx,
color
)
}
fun getAvgDrawable(ctx: Context, avg: String): Drawable {
val d = GradientDrawable()
d.setColor(getAvgBgColor(ctx, avg))
d.setStroke(10, getAvgStrokeColor(ctx, avg))
d.cornerRadius = 8f
return d
}*/
data class AppVersion(
val versionName: String,
val versionNumber: Long,
@@ -234,28 +156,6 @@ class Util {
}
}
fun stringToSettingsData(s: String): SettingsData {
val settings = s.split("\n")
val auxSettingsData = SettingsData()
val useSvobodaMode = settings[0].split("#")
val useAmoledMode = settings[1].split("#")
val showRefreshButton = settings[2].split("#")
auxSettingsData.useSvobodaMode.id = SettingsId.Svoboda
auxSettingsData.useSvobodaMode.name = useSvobodaMode[1]
auxSettingsData.useSvobodaMode.checked.value = useSvobodaMode[2].toBoolean()
auxSettingsData.useAmoledMode.id = SettingsId.Amoled
auxSettingsData.useAmoledMode.name = useAmoledMode[1]
auxSettingsData.useAmoledMode.checked.value = useAmoledMode[2].toBoolean()
auxSettingsData.showRefreshButton.id = SettingsId.RefreshButton
auxSettingsData.showRefreshButton.name = showRefreshButton[1]
auxSettingsData.showRefreshButton.checked.value = showRefreshButton[2].toBoolean()
return auxSettingsData
}
fun getLectureId(date: String, subjectIndex: Int): Int {
val concat = "$date[$subjectIndex]"
val bytes = concat.toByteArray()
@@ -265,8 +165,8 @@ class Util {
})
}
private fun csvToLectureNotes() {
val lines = fileToString(lectureNotesFilePath).lines()
fun csvToLectureNotes() {
val lines = fileToString(global.filePaths.lectureNotes).lines()
for (line in lines) {
if (line.isBlank()) continue
@@ -280,13 +180,13 @@ class Util {
note += ",${parts[part]}"
}
lectureNotes[id]?.note = mutableStateOf(note)
lectureNotes[id]?.type = mutableStateOf(type)
global.lectureNotes[id]?.note = mutableStateOf(note)
global.lectureNotes[id]?.type = mutableStateOf(type)
}
}
}
private fun lectureNotesToCsvString(hashMap: HashMap<Int, LectureNote>): String {
fun lectureNotesToCsvString(hashMap: HashMap<Int, LectureNote>): String {
var result = ""
for ((key, lectureNote) in hashMap) {
result +=
@@ -298,10 +198,10 @@ class Util {
}
fun loadLectureNotes() {
for (day in days) {
for (day in global.timetable) {
for ((subjectIndex, subject) in day.subjects.withIndex()) {
val id = getLectureId(day.date, subjectIndex)
lectureNotes[id] = LectureNote(
global.lectureNotes[id] = LectureNote(
id = id,
type = mutableStateOf(LectureNoteType.Normal),
note = mutableStateOf(""),
@@ -315,8 +215,8 @@ class Util {
}
fun saveLectureNotes() {
val toSave = lectureNotesToCsvString(lectureNotes)
saveStringToFile(lectureNotesFilePath, toSave)
val toSave = lectureNotesToCsvString(global.lectureNotes)
saveStringToFile(global.filePaths.lectureNotes, toSave)
}
}
}
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
cp app/build/outputs/apk/release/com.odweta.solon_$1.apk ../release/