Compare commits

..

2 Commits

Author SHA1 Message Date
Faierbel
7a85d13812 Merge branch 'develop' into feature/update-colors 2024-03-13 19:20:39 +01:00
Faierbel
b978abfcbe Update primary colors 2024-02-19 22:33:15 +01:00
218 changed files with 1268 additions and 13558 deletions

View File

@ -1,7 +1,7 @@
[*]
charset=utf-8
end_of_line=lf
insert_final_newline=Advanced
insert_final_newline=true
indent_style=space
indent_size=4

View File

@ -1,18 +0,0 @@
changelog:
exclude:
labels:
- "release ignore"
categories:
- title: breaking changes
labels:
- major
- title: new features
labels:
- minor
- fr:approved
- title: translation updates
labels:
- translation
- title: features
labels:
- "*"

View File

@ -1,84 +0,0 @@
name: Generate APK
env:
main_project_module: app
on:
pull_request:
types:
- closed
jobs:
build:
runs-on: ubuntu-latest
env:
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout the repository
uses: actions/checkout@v3
- name: Set current date as env variable
run: echo "date_today=$(date +'%Y-%m-%d')" >> $GITHUB_ENV
- name: Set repository name as env variable
run: echo "repository_name=$(echo '${{ gitea.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_ENV
- name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
cache: 'gradle'
- name: Set up Go environment
uses: actions/setup-go@v3
with:
go-version: '1.22'
- name: Get hash of Gradle files
uses: https://gitea.com/actions/go-hashfiles@v0.0.1
id: get-hash
with:
patterns: |-
**/*.gradle*
- name: Cache Gradle
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ steps.get-hash.outputs.hash }}
- name: Get app version
id: get_version
run: echo "VERSION_NAME=$(grep -m1 "versionName" app/build.gradle | awk '{print $2}' | tr -d \'\'\"\')" >> $GITHUB_ENV
- name: Change wrapper permissions
run: chmod +x ./gradlew
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Build debug APK
run: ./gradlew assembleDebug
- name: Upload artifacts
uses: actions/upload-artifact@v3 # not v4 because of GHES
with:
name: wulkanowy_mod_debug_builds
path: |
app/build/outputs/**/*-debug.apk
- name: Create release
uses: akkuman/gitea-release-action@v1
env:
NODE_OPTIONS: '--experimental-fetch'
with:
files: |
app/build/outputs/**/*-debug.apk
name: Release ${{ env.VERSION_NAME }} (${{ env.date_today }})
tag_name: v${{ env.VERSION_NAME }}

79
.github/workflows/deploy-store.yml vendored Normal file
View File

@ -0,0 +1,79 @@
name: Deploy release
on:
release:
types: [ created ]
jobs:
deploy-google-play:
name: Google Play
runs-on: ubuntu-latest
timeout-minutes: 10
environment: google-play
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Decrypt keys
env:
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }}
run: |
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/google-services.json.gpg
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg
- name: Upload apk to google play
env:
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
ANDROID_PUBLISHER_CREDENTIALS: ${{ secrets.ANDROID_PUBLISHER_CREDENTIALS }}
ADMOB_PROJECT_ID: ${{ secrets.ADMOB_PROJECT_ID }}
SINGLE_SUPPORT_AD_ID: ${{ secrets.SINGLE_SUPPORT_AD_ID }}
DASHBOARD_TILE_AD_ID: ${{ secrets.DASHBOARD_TILE_AD_ID }}
SET_BUILD_TIMESTAMP: ${{ secrets.SET_BUILD_TIMESTAMP }}
run: ./gradlew publishPlayReleaseApps --stacktrace;
deploy-app-gallery:
name: AppGallery
runs-on: ubuntu-latest
timeout-minutes: 10
environment: app-gallery
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Decrypt keys
env:
ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
SERVICES_ENCRYPT_KEY: ${{ secrets.SERVICES_ENCRYPT_KEY }}
run: |
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg
- name: Prepare credentials
env:
AGC_CREDENTIALS: ${{ secrets.AGC_CREDENTIALS }}
run: echo $AGC_CREDENTIALS > ./app/src/release/agconnect-credentials.json
- name: Build and publish HMS version
env:
PLAY_STORE_PASSWORD: ${{ secrets.PLAY_STORE_PASSWORD }}
PLAY_KEY_ALIAS: ${{ secrets.PLAY_KEY_ALIAS }}
PLAY_KEY_PASSWORD: ${{ secrets.PLAY_KEY_PASSWORD }}
SET_BUILD_TIMESTAMP: ${{ secrets.SET_BUILD_TIMESTAMP }}
run: ./gradlew bundleHmsRelease --stacktrace && ./gradlew publishHuaweiAppGalleryHmsRelease --stacktrace

145
.github/workflows/deploy-test.yml vendored Normal file
View File

@ -0,0 +1,145 @@
name: Deploy DEV
on:
push:
# branches: [ develop ]
branches: [ '!*' ]
pull_request_target:
# branches: [ develop ]
branches: [ '!*' ]
workflow_dispatch:
jobs:
deploy-appcenter:
name: App Center
runs-on: ubuntu-latest
timeout-minutes: 10
environment: app-center
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Set run number with offset
env:
BUILD_NUMBER_OFFSET: ${{ secrets.BUILD_NUMBER_OFFSET }}
run: echo "RUN_NUMBER=$((GITHUB_RUN_NUMBER+BUILD_NUMBER_OFFSET))" >> $GITHUB_ENV
- name: Prepare build configuration
run: |
sed -i -e "s#applicationIdSuffix \".dev\"#applicationIdSuffix \".${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/build.gradle
sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/google-services.json
sed -i -e '/versionNameSuffix/d' app/build.gradle
- name: Add signing config
run: |
cat >> app/build.gradle <<EOF
android.signingConfigs.debug {
storeFile file("bitrise.jks")
storePassword System.getenv("BITRISE_KEYSTORE_PASSWORD")
keyAlias System.getenv("BITRISE_KEY_ALIAS")
keyPassword System.getenv("BITRISE_KEY_PASSWORD")
}
EOF
- name: Decrypt keys
env:
BITRISE_ENCRYPT_KEY: ${{ secrets.BITRISE_ENCRYPT_KEY }}
run: |
gpg --yes --batch --passphrase=$BITRISE_ENCRYPT_KEY ./app/bitrise.jks.gpg
- name: Bump version
uses: chkfung/android-version-actions@v1.1
with:
gradlePath: app/build.gradle
versionCode: ${{ env.RUN_NUMBER }}
versionName: ${{ env.RUN_NUMBER }}-${{ github.head_ref }}
- name: Build apk
env:
BITRISE_KEYSTORE_PASSWORD: ${{ secrets.BITRISE_KEYSTORE_PASSWORD }}
BITRISE_KEY_ALIAS: ${{ secrets.BITRISE_KEY_ALIAS }}
BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }}
run: ./gradlew assembleFdroidDebug --stacktrace
- name: Upload apk to github artifacts
uses: actions/upload-artifact@v3
with:
name: wulkanowyDEV-${{ env.RUN_NUMBER }}.apk
path: app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk
- name: Deploy to app center
uses: wzieba/AppCenter-Github-Action@v1
with:
appName: wulkanowy/wulkanowy
token: ${{ secrets.APP_CENTER_TOKEN }}
group: Testers
file: app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk
notifyTesters: true
debug: true
deploy-app-distribution:
name: App Distribution
runs-on: ubuntu-latest
timeout-minutes: 10
environment: app-distribution
if: github.event_name != 'pull_request_target'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Set run number with offset
env:
BUILD_NUMBER_OFFSET: ${{ secrets.BUILD_NUMBER_OFFSET }}
run: echo "RUN_NUMBER=$((GITHUB_RUN_NUMBER+BUILD_NUMBER_OFFSET))" >> $GITHUB_ENV
- name: Add signing config
run: |
cat >> app/build.gradle <<EOF
android.signingConfigs.debug {
storeFile file("bitrise.jks")
storePassword System.getenv("BITRISE_KEYSTORE_PASSWORD")
keyAlias System.getenv("BITRISE_KEY_ALIAS")
keyPassword System.getenv("BITRISE_KEY_PASSWORD")
}
EOF
- name: Decrypt keys
env:
BITRISE_ENCRYPT_KEY: ${{ secrets.BITRISE_ENCRYPT_KEY }}
BITRISE_SERVICES_ENCRYPT_KEY: ${{ secrets.BITRISE_SERVICES_ENCRYPT_KEY }}
run: |
gpg --yes --batch --passphrase=$BITRISE_SERVICES_ENCRYPT_KEY ./app/src/debug/google-services.json.gpg
gpg --yes --batch --passphrase=$BITRISE_ENCRYPT_KEY ./app/bitrise.jks.gpg
- name: Bump version
uses: chkfung/android-version-actions@v1.1
with:
gradlePath: app/build.gradle
versionCode: ${{ env.RUN_NUMBER }}
versionName: ${{ env.RUN_NUMBER }}
- name: Build apk
env:
BITRISE_KEYSTORE_PASSWORD: ${{ secrets.BITRISE_KEYSTORE_PASSWORD }}
BITRISE_KEY_ALIAS: ${{ secrets.BITRISE_KEY_ALIAS }}
BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }}
run: ./gradlew assemblePlayDebug --stacktrace
- name: Upload apk to github artifacts
uses: actions/upload-artifact@v3
with:
name: wulkanowyDEV-${{ env.RUN_NUMBER }}-dev.apk
path: app/build/outputs/apk/play/debug/app-play-debug.apk
- name: Deploy to app distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
token: ${{ secrets.FIREBASE_TOKEN }}
groups: discord
file: app/build/outputs/apk/play/debug/app-play-debug.apk

90
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,90 @@
name: Tests
on:
push:
branches:
- master
- develop
- 'hotfix/**'
tags: [ '*' ]
pull_request:
jobs:
tests-fdroid:
name: F-Droid
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Unit tests
run: |
./gradlew testFdroidDebugUnitTest --stacktrace
./gradlew jacocoTestReport --stacktrace
- uses: codecov/codecov-action@v3
with:
flags: unit
tests-play:
name: Play
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Unit tests
run: |
./gradlew testPlayDebugUnitTest --stacktrace
./gradlew jacocoTestReport --stacktrace
- uses: codecov/codecov-action@v3
with:
flags: unit
tests-hms:
name: HMS
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: 17
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Unit tests
run: |
./gradlew testHmsDebugUnitTest --stacktrace
./gradlew jacocoTestReport --stacktrace
- uses: codecov/codecov-action@v3
with:
flags: unit

1
.gitignore vendored
View File

@ -127,4 +127,3 @@ google-services.json
!app/google-services.json
.idea/appInsightsSettings.xml

View File

@ -1,33 +1,73 @@
Česká verze / [Deutsche Version](README.de.md) / [English version](README.en.md) / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md)
# Wulkanowy MOD
# Wulkanowy
## Funkce:
* skrýt známky
* Skrýt jednotlivé záznamy o docházce.
* Skrýt komentáře.
* falešná docházka %
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy)
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Chcete-li se dostat na skrytý panel:
1. Přejděte na kartu „Další“.
2. Přejděte na panel „Nastavení“.
3. Přejděte na panel „O aplikaci“.
4. Klikněte 5x na logo aplikace
5. Přejděte na domovskou obrazovku
6. Přejděte do nastavení
7. Zadejte „tajná nastavení“
# Instalace
Neoficiální klient deníku VULCAN UONET+ pro žáka a rodiče
| Název souboru | Přizpůsobeno |
| ---------------- | ----------------- |
| `*-fdroid-*.apk` | F-Droid |
| `*-hms-*.apk` | Huawei AppGallery |
| `*-play-*.apk` | Play Store |
## Funkce
Stáhněte si vybranou verzi z [releases](https://git.sador.me/sadorowo/wulkanowy-mod/releases).
Doporučujeme stáhnout nejnovější dostupnou verzi.
* přihlášení pomocí emailu a hesla
* funkce z webové stránky deníku:
* známky
* statistiky známek
* frekvence
* procento frekvence
* zkoušky
* plán lekce
* dokončené lekce
* zprávy
* domácí úkoly
* poznámky
* šťastné číslo
* další lekce
* školní setkání
* informace o žáku a škole
* výpočet průměru nezávisle na preferencích školy
* upozornění, např. o nových známkách
* podpora více účtů s možností přejmenování žáků
* tmavý a černý (AMOLED) motiv
* offline režim
* volitelné reklamy na podporu projektu
# O projektu Wulkanowy
## Stáhnout
Chcete si přečíst více o projektu Wulkanowy? [Klikněte sem](https://github.com/wulkanowy/wulkanowy)
Aktuální verzi si můžete stáhnout z Google Play, F-Droid nebo Huawei AppGallery
[<img src="https://play.google.com/intl/cs-CZ/badges/images/generic/cs_badge_web_generic.png"
alt="Nyní na Google Play"
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Stáhnout s F-Droid"
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
[<img src="https://i.imgur.com/baTGiDP.png"
alt="Objevuj v AppGallery"
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
Můžete si také stáhnout [vývojovou verzi](https://wulkanowy.github.io/#download), která zahrnuje nové funkce připravované pro příští vydání
## Postaveno s pomocí
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
* [Hilt](https://dagger.dev/hilt/)
* [Room](https://developer.android.com/topic/libraries/architecture/room)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
## Spolupráce
Přispějte do projektu vytvořením PR nebo odesláním issue na GitHub.
Pro zájemce o překlad aplikace do různých jazyků poskytujeme Crowdin:
https://crowdin.com/project/wulkanowy2
## Licence
Tento projekt je licencován pod licencí Apache License 2.0 - podrobnosti v souboru [LICENSE](LICENSE)

View File

@ -1,33 +1,73 @@
[Česká verze](README.cs.md) / Deutsche Version / [English version](README.en.md) / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md)
# Wulkanowy MOD
# Wulkanowy
## Funktionen:
* Noten ausblenden
* Individuelle Anwesenheitslisten ausblenden.
* Kommentare ausblenden.
* Anwesenheit fälschen %
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy)
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
So gelangen Sie zum ausgeblendeten Bereich:
1. Gehen Sie zur Registerkarte „Mehr“.
2. Gehen Sie zum Bereich „Einstellungen“.
3. Gehen Sie zum Bereich „Über die Anwendung“.
4. Klicken Sie fünfmal auf das Anwendungslogo
5. Gehen Sie zum Startbildschirm
6. Gehen Sie zu den Einstellungen
7. Geben Sie „Geheime Einstellungen“ ein
# Installation
Inoffizieller Android VULCAN UONET+ Registrierungsclient für Schüler und ihre Eltern
| Dateiname | Angepasst an |
| ---------------- | ----------------- |
| `*-fdroid-*.apk` | F-Droid |
| `*-hms-*.apk` | Huawei AppGallery |
| `*-play-*.apk` | Play Store |
## Merkmale
Laden Sie die ausgewählte Version von [hier](https://git.sador.me/sadorowo/wulkanowy-mod/releases) herunter.
Wir empfehlen, die neueste verfügbare Version herunterzuladen.
* Einloggen mit E-Mail und Passwort
* Funktionen von der Registerwebsite:
* Noten
* Notenstatistik
* Anwesenheit
* Prozentsatz der Anwesenheit
* Prüfungen
* Stundenplan
* abgeschlossene Unterrichtsstunden
* Nachrichten
* Hausaufgaben
* Anmerkungen
* Glückszahl
* Zusätzliche Lektionen
* Schulkonferenzen
* Schüler- und Schulinformationen
* Berechnung des Durchschnitts unabhängig von den Präferenzen der Schule
* Benachrichtigungen, z. B. über eine neue Note
* Unterstützung für mehrere Konten mit der Möglichkeit, den Namen des Schülers zu ändern
* dunkles und schwarzes (AMOLED) Thema
* Offline-Modus
* optionale Werbungen, die es uns ermöglichen das Projekt zu unterstützen
# Über das Wulkanowy-Projekt
## Herunterladen
Möchten Sie mehr über das Wulkanowy-Projekt lesen? [Hier klicken](https://github.com/wulkanowy/wulkanowy)
Die aktuelle Version können Sie von der Google Play, F-Droid oder Huawei AppGallery store herunterladen
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
[<img src="appgallery_badge.png"
alt="Explore it on AppGallery"
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
Sie können auch eine [Entwicklungsversion herunterladen](https://wulkanowy.github.io/#download) die beinhaltet neue Funktionen, die für die nächste Version vorbereitet werden
## Gebaut mit
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
* [Hilt](https://dagger.dev/hilt/)
* [Room](https://developer.android.com/topic/libraries/architecture/room)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
## Beitragen
Bitte tragen Sie zum Projekt bei, indem Sie entweder eine PR erstellen oder ein Issue auf GitHub einreichen.
Für Personen, die daran interessiert sind, die Anwendung in verschiedene Sprachen zu übersetzen, bieten wir Crowdin
https://crowdin.com/project/wulkanowy2
## Lizenz
Dieses Projekt ist unter der Apache License 2.0 lizenziert - siehe die [LIZENZ](LICENSE) Datei für Details

View File

@ -1,33 +1,73 @@
[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / English version / [Polska wersja](README.md) / [Slovenská verzia](README.sk.md)
# Wulkanowy MOD
# Wulkanowy
## Functions:
* hide grades
* hide individual attendance entries
* hide comments
* fake attendance %.
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy)
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
To get to the hidden panel:
1. Go to the "More" tab
2. Go to the "Settings" panel
3. Go to the "About application" panel
4. Click on the application logo 5 times
5. Go to the home screen
6. Go to settings
7. Enter "secret settings"
# Installation
Unofficial android VULCAN UONET+ register client for both students and their parents
| File name | Adapted to |
| ---------------- | ----------------- |
| `*-fdroid-*.apk` | F-Droid |
| `*-hms-*.apk` | Huawei AppGallery |
| `*-play-*.apk` | Play Store |
## Features
Download application from [releases](https://git.sador.me/sadorowo/wulkanowy-mod/releases).
We recommend downloading the latest available version.
* logging in using the email and password
* functions from the register website:
* grades
* grade statistics
* attendance
* percentage of attendance
* exams
* timetable
* completed lessons
* messages
* homework
* notes
* lucky number
* additional lessons
* school conferences
* student and school information
* calculation of the average independently of school's preferences
* notifications, e.g. about a new grade
* support for multiple accounts with the ability to rename students
* dark and black (AMOLED) theme
* offline mode
* optional ads which allow to support the project
# About the Wulkanowy project
## Download
Want to read more about the Wulkanowy project? [Click here](https://github.com/wulkanowy/wulkanowy)
You can download the current version from the Google Play, F-Droid or Huawei AppGallery store
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
[<img src="appgallery_badge.png"
alt="Explore it on AppGallery"
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
You can also download a [development version](https://wulkanowy.github.io/#download) that includes new features being prepared for the next release
## Built With
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
* [Hilt](https://dagger.dev/hilt/)
* [Room](https://developer.android.com/topic/libraries/architecture/room)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
## Contributing
Please contribute to the project either by creating a PR or submitting an issue on GitHub.
For people interested in translating the application into different languages, we provide Crowdin
https://crowdin.com/project/wulkanowy2
## License
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details

View File

@ -1,33 +1,74 @@
[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / [English version](README.en.md) / Polska wersja / [Slovenská verzia](README.sk.md)
# Wulkanowy MOD
# Wulkanowy
## Funkcje:
* ukryj oceny
* ukryj poszczególne wpisy frekwencji
* ukryj uwagi
* sfałszuj % frekwencji
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy)
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Aby dostać się do ukrytego panelu:
1. Przejdź do karty "Więcej"
2. Przejdź do panelu "Ustawienia"
3. Przejdź do panelu "O aplikacji"
4. Kliknij 5 razy w logo aplikacji
5. Przejdź na ekran główny
6. Wejdź w ustawienia
7. Wejdź w "sekretne ustawienia"
# Instalacja
Nieoficjalny klient dziennika VULCAN UONET+ dla ucznia i rodzica
| Nazwa pliku | Przystosowana do |
| ---------------- | ----------------- |
| `*-fdroid-*.apk` | F-Droid |
| `*-hms-*.apk` | Huawei AppGallery |
| `*-play-*.apk` | Sklep Play |
## Funkcje
Pobierz wybraną wersję z [wydań](https://git.sador.me/sadorowo/wulkanowy-mod/releases).
Zalecamy pobranie najnowszej dostępnej wersji.
* logowanie za pomocą e-maila i hasła
* funkcje ze strony internetowej dziennika:
* oceny
* statystyki ocen
* frekwencja
* procent frekwencji
* sprawdziany
* plan lekcji
* lekcje zrealizowane
* wiadomości
* zadania domowe
* uwagi
* szczęśliwy numerek
* dodatkowe lekcje
* zebrania w szkole
* informacje o uczniu i szkole
* obliczanie średniej niezależnie od preferencji szkoły
* powiadomienia np. o nowej ocenie
* obsługa wielu kont wraz z możliwością zmiany nazwy ucznia
* ciemny i czarny (AMOLED) motyw
* tryb offline
* opcjonalne reklamy umożliwiające wsparcie projektu
# O projekcie Wulkanowy
## Pobierz
Chcesz poczytać więcej o projekcie Wulkanowy? [Kliknij tutaj](https://github.com/wulkanowy/wulkanowy)
Aktualną wersję możesz pobrać ze sklepu Google Play, F-Droid lub Huawei AppGallery
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Pobierz z Google Play"
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Pobierz z F-Droid"
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
[<img src="appgallery_badge.png"
alt="Odkrywaj w AppGallery"
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
Możesz także pobrać [wersję rozwojową](https://wulkanowy.github.io/#download), która zawiera nowe funkcje przygotowywane do następnego wydania
## Zbudowana za pomocą
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
* [Hilt](https://dagger.dev/hilt/)
* [Room](https://developer.android.com/topic/libraries/architecture/room)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
## Współpraca
Wnieś swój wkład w projekt, tworząc PR lub wysyłając issue na GitHub.
Dla osób zainteresowanych tłumaczeniem aplikacji na różne języki udostępniamy Crowdina
https://crowdin.com/project/wulkanowy2
## Licencja
Ten projekt udostępniany jest na licencji Apache License 2.0 - szczegóły w pliku [LICENSE](LICENSE)

View File

@ -1,33 +1,73 @@
[Česká verze](README.cs.md) / [Deutsche Version](README.de.md) / [English version](README.en.md) / [Polska wersja](README.md) / Slovenská verzia
# Wulkanowy MOD
# Wulkanowy
## Funkcie:
* skryť známky
* Skryť individuálne záznamy o dochádzke.
* Skryť komentáre.
* falošná dochádzka %
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/wulkanowy/wulkanowy/test.yml?branch=develop&style=flat-square)](https://github.com/wulkanowy/wulkanowy/actions)
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy)
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![F-Droid](https://img.shields.io/f-droid/v/io.github.wulkanowy.svg?style=flat-square)](https://f-droid.org/packages/io.github.wulkanowy/)
[![Last release](https://img.shields.io/github/release/wulkanowy/wulkanowy.svg?logo=github&style=flat-square)](https://github.com/wulkanowy/wulkanowy/releases)
[![Crowdin](https://badges.crowdin.net/wulkanowy2/localized.svg)](https://translate.wulkanowy.net.pl)
Ak chcete prejsť na skrytý panel:
1. Prejdite na kartu „Viac“.
2. Prejdite na panel „Nastavenia“.
3. Prejdite na panel „O aplikácii“.
4. Kliknite 5-krát na logo aplikácie
5. Prejdite na domovskú obrazovku
6. Prejdite do nastavení
7. Zadajte „tajné nastavenia“
# Inštalácia
Neoficiálny klient denníka VULCAN UONET+ pre žiaka a rodičov
| Názov súboru | Prispôsobené |
| ---------------- | ----------------- |
| `*-fdroid-*.apk` | F-Droid |
| `*-hms-*.apk` | Huawei AppGallery |
| `*-play-*.apk` | Play Store |
## Funkcie
Stiahnite si vybranú verziu z [releases](https://git.sador.me/sadorowo/wulkanowy-mod/releases).
Odporúčame stiahnuť najnovšiu dostupnú verziu.
* prihlásenie pomocou emailu a hesla
* funkcie z webovej stránky denníka:
* známky
* štatistiky známok
* frekvencia
* percento frekvencie
* skúšky
* plán lekcie
* dokončené lekcie
* správy
* domáce úlohy
* poznámky
* šťastné číslo
* ďalšie lekcie
* školské stretnutie
* informácie o žiakovi a škole
* výpočet priemeru nezávisle od preferencií školy
* upozornenia, napr. o nových známkach
* podpora viacerých účtov s možnosťou premenovania žiakov
* tmavý a čierny (AMOLED) motív
* offline režim
* voliteľné reklamy na podporu projektu
# O projekte Wulkanowy
## Stiahnuť
Chcete si prečítať viac o projekte Wulkanowy? [Kliknite sem](https://github.com/wulkanowy/wulkanowy)
Aktuálnu verziu si môžete stiahnuť z Google Play, F-Droid alebo Huawei AppGallery
[<img src="https://play.google.com/intl/sk/badges/images/generic/sk_badge_web_generic.png"
alt="Nyní na Google Play"
height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Stiahnuť s F-Droid"
height="80">](https://f-droid.org/packages/io.github.wulkanowy/)
[<img src="https://i.imgur.com/sX8UyAw.png"
alt="Objavíte v AppGallery"
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
Môžete si tiež stiahnuť [vývojovú verziu](https://wulkanowy.github.io/#download), ktorá zahrňuje nové funkcie pripravované pre budúce vydanie
## Postavené s pomocou
* [Wulkanowy SDK](https://github.com/wulkanowy/sdk)
* [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)
* [Hilt](https://dagger.dev/hilt/)
* [Room](https://developer.android.com/topic/libraries/architecture/room)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager)
## Spolupráca
Prispejte do projektu vytvorením PR alebo odoslaním issue na GitHub.
Pre záujemcov o preklad aplikácie do rôznych jazykov poskytujeme Crowdin:
https://crowdin.com/project/wulkanowy2
## Licencia
Tento projekt je licencovaný pod licenciou Apache License 2.0 - podrobnosti v súbore [LICENSE](LICENSE)

View File

@ -27,8 +27,8 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 34
versionCode 173
versionName "2.6.13"
versionCode 150
versionName "2.5.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
@ -62,8 +62,8 @@ android {
release {
minifyEnabled true
shrinkResources true
// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
// signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"'
}
@ -160,8 +160,8 @@ play {
defaultToAppBundles = false
track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.1d
updatePriority = 2
userFraction = 0.50d
updatePriority = 1
enabled.set(false)
}
@ -187,29 +187,27 @@ ext {
room = "2.6.1"
chucker = "4.0.0"
mockk = "1.13.10"
coroutines = "1.8.1"
coroutines = "1.8.0"
}
dependencies {
implementation 'io.github.wulkanowy:sdk:2.6.11'
implementation 'io.github.wulkanowy:sdk:2.5.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutines"
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation "androidx.activity:activity-ktx:1.9.0"
implementation "androidx.activity:activity-ktx:1.8.2"
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.7.0"
implementation "androidx.fragment:fragment-ktx:1.6.2"
implementation "androidx.annotation:annotation:1.7.1"
implementation "androidx.javascriptengine:javascriptengine:1.0.0-beta01"
implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.recyclerview:recyclerview:1.3.2"
implementation "androidx.viewpager2:viewpager2:1.1.0-rc01"
implementation "androidx.viewpager2:viewpager2:1.1.0-beta02"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
@ -235,7 +233,7 @@ dependencies {
implementation 'com.github.ncapdevi:FragNav:3.3.0'
implementation "com.github.YarikSOffice:lingver:1.3.0"
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
@ -248,15 +246,15 @@ dependencies {
implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.9.1'
implementation 'org.apache.commons:commons-text:1.12.0'
implementation 'org.apache.commons:commons-text:1.11.0'
playImplementation platform('com.google.firebase:firebase-bom:33.0.0')
playImplementation platform('com.google.firebase:firebase-bom:32.7.4')
playImplementation 'com.google.firebase:firebase-analytics'
playImplementation 'com.google.firebase:firebase-messaging'
playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.firebase:firebase-config'
playImplementation 'com.google.android.gms:play-services-ads:22.6.0'
playImplementation 'com.google.android.gms:play-services-ads:23.0.0'
playImplementation "com.google.android.play:integrity:1.3.0"
playImplementation 'com.google.android.play:app-update-ktx:2.1.0'
playImplementation 'com.google.android.play:review-ktx:2.0.1'
@ -276,7 +274,7 @@ dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.12.1'
testImplementation 'org.robolectric:robolectric:4.11.1'
testImplementation "androidx.test:runner:1.5.2"
testImplementation "androidx.test.ext:junit:1.1.5"
testImplementation "androidx.test:core:1.5.0"

View File

@ -36,37 +36,6 @@
"status": 2
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:1091101852179:android:b558a25f65d088b1",
"android_client_info": {
"package_name": "io.github.wulkanowy"
}
},
"oauth_client": [
{
"client_id": "",
"client_type": 3
}
],
"api_key": [
{
"current_key": ""
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,8 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly">
<uses-sdk tools:overrideLibrary="androidx.javascriptengine" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
@ -44,16 +42,16 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:resizeableActivity="true"
android:supportsRtl="false"
android:theme="@style/WulkanowyTheme"
android:resizeableActivity="true"
tools:ignore="DataExtractionRules,UnusedAttribute">
<activity
android:name=".ui.modules.splash.SplashActivity"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/WulkanowyTheme.SplashScreen"
tools:ignore="DiscouragedApi,LockedOrientationActivity">
tools:ignore="LockedOrientationActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View File

@ -13,8 +13,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.github.wulkanowy.data.api.services.SchoolsService
import io.github.wulkanowy.data.api.services.WulkanowyService
import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository
@ -71,7 +71,7 @@ internal class DataModule {
okHttpClient: OkHttpClient,
json: Json,
appInfo: AppInfo
): WulkanowyService = Retrofit.Builder()
): AdminMessageService = Retrofit.Builder()
.baseUrl(appInfo.messagesBaseUrl)
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))

View File

@ -1,47 +1,25 @@
package io.github.wulkanowy.data
import android.content.Context
import android.os.Build
import androidx.javascriptengine.JavaScriptSandbox
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.google.common.util.concurrent.ListenableFuture
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.scrapper.EvaluateHandler
import io.github.wulkanowy.utils.RemoteConfigHelper
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class WulkanowySdkFactory @Inject constructor(
@ApplicationContext private val context: Context,
private val chuckerInterceptor: ChuckerInterceptor,
private val remoteConfig: RemoteConfigHelper,
private val webkitCookieManagerProxy: WebkitCookieManagerProxy,
private val studentDb: StudentDao,
private val wulkanowyRepository: WulkanowyRepository,
private val webkitCookieManagerProxy: WebkitCookieManagerProxy
) {
private val eduOneMutex = Mutex()
private val migrationFailedStudentIds = mutableSetOf<Long>()
private val sandbox: ListenableFuture<JavaScriptSandbox>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && JavaScriptSandbox.isSupported())
JavaScriptSandbox.createConnectedInstanceAsync(context)
else null
private val sdk = Sdk().apply {
androidVersion = Build.VERSION.RELEASE
buildTag = Build.MODEL
androidVersion = android.os.Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL
userAgentTemplate = remoteConfig.userAgentTemplate
setSimpleHttpLogger { Timber.d(it) }
setAdditionalCookieManager(webkitCookieManagerProxy)
@ -50,46 +28,9 @@ class WulkanowySdkFactory @Inject constructor(
addInterceptor(chuckerInterceptor, network = true)
}
fun createBase() = sdk
fun create() = sdk
suspend fun create(): Sdk {
val mapping = wulkanowyRepository.getMapping()
return createBase().apply {
if (mapping != null) {
endpointsMapping = mapping.endpoints
vTokenMapping = mapping.vTokens
vHeaders = mapping.vHeaders
vParamsEvaluation = createIsolate()
}
}
}
private suspend fun createIsolate(): suspend () -> EvaluateHandler {
return {
val isolate = sandbox?.await()?.createIsolate()
object : EvaluateHandler {
override suspend fun evaluate(code: String): String? {
return isolate?.evaluateJavaScriptAsync(code)?.await()
}
override fun close() {
isolate?.close()
}
}
}
}
suspend fun create(student: Student, semester: Semester? = null): Sdk {
val overrideIsEduOne = checkEduOneAndMigrateIfNecessary(student)
return buildSdk(student, semester, overrideIsEduOne)
}
private suspend fun buildSdk(
student: Student,
semester: Semester?,
isStudentEduOne: Boolean
): Sdk {
fun create(student: Student, semester: Semester? = null): Sdk {
return create().apply {
email = student.email
password = student.password
@ -98,7 +39,6 @@ class WulkanowySdkFactory @Inject constructor(
studentId = student.studentId
classId = student.classId
emptyCookieJarInterceptor = true
isEduOne = isStudentEduOne
if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) {
mobileBaseUrl = student.mobileBaseUrl
@ -121,51 +61,4 @@ class WulkanowySdkFactory @Inject constructor(
}
}
}
private suspend fun checkEduOneAndMigrateIfNecessary(student: Student): Boolean {
if (student.isEduOne != null) return student.isEduOne
if (student.id in migrationFailedStudentIds) {
Timber.i("Migration eduOne: skipping because of previous failure")
return false
}
eduOneMutex.withLock {
if (student.id in migrationFailedStudentIds) {
Timber.i("Migration eduOne: skipping because of previous failure")
return false
}
val studentFromDatabase = studentDb.loadById(student.id)
if (studentFromDatabase?.isEduOne != null) {
Timber.i("Migration eduOne: already done")
return studentFromDatabase.isEduOne
}
Timber.i("Migration eduOne: flag missing. Running migration...")
val initializedSdk = buildSdk(
student = student,
semester = null,
isStudentEduOne = false, // doesn't matter
)
val newCurrentStudent = runCatching { initializedSdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Migration eduOne: can't get current student") }
.getOrNull()
if (newCurrentStudent == null) {
Timber.i("Migration eduOne: failed, so skipping")
migrationFailedStudentIds.add(student.id)
return false
}
Timber.i("Migration eduOne: success. New isEduOne flag: ${newCurrentStudent.isEduOne}")
val studentIsEduOne = StudentIsEduOne(
id = student.id,
isEduOne = newCurrentStudent.isEduOne
)
studentDb.update(studentIsEduOne)
return newCurrentStudent.isEduOne
}
}
}

View File

@ -1,16 +1,12 @@
package io.github.wulkanowy.data.api.services
package io.github.wulkanowy.data.api
import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.db.entities.AdminMessage
import retrofit2.http.GET
import javax.inject.Singleton
@Singleton
interface WulkanowyService {
interface AdminMessageService {
@GET("/v1.json")
suspend fun getAdminMessages(): List<AdminMessage>
@GET("/mapping2.json")
suspend fun getMapping(): Mapping
}
}

View File

@ -1,4 +1,4 @@
package io.github.wulkanowy.data.api.services
package io.github.wulkanowy.data.api
import io.github.wulkanowy.data.pojos.IntegrityRequest
import io.github.wulkanowy.data.pojos.LoginEvent

View File

@ -1,20 +0,0 @@
package io.github.wulkanowy.data.api.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Mapping(
@SerialName("endpoints")
val endpoints: Map<String, Map<String, Map<String, String>>>,
@SerialName("vTokens")
val vTokens: Map<String, Map<String, Map<String, String>>>,
@SerialName("vTokenScheme")
val vTokenScheme: Map<String, Map<String, String>> = emptyMap(),
@SerialName("vHeaders")
val vHeaders: Map<String, Map<String, Map<String, String>>> = emptyMap(),
)

View File

@ -120,7 +120,6 @@ import io.github.wulkanowy.data.db.migrations.Migration55
import io.github.wulkanowy.data.db.migrations.Migration57
import io.github.wulkanowy.data.db.migrations.Migration58
import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration63
import io.github.wulkanowy.data.db.migrations.Migration7
import io.github.wulkanowy.data.db.migrations.Migration8
import io.github.wulkanowy.data.db.migrations.Migration9
@ -175,9 +174,6 @@ import javax.inject.Singleton
AutoMigration(from = 58, to = 59),
AutoMigration(from = 59, to = 60),
AutoMigration(from = 60, to = 61),
AutoMigration(from = 61, to = 62),
AutoMigration(from = 62, to = 63, spec = Migration63::class),
AutoMigration(from = 63, to = 64),
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -186,7 +182,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 64
const val VERSION_SCHEMA = 61
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
@ -313,6 +309,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val adminMessagesDao: AdminMessageDao
abstract val mutedMessageSendersDao: MutedMessageSendersDao
abstract val gradeDescriptiveDao: GradeDescriptiveDao
}

View File

@ -3,7 +3,6 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import kotlinx.coroutines.flow.Flow
import java.time.LocalDate
import javax.inject.Singleton

View File

@ -12,8 +12,4 @@ interface GradeDao : BaseDao<Grade> {
@Query("SELECT * FROM Grades WHERE semester_id = :semesterId AND student_id = :studentId")
fun loadAll(semesterId: Int, studentId: Int): Flow<List<Grade>>
@Query("SELECT * FROM Grades WHERE semester_id = :semesterId AND student_id = :studentId " +
"AND entry NOT IN(:censoredEntries)")
fun loadAllCensored(semesterId: Int, studentId: Int, censoredEntries: Array<String>): Flow<List<Grade>>
}

View File

@ -8,6 +8,6 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface MobileDeviceDao : BaseDao<MobileDevice> {
@Query("SELECT * FROM MobileDevices WHERE user_login_id = :studentId ORDER BY date DESC")
fun loadAll(studentId: Int): Flow<List<MobileDevice>>
@Query("SELECT * FROM MobileDevices WHERE user_login_id = :userLoginId ORDER BY date DESC")
fun loadAll(userLoginId: Int): Flow<List<MobileDevice>>
}

View File

@ -9,6 +9,7 @@ import javax.inject.Singleton
@Singleton
@Dao
interface NoteDao : BaseDao<Note> {
@Query("SELECT * FROM Notes WHERE student_id = :studentId")
fun loadAll(studentId: Int): Flow<List<Note>>
}

View File

@ -10,6 +10,6 @@ import javax.inject.Singleton
@Singleton
interface SchoolAnnouncementDao : BaseDao<SchoolAnnouncement> {
@Query("SELECT * FROM SchoolAnnouncements WHERE user_login_id = :studentId ORDER BY date DESC")
fun loadAll(studentId: Int): Flow<List<SchoolAnnouncement>>
@Query("SELECT * FROM SchoolAnnouncements WHERE user_login_id = :userLoginId ORDER BY date DESC")
fun loadAll(userLoginId: Int): Flow<List<SchoolAnnouncement>>
}

View File

@ -14,6 +14,6 @@ interface SemesterDao : BaseDao<Semester> {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertSemesters(items: List<Semester>): List<Long>
@Query("SELECT * FROM Semesters WHERE (student_id = :studentId AND class_id = :classId) OR (student_id = :studentId AND class_id = 0)")
@Query("SELECT * FROM Semesters WHERE student_id = :studentId AND class_id = :classId")
suspend fun loadAll(studentId: Int, classId: Int): List<Semester>
}

View File

@ -9,8 +9,6 @@ import androidx.room.Transaction
import androidx.room.Update
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import javax.inject.Singleton
@ -25,12 +23,6 @@ abstract class StudentDao {
@Delete
abstract suspend fun delete(student: Student)
@Update(entity = Student::class)
abstract suspend fun update(studentIsAuthorized: StudentIsAuthorized)
@Update(entity = Student::class)
abstract suspend fun update(studentIsEduOne: StudentIsEduOne)
@Update(entity = Student::class)
abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
@ -47,11 +39,11 @@ abstract class StudentDao {
abstract suspend fun loadAll(): List<Student>
@Transaction
@Query("SELECT * FROM Students JOIN Semesters ON (Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id) OR (Students.student_id = Semesters.student_id AND Semesters.class_id = 0)")
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id")
abstract suspend fun loadStudentsWithSemesters(): Map<Student, List<Semester>>
@Transaction
@Query("SELECT * FROM Students JOIN Semesters ON (Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id) OR (Students.student_id = Semesters.student_id AND Semesters.class_id = 0) WHERE Students.id = :id")
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id WHERE Students.id = :id")
abstract suspend fun loadStudentWithSemestersById(id: Long): Map<Student, List<Semester>>
@Query("UPDATE Students SET is_current = 1 WHERE id = :id")

View File

@ -4,8 +4,6 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.serializers.SafeMessageTypeEnumListSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@ -36,8 +34,6 @@ data class AdminMessage(
val priority: String,
@SerialName("messageTypes")
@Serializable(with = SafeMessageTypeEnumListSerializer::class)
@ColumnInfo(name = "types", defaultValue = "[]")
val types: List<MessageType> = emptyList(),

View File

@ -33,13 +33,7 @@ data class GradeSummary(
@ColumnInfo(name = "points_sum")
val pointsSum: String,
@ColumnInfo(name = "points_sum_all_year")
val pointsSumAllYear: String?,
val average: Double,
@ColumnInfo(name = "average_all_year")
val averageAllYear: Double? = null,
val average: Double
) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0

View File

@ -9,8 +9,8 @@ import java.time.Instant
@Entity(tableName = "MobileDevices")
data class MobileDevice(
@ColumnInfo(name = "user_login_id") // todo: change column name
val studentId: Int,
@ColumnInfo(name = "user_login_id")
val userLoginId: Int,
@ColumnInfo(name = "device_id")
val deviceId: Int,

View File

@ -9,8 +9,8 @@ import java.time.LocalDate
@Entity(tableName = "SchoolAnnouncements")
data class SchoolAnnouncement(
@ColumnInfo(name = "user_login_id") // todo: change column name
val studentId: Int,
@ColumnInfo(name = "user_login_id")
val userLoginId: Int,
val date: LocalDate,

View File

@ -49,7 +49,6 @@ data class Student(
@ColumnInfo(name = "student_id")
val studentId: Int,
@Deprecated("not available in VULCAN anymore")
@ColumnInfo(name = "user_login_id")
val userLoginId: Int,
@ -79,13 +78,6 @@ data class Student(
@ColumnInfo(name = "registration_date")
val registrationDate: Instant,
@ColumnInfo(name = "is_authorized", defaultValue = "0")
val isAuthorized: Boolean,
@ColumnInfo(name = "is_edu_one", defaultValue = "NULL")
val isEduOne: Boolean?,
) : Serializable {
@PrimaryKey(autoGenerate = true)
@ -96,22 +88,3 @@ data class Student(
@ColumnInfo(name = "avatar_color")
var avatarColor = 0L
}
@Entity
data class StudentIsAuthorized(
@PrimaryKey
var id: Long,
@ColumnInfo(name = "is_authorized", defaultValue = "NULL")
val isAuthorized: Boolean?,
) : Serializable
@Entity
data class StudentIsEduOne(
@PrimaryKey
var id: Long,
@ColumnInfo(name = "is_edu_one", defaultValue = "NULL")
val isEduOne: Boolean?,
) : Serializable

View File

@ -1,11 +0,0 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.AutoMigrationSpec
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration63 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
db.execSQL("UPDATE Students SET is_edu_one = NULL WHERE is_edu_one = 0")
}
}

View File

@ -7,6 +7,6 @@ enum class AppTheme(val value: String) {
BLACK("black");
companion object {
fun getByValue(value: String) = entries.find { it.value == value } ?: LIGHT
fun getByValue(value: String) = values().find { it.value == value } ?: LIGHT
}
}
}

View File

@ -8,6 +8,6 @@ enum class GradeColorTheme(val value: String) : Serializable {
GRADE_COLOR("grade_color");
companion object {
fun getByValue(value: String) = entries.find { it.value == value } ?: VULCAN
fun getByValue(value: String) = values().find { it.value == value } ?: VULCAN
}
}
}

View File

@ -6,6 +6,6 @@ enum class GradeExpandMode(val value: String) {
ALWAYS_EXPANDED("always");
companion object {
fun getByValue(value: String) = entries.find { it.value == value } ?: ONE
fun getByValue(value: String) = values().find { it.value == value } ?: ONE
}
}
}

View File

@ -6,6 +6,6 @@ enum class GradeSortingMode(val value: String) {
AVERAGE("average");
companion object {
fun getByValue(value: String) = entries.find { it.value == value } ?: ALPHABETIC
fun getByValue(value: String) = values().find { it.value == value } ?: ALPHABETIC
}
}

View File

@ -4,8 +4,6 @@ enum class MessageType {
GENERAL_MESSAGE,
DASHBOARD_MESSAGE,
LOGIN_MESSAGE,
LOGIN_STUDENT_SELECT_MESSAGE,
LOGIN_SYMBOL_MESSAGE,
PASS_RESET_MESSAGE,
ERROR_OVERRIDE,
}

View File

@ -1,11 +0,0 @@
package io.github.wulkanowy.data.enums
enum class ShowAdditionalLessonsMode(val value: String) {
NONE("none"),
INLINE("inline"),
BELOW("below");
companion object {
fun getByValue(value: String) = entries.find { it.value == value } ?: INLINE
}
}

View File

@ -6,6 +6,6 @@ enum class TimetableMode(val value: String) {
SMALL_OTHER_GROUP("small");
companion object {
fun getByValue(value: String) = entries.find { it.value == value } ?: ONLY_CURRENT_GROUP
fun getByValue(value: String) = values().find { it.value == value } ?: ONLY_CURRENT_GROUP
}
}
}

View File

@ -8,7 +8,7 @@ import io.github.wulkanowy.sdk.pojo.LastAnnouncement as SdkLastAnnouncement
@JvmName("mapDirectorInformationToEntities")
fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
SchoolAnnouncement(
studentId = student.studentId,
userLoginId = student.userLoginId,
date = it.date,
subject = it.subject,
content = it.content,
@ -19,7 +19,7 @@ fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
@JvmName("mapLastAnnouncementsToEntities")
fun List<SdkLastAnnouncement>.mapToEntities(student: Student) = map {
SchoolAnnouncement(
studentId = student.studentId,
userLoginId = student.userLoginId,
date = it.date,
subject = it.subject,
content = it.content,

View File

@ -37,11 +37,9 @@ fun List<SdkGradeSummary>.mapToEntities(semester: Semester) = map {
predictedGrade = it.predicted,
finalGrade = it.final,
pointsSum = it.pointsSum,
pointsSumAllYear = it.pointsSumAllYear,
proposedPoints = it.proposedPoints,
finalPoints = it.finalPoints,
average = it.average,
averageAllYear = it.averageAllYear,
average = it.average
)
}

View File

@ -8,7 +8,7 @@ import io.github.wulkanowy.sdk.pojo.Token as SdkToken
fun List<SdkDevice>.mapToEntities(student: Student) = map {
MobileDevice(
studentId = student.studentId,
userLoginId = student.userLoginId,
date = it.createDate.toInstant(),
deviceId = it.id,
name = it.name

View File

@ -34,19 +34,17 @@ fun SdkRegisterUser.mapToPojo(password: String?) = RegisterUser(
error = it.error,
students = it.subjects
.filterIsInstance<SdkRegisterStudent>()
.map { registerStudent ->
.map { registerSubject ->
RegisterStudent(
studentId = registerStudent.studentId,
studentName = registerStudent.studentName,
studentSecondName = registerStudent.studentSecondName,
studentSurname = registerStudent.studentSurname,
className = registerStudent.className,
classId = registerStudent.classId,
isParent = registerStudent.isParent,
isAuthorized = registerStudent.isAuthorized,
isEduOne = registerStudent.isEduOne,
semesters = registerStudent.semesters
.mapToEntities(registerStudent.studentId),
studentId = registerSubject.studentId,
studentName = registerSubject.studentName,
studentSecondName = registerSubject.studentSecondName,
studentSurname = registerSubject.studentSurname,
className = registerSubject.className,
classId = registerSubject.classId,
isParent = registerSubject.isParent,
semesters = registerSubject.semesters
.mapToEntities(registerSubject.studentId),
)
},
)
@ -86,8 +84,6 @@ fun RegisterStudent.mapToStudentWithSemesters(
password = user.password.orEmpty(),
isCurrent = false,
registrationDate = Instant.now(),
isAuthorized = this.isAuthorized,
isEduOne = this.isEduOne,
).apply {
avatarColor = colors.random()
},

View File

@ -45,6 +45,4 @@ data class RegisterStudent(
val classId: Int,
val isParent: Boolean,
val semesters: List<Semester>,
val isAuthorized: Boolean,
val isEduOne: Boolean
) : java.io.Serializable

View File

@ -0,0 +1,34 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.networkBoundResource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AdminMessageRepository @Inject constructor(
private val adminMessageService: AdminMessageService,
private val adminMessageDao: AdminMessageDao,
) {
private val saveFetchResultMutex = Mutex()
fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> =
networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { false },
query = { adminMessageDao.loadAll() },
fetch = { adminMessageService.getAdminMessages() },
shouldFetch = { true },
saveFetchResult = { oldItems, newItems ->
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
},
)
.filterNot { it is Resource.Intermediate }
}

View File

@ -9,15 +9,12 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.sdk.pojo.Absent
import io.github.wulkanowy.sdk.pojo.Attendance as SdkAttendance
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.monday
import io.github.wulkanowy.utils.sunday
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import java.time.LocalDate
import java.time.LocalDateTime
@ -31,71 +28,12 @@ class AttendanceRepository @Inject constructor(
private val timetableDb: TimetableDao,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
private val preferencesRepository: PreferencesRepository
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "attendance"
private fun filterAttendance(
hiddenAttendanceTiles: List<DashboardItem.HiddenAttendanceTile>,
attendanceItem: Attendance
): Boolean {
return when {
attendanceItem.absence && attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.EXCUSED_ABSENCE
) -> false
attendanceItem.absence && !attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.UNEXCUSED_ABSENCE
) -> false
attendanceItem.lateness && attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.EXCUSED_LATENESS
) -> false
attendanceItem.lateness && !attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.UNEXCUSED_LATENESS
) -> false
attendanceItem.exemption && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.EXEMPTION) -> false
attendanceItem.deleted && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.DELETED) -> false
attendanceItem.presence && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.PRESENT) -> false
else -> !hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.UNKNOWN)
}
}
private fun filterAttendance(
hiddenAttendanceTiles: List<DashboardItem.HiddenAttendanceTile>,
attendanceItem: SdkAttendance
): Boolean {
return when {
attendanceItem.absence && attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.EXCUSED_ABSENCE
) -> false
attendanceItem.absence && !attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.UNEXCUSED_ABSENCE
) -> false
attendanceItem.lateness && attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.EXCUSED_LATENESS
) -> false
attendanceItem.lateness && !attendanceItem.excused && hiddenAttendanceTiles.contains(
DashboardItem.HiddenAttendanceTile.UNEXCUSED_LATENESS
) -> false
attendanceItem.exemption && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.EXEMPTION) -> false
attendanceItem.deleted && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.DELETED) -> false
attendanceItem.presence && hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.PRESENT) -> false
else -> !hiddenAttendanceTiles.contains(DashboardItem.HiddenAttendanceTile.UNKNOWN)
}
}
fun getAttendance(
student: Student,
semester: Semester,
@ -113,24 +51,14 @@ class AttendanceRepository @Inject constructor(
it.isEmpty() || forceRefresh || isExpired
},
query = {
val hiddenAttendanceItems = preferencesRepository.hiddenAttendanceItems
attendanceDb
.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
.map {
it.filter { item -> filterAttendance(hiddenAttendanceItems, item) }
}
attendanceDb.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
},
fetch = {
val hiddenAttendanceItems = preferencesRepository.hiddenAttendanceItems
val lessons = timetableDb.load(
semester.diaryId, semester.studentId, start.monday, end.sunday
)
wulkanowySdkFactory.create(student, semester)
.getAttendance(start.monday, end.sunday)
.filter { item -> filterAttendance(hiddenAttendanceItems, item) }
.mapToEntities(semester, lessons)
},
saveFetchResult = { old, new ->
@ -151,13 +79,7 @@ class AttendanceRepository @Inject constructor(
start: LocalDate,
end: LocalDate
): Flow<List<Attendance>> {
val hiddenAttendanceItems = preferencesRepository.hiddenAttendanceItems
return attendanceDb
.loadAll(semester.diaryId, semester.studentId, start.monday, end.sunday)
.map {
it.filter { item -> filterAttendance(hiddenAttendanceItems, item) }
}
return attendanceDb.loadAll(semester.diaryId, semester.studentId, start, end)
}
suspend fun updateTimetable(timetable: List<Attendance>) {

View File

@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import timber.log.Timber
import java.time.Instant
import javax.inject.Inject
import javax.inject.Singleton
@ -31,18 +30,10 @@ class GradeRepository @Inject constructor(
private val gradeDescriptiveDb: GradeDescriptiveDao,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
private val preferencesRepository: PreferencesRepository
) {
private val saveFetchResultMutex = Mutex()
private fun loadGrades(semesterId: Int, studentId: Int): Flow<List<Grade>> {
val hiddenGrades = preferencesRepository.hiddenGrades
Timber.i("Load grades for semester $semesterId student $studentId")
return gradeDb.loadAllCensored(semesterId, studentId, hiddenGrades.toTypedArray())
}
fun getGrades(
student: Student,
semester: Semester,
@ -70,15 +61,11 @@ class GradeRepository @Inject constructor(
}
},
fetch = {
val hiddenGrades = preferencesRepository.hiddenGrades
val (details, summary, descriptive) = wulkanowySdkFactory.create(student, semester)
.getGrades(semester.semesterId)
val censoredDetails = details.filterNot { it.entry in hiddenGrades }
Triple(
censoredDetails.mapToEntities(semester),
details.mapToEntities(semester),
summary.mapToEntities(semester),
descriptive.mapToEntities(semester)
)
@ -170,13 +157,13 @@ class GradeRepository @Inject constructor(
}
fun getUnreadGrades(semester: Semester): Flow<List<Grade>> {
return loadGrades(semester.semesterId, semester.studentId).map {
return gradeDb.loadAll(semester.semesterId, semester.studentId).map {
it.filter { grade -> !grade.isRead }
}
}
fun getGradesFromDatabase(semester: Semester): Flow<List<Grade>> {
return loadGrades(semester.semesterId, semester.studentId)
return gradeDb.loadAll(semester.semesterId, semester.studentId)
}
fun getGradesPredictedFromDatabase(semester: Semester): Flow<List<GradeSummary>> {

View File

@ -6,8 +6,6 @@ import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider
import io.github.wulkanowy.utils.AppWidgetUpdater
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
@ -20,7 +18,6 @@ import javax.inject.Singleton
class LuckyNumberRepository @Inject constructor(
private val luckyNumberDb: LuckyNumberDao,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val appWidgetUpdater: AppWidgetUpdater,
) {
private val saveFetchResultMutex = Mutex()
@ -29,7 +26,6 @@ class LuckyNumberRepository @Inject constructor(
student: Student,
forceRefresh: Boolean,
notify: Boolean = false,
isFromAppWidget: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { it == null },
@ -48,9 +44,6 @@ class LuckyNumberRepository @Inject constructor(
oldItems = listOfNotNull(oldLuckyNumber),
newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }),
)
if (!isFromAppWidget) {
appWidgetUpdater.updateAllAppWidgetsByProvider(LuckyNumberWidgetProvider::class)
}
}
}
)

View File

@ -38,7 +38,7 @@ class MobileDeviceRepository @Inject constructor(
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
it.isEmpty() || forceRefresh || isExpired
},
query = { mobileDb.loadAll(student.studentId) },
query = { mobileDb.loadAll(student.userLoginId) },
fetch = {
wulkanowySdkFactory.create(student, semester)
.getRegisteredDevices()

View File

@ -21,7 +21,6 @@ class NoteRepository @Inject constructor(
private val noteDb: NoteDao,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val refreshHelper: AutoRefreshHelper,
private val preferencesRepository: PreferencesRepository
) {
private val saveFetchResultMutex = Mutex()
@ -40,16 +39,12 @@ class NoteRepository @Inject constructor(
val isExpired = refreshHelper.shouldBeRefreshed(
getRefreshKey(cacheKey, semester)
)
it.isEmpty() || forceRefresh || isExpired
},
query = { noteDb.loadAll(student.studentId) },
fetch = {
val showNotes = preferencesRepository.showNotes
wulkanowySdkFactory.create(student, semester)
.getNotes()
.filter { showNotes }
.mapToEntities(semester)
},
saveFetchResult = { old, new ->

View File

@ -7,16 +7,13 @@ import androidx.core.content.edit
import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.fredporciuncula.flow.preferences.Preference
import com.fredporciuncula.flow.preferences.Serializer
import com.fredporciuncula.flow.preferences.map
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.enums.AppTheme
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.data.enums.GradeExpandMode
import io.github.wulkanowy.data.enums.GradeSortingMode
import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode
import io.github.wulkanowy.data.enums.TimetableGapsMode
import io.github.wulkanowy.data.enums.TimetableMode
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
@ -26,7 +23,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import timber.log.Timber
import java.time.Instant
import java.util.UUID
import javax.inject.Inject
@ -39,7 +35,6 @@ class PreferencesRepository @Inject constructor(
private val flowSharedPref: FlowSharedPreferences,
private val json: Json,
) {
private val NO_ATTENDANCE_VALUE = -1.0
val isShowPresent: Boolean
get() = getBoolean(
@ -218,12 +213,6 @@ class PreferencesRepository @Inject constructor(
)
)
val showAdditionalLessonsInPlan: ShowAdditionalLessonsMode
get() = getString(
R.string.pref_key_timetable_show_additional_lessons,
R.string.pref_default_timetable_show_additional_lessons
).let { ShowAdditionalLessonsMode.getByValue(it) }
val gradeSortingMode: GradeSortingMode
get() = GradeSortingMode.getByValue(
getString(
@ -305,60 +294,6 @@ class PreferencesRepository @Inject constructor(
selectedDashboardTilesPreference.set(filteredValue)
}
var attendancePercentage: Double?
get() = attendancePercentagePreference.get().takeIf { it != NO_ATTENDANCE_VALUE }
set(value) = attendancePercentagePreference.set(value ?: NO_ATTENDANCE_VALUE)
var hiddenAttendanceItems: List<DashboardItem.HiddenAttendanceTile>
get() = hiddenAttendanceItemsPreference.get().toList()
set(value) = hiddenAttendanceItemsPreference.set(value.toSet())
var hiddenGrades: List<String>
get() = hiddenGradesPreference.get().toList()
set(value) = hiddenGradesPreference.set(value.toSet())
var showNotes: Boolean
get() = showNotesPreference.get()
set(value) = showNotesPreference.set(value)
var developerMode: Boolean
get() = developerModePreference.get()
set(value) = developerModePreference.set(value)
private val developerModePreference: Preference<Boolean>
get() = flowSharedPref.getBoolean(
context.getString(R.string.pref_key_developer_mode),
context.resources.getBoolean(R.bool.pref_default_developer_mode)
)
private val hiddenGradesPreference: Preference<Set<String>>
get() {
val defaultSet = context.resources.getStringArray(R.array.pref_default_hidden_grades).toSet()
val prefKey = "hidden_grades"
return flowSharedPref.getStringSet(prefKey, defaultSet)
}
private val showNotesPreference: Preference<Boolean>
get() = flowSharedPref.getBoolean(
context.getString(R.string.pref_key_show_notes),
context.resources.getBoolean(R.bool.pref_default_show_notes)
)
private val hiddenAttendanceItemsPreference: Preference<Set<DashboardItem.HiddenAttendanceTile>>
get() {
val defaultSet =
context.resources.getStringArray(R.array.pref_default_hidden_attendance_items).toSet()
val prefKey = "attendance_items"
return flowSharedPref
.getStringSet(prefKey, defaultSet)
.map(
mapper = { it -> it.map { DashboardItem.HiddenAttendanceTile.valueOf(it) }.toSet() },
reverse = { it -> it.map { it.name }.toSet() }
)
}
private val selectedDashboardTilesPreference: Preference<Set<String>>
get() {
val defaultSet =
@ -368,19 +303,6 @@ class PreferencesRepository @Inject constructor(
return flowSharedPref.getStringSet(prefKey, defaultSet)
}
private val attendancePercentagePreference: Preference<Double>
get() {
val prefKey = context.getString(R.string.pref_key_attendance_percentage)
val defaultValue = context.resources.getString(R.string.pref_default_attendance_percentage)
return flowSharedPref
.getString(prefKey, defaultValue)
.map(
mapper = { it.toDoubleOrNull() ?: NO_ATTENDANCE_VALUE },
reverse = { it.toString() }
)
}
var dismissedAdminMessageIds: List<Int>
get() = sharedPref.getStringSet(PREF_KEY_ADMIN_DISMISSED_MESSAGE_IDS, emptySet())
.orEmpty()
@ -446,15 +368,6 @@ class PreferencesRepository @Inject constructor(
get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty()
private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) }
var mapping: Mapping?
get() {
val value = sharedPref.getString("mapping", null)
return value?.let { json.decodeFromString(it) }
}
set(value) = sharedPref.edit(commit = true) {
putString("mapping", value?.let { json.encodeToString(it) })
}
init {
if (installationId.isEmpty()) {
installationId = UUID.randomUUID().toString()

View File

@ -37,7 +37,7 @@ class SchoolAnnouncementRepository @Inject constructor(
it.isEmpty() || forceRefresh || isExpired
},
query = {
schoolAnnouncementDb.loadAll(student.studentId)
schoolAnnouncementDb.loadAll(student.userLoginId)
},
fetch = {
val sdk = wulkanowySdkFactory.create(student)
@ -57,7 +57,7 @@ class SchoolAnnouncementRepository @Inject constructor(
)
fun getSchoolAnnouncementFromDatabase(student: Student): Flow<List<SchoolAnnouncement>> {
return schoolAnnouncementDb.loadAll(student.studentId)
return schoolAnnouncementDb.loadAll(student.userLoginId)
}
suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) =

View File

@ -1,7 +1,7 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.api.services.SchoolsService
import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters

View File

@ -64,10 +64,7 @@ class SemesterRepository @Inject constructor(
.getSemesters()
.mapToEntities(student.studentId)
if (new.isEmpty()) {
Timber.i("Empty semester list from SDK!")
return
}
if (new.isEmpty()) return Timber.i("Empty semester list!")
val old = semesterDb.loadAll(student.studentId, student.classId)
semesterDb.removeOldAndSaveNew(

View File

@ -7,19 +7,16 @@ import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.mappers.mapToPojo
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.security.Scrambler
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -42,7 +39,6 @@ class StudentRepository @Inject constructor(
): RegisterUser = wulkanowySdkFactory.create()
.getStudentsFromHebe(token, pin, symbol, "")
.mapToPojo(null)
.also { it.logErrors() }
suspend fun getUserSubjectsFromScrapper(
email: String,
@ -53,7 +49,6 @@ class StudentRepository @Inject constructor(
): RegisterUser = wulkanowySdkFactory.create()
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol)
.mapToPojo(password)
.also { it.logErrors() }
suspend fun getStudentsHybrid(
email: String,
@ -63,7 +58,6 @@ class StudentRepository @Inject constructor(
): RegisterUser = wulkanowySdkFactory.create()
.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
.mapToPojo(password)
.also { it.logErrors() }
suspend fun getSavedStudents(decryptPass: Boolean = true): List<StudentWithSemesters> {
return studentDb.loadStudentsWithSemesters().map { (student, semesters) ->
@ -105,46 +99,6 @@ class StudentRepository @Inject constructor(
return student
}
suspend fun updateCurrentStudentAuthStatus() {
Timber.i("Check isAuthorized: started")
val student = getCurrentStudent()
if (student.isAuthorized) {
Timber.i("Check isAuthorized: already authorized")
return
}
val initializedSdk = wulkanowySdkFactory.create(student)
val newCurrentStudent = runCatching { initializedSdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Check isAuthorized: error occurred") }
.getOrNull()
if (newCurrentStudent == null) {
Timber.d("Check isAuthorized: current user is null")
return
}
val currentStudentSemesters = semesterDb.loadAll(student.studentId, student.classId)
if (currentStudentSemesters.isEmpty()) {
Timber.d("Check isAuthorized: apply empty semesters workaround")
semesterDb.insertSemesters(
items = newCurrentStudent.semesters.mapToEntities(student.studentId),
)
}
if (!newCurrentStudent.isAuthorized) {
Timber.i("Check isAuthorized: authorization required")
throw NoAuthorizationException()
}
val studentIsAuthorized = StudentIsAuthorized(
id = student.id,
isAuthorized = true
)
Timber.i("Check isAuthorized: already authorized, update local status")
studentDb.update(studentIsAuthorized)
}
suspend fun getCurrentStudent(decryptPass: Boolean = true): Student {
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException()
@ -197,21 +151,15 @@ class StudentRepository @Inject constructor(
wulkanowySdkFactory.create(student, semester)
.authorizePermission(pesel)
suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) {
val wulkanowySdk = wulkanowySdkFactory.create(student, semester)
val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Can't find student with id ${student.studentId}") }
.getOrNull() ?: return
suspend fun refreshStudentName(student: Student, semester: Semester) {
val newCurrentApiStudent = wulkanowySdkFactory.create(student, semester)
.getCurrentStudent() ?: return
val studentName = StudentName(
studentName = "${newCurrentApiStudent.studentName} ${newCurrentApiStudent.studentSurname}"
).apply { id = student.id }
studentDb.update(studentName)
semesterDb.removeOldAndSaveNew(
oldItems = semesterDb.loadAll(student.studentId, semester.classId),
newItems = newCurrentApiStudent.semesters.mapToEntities(newCurrentApiStudent.studentId)
)
}
suspend fun deleteStudentsAssociatedWithAccount(student: Student) {
@ -224,18 +172,4 @@ class StudentRepository @Inject constructor(
appDatabase.clearAllTables()
}
}
private fun RegisterUser.logErrors() {
val symbolsErrors = symbols.filter { it.error != null }
.map { it.error }
val unitsErrors = symbols.flatMap { it.schools }
.filter { it.error != null }
.map { it.error }
(symbolsErrors + unitsErrors).forEach { error ->
Timber.e(error, "Error occurred while fetching students")
}
}
}
class NoAuthorizationException : Exception()

View File

@ -13,8 +13,6 @@ import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.pojos.TimetableFull
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider
import io.github.wulkanowy.utils.AppWidgetUpdater
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.monday
@ -28,7 +26,6 @@ import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class TimetableRepository @Inject constructor(
private val timetableDb: TimetableDao,
@ -37,7 +34,6 @@ class TimetableRepository @Inject constructor(
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val schedulerHelper: TimetableNotificationSchedulerHelper,
private val refreshHelper: AutoRefreshHelper,
private val appWidgetUpdater: AppWidgetUpdater,
) {
private val saveFetchResultMutex = Mutex()
@ -56,8 +52,7 @@ class TimetableRepository @Inject constructor(
forceRefresh: Boolean,
refreshAdditional: Boolean = false,
notify: Boolean = false,
timetableType: TimetableType = TimetableType.NORMAL,
isFromAppWidget: Boolean = false
timetableType: TimetableType = TimetableType.NORMAL
) = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = {
@ -88,9 +83,6 @@ class TimetableRepository @Inject constructor(
refreshDayHeaders(timetableOld.headers, timetableNew.headers)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
if (!isFromAppWidget) {
appWidgetUpdater.updateAllAppWidgetsByProvider(TimetableWidgetProvider::class)
}
},
filterResult = { (timetable, additional, headers) ->
TimetableFull(

View File

@ -1,66 +0,0 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.api.services.WulkanowyService
import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.sync.Mutex
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class WulkanowyRepository @Inject constructor(
private val wulkanowyService: WulkanowyService,
private val adminMessageDao: AdminMessageDao,
private val preferencesRepository: PreferencesRepository,
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "mapping_refresh_key"
fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> =
networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { false },
query = { adminMessageDao.loadAll() },
fetch = { wulkanowyService.getAdminMessages() },
shouldFetch = { true },
saveFetchResult = { oldItems, newItems ->
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
},
)
.filterNot { it is Resource.Intermediate }
suspend fun getMapping(): Mapping? {
var savedMapping = preferencesRepository.mapping
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey)
)
if (savedMapping == null || isExpired) {
fetchMapping()
savedMapping = preferencesRepository.mapping
}
return savedMapping
}
suspend fun fetchMapping() {
runCatching { wulkanowyService.getMapping() }
.onFailure { Timber.e(it) }
.onSuccess {
preferencesRepository.mapping = it
refreshHelper.updateLastRefreshTimestamp(cacheKey)
}
}
}

View File

@ -1,27 +0,0 @@
package io.github.wulkanowy.data.serializers
import io.github.wulkanowy.data.enums.MessageType
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@OptIn(ExperimentalSerializationApi::class)
object SafeMessageTypeEnumListSerializer : KSerializer<List<MessageType>> {
private val serializer = ListSerializer(String.serializer())
override val descriptor = serializer.descriptor
override fun serialize(encoder: Encoder, value: List<MessageType>) {
encoder.encodeNotNullMark()
serializer.serialize(encoder, value.map { it.name })
}
override fun deserialize(decoder: Decoder): List<MessageType> =
serializer.deserialize(decoder).mapNotNull { enumName ->
MessageType.entries.find { it.name == enumName }
}
}

View File

@ -5,14 +5,14 @@ import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.repositories.AdminMessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetAppropriateAdminMessageUseCase @Inject constructor(
private val wulkanowyRepository: WulkanowyRepository,
private val adminMessageRepository: AdminMessageRepository,
private val preferencesRepository: PreferencesRepository,
private val appInfo: AppInfo
) {
@ -22,7 +22,7 @@ class GetAppropriateAdminMessageUseCase @Inject constructor(
}
operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow<Resource<AdminMessage?>> {
return wulkanowyRepository.getAdminMessages().mapResourceData { adminMessages ->
return adminMessageRepository.getAdminMessages().mapResourceData { adminMessages ->
adminMessages
.asSequence()
.filter { it.isNotDismissed() }

View File

@ -59,7 +59,7 @@ class GetMailboxByStudentUseCase @Inject constructor(
private fun String.getUnauthorizedVersion(): String {
return normalizeStudentName().split(" ")
.joinToString(" ") {
it.firstOrNull()?.toString().orEmpty() + "*".repeat((it.length - 1).coerceAtLeast(0))
it.first() + "*".repeat(it.length - 1)
}
}
}

View File

@ -17,7 +17,6 @@ import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureUnavailableException
import io.github.wulkanowy.services.sync.channels.DebugChannel
import io.github.wulkanowy.services.sync.works.Work
import io.github.wulkanowy.utils.DispatchersProvider
@ -49,7 +48,6 @@ class SyncWorker @AssistedInject constructor(
val semester = semesterRepository.getCurrentSemester(student, true)
student to semester
} catch (e: Throwable) {
Timber.e(e)
return@withContext getResultFromErrors(listOf(e))
}
@ -61,7 +59,7 @@ class SyncWorker @AssistedInject constructor(
null
} catch (e: Throwable) {
Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred")
if (e is FeatureDisabledException || e is FeatureNotAvailableException || e is FeatureUnavailableException) {
if (e is FeatureDisabledException || e is FeatureNotAvailableException) {
null
} else {
Timber.e(e)

View File

@ -19,18 +19,16 @@ class NewGradeNotification @Inject constructor(
) {
suspend fun notifyDetails(items: List<Grade>, student: Student) {
val notificationDataList = items
.filter { !it.isNotified }
.map {
NotificationData(
title = context.getPlural(R.plurals.grade_new_items, 1),
content = buildString {
append("${it.subject}: ${it.entry}")
if (it.comment.isNotBlank()) append(" (${it.comment})")
},
destination = Destination.Grade,
)
}
val notificationDataList = items.map {
NotificationData(
title = context.getPlural(R.plurals.grade_new_items, 1),
content = buildString {
append("${it.subject}: ${it.entry}")
if (it.comment.isNotBlank()) append(" (${it.comment})")
},
destination = Destination.Grade,
)
}
val groupNotificationData = GroupNotificationData(
notificationDataList = notificationDataList,

View File

@ -3,7 +3,7 @@ package io.github.wulkanowy.ui.base
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.repositories.NoAuthorizationException
import io.github.wulkanowy.sdk.scrapper.exception.AuthorizationRequiredException
import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
@ -40,7 +40,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
is ScramblerException -> onDecryptionFailed()
is BadCredentialsException -> onExpiredCredentials()
is NoCurrentStudentException -> onNoCurrentStudent()
is NoAuthorizationException -> onAuthorizationRequired()
is AuthorizationRequiredException -> onAuthorizationRequired()
is CloudflareVerificationException -> onCaptchaVerificationRequired(error.originalUrl)
}
}

View File

@ -3,19 +3,14 @@ package io.github.wulkanowy.ui.modules.about
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.ItemAboutBinding
import io.github.wulkanowy.databinding.ScrollableHeaderAboutBinding
import javax.inject.Inject
class AboutAdapter @Inject constructor(
private val preferencesRepository: PreferencesRepository
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class AboutAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var developerModeClicks = 0
private enum class ViewType(val id: Int) {
ITEM_HEADER(1),
ITEM_ELEMENT(2)
@ -51,19 +46,6 @@ class AboutAdapter @Inject constructor(
private fun bindHeaderViewHolder(binding: ScrollableHeaderAboutBinding) {
with(binding.aboutScrollableHeaderIcon) {
setOnClickListener {
if (++developerModeClicks == 5 && !preferencesRepository.developerMode) {
preferencesRepository.developerMode = true
developerModeClicks = 0
Toast.makeText(
context,
"done!",
Toast.LENGTH_SHORT
).show()
}
}
setImageDrawable(ResourcesCompat.getDrawableForDensity(
context.resources, context.applicationInfo.icon, 640, null)
)

View File

@ -1,9 +1,6 @@
package io.github.wulkanowy.ui.modules.attendance.calculator
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
@ -12,9 +9,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.pojos.AttendanceData
import io.github.wulkanowy.databinding.FragmentAttendanceCalculatorBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.settings.appearance.AppearanceFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@ -38,12 +33,6 @@ class AttendanceCalculatorFragment :
override val isViewEmpty get() = attendanceCalculatorAdapter.items.isEmpty()
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAttendanceCalculatorBinding.bind(view)
@ -51,19 +40,6 @@ class AttendanceCalculatorFragment :
presenter.onAttachView(this)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.action_menu_attendance_calculator, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.attendance_calculator_menu_settings) presenter.onSettingsSelected()
else false
}
override fun openSettingsView() {
(activity as? MainActivity)?.pushView(AppearanceFragment.withFocusedPreference(getString(R.string.pref_key_attendance_target)))
}
override fun initView() {
with(binding.attendanceCalculatorRecycler) {
layoutManager = LinearLayoutManager(context)
@ -74,11 +50,7 @@ class AttendanceCalculatorFragment :
with(binding) {
attendanceCalculatorSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceCalculatorSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
attendanceCalculatorSwipe.setProgressBackgroundColorSchemeColor(
requireContext().getThemeAttrColor(
R.attr.colorSwipeRefresh
)
)
attendanceCalculatorSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
attendanceCalculatorErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceCalculatorErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}

View File

@ -1,11 +1,6 @@
package io.github.wulkanowy.ui.modules.attendance.calculator
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceIntermediate
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.domain.attendance.GetAttendanceCalculatorDataUseCase
@ -86,9 +81,4 @@ class AttendanceCalculatorPresenter @Inject constructor(
} else showError(message, error)
}
}
fun onSettingsSelected(): Boolean {
view?.openSettingsView()
return true
}
}

View File

@ -26,6 +26,4 @@ interface AttendanceCalculatorView : BaseView {
fun updateData(data: List<AttendanceData>)
fun clearView()
fun openSettingsView()
}

View File

@ -5,7 +5,6 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.ItemAttendanceSummaryBinding
import io.github.wulkanowy.databinding.ScrollableHeaderAttendanceSummaryBinding
import io.github.wulkanowy.utils.calculatePercentage
@ -14,13 +13,9 @@ import java.time.Month
import java.util.Locale
import javax.inject.Inject
class AttendanceSummaryAdapter @Inject constructor(
private val preferencesRepository: PreferencesRepository
) :
class AttendanceSummaryAdapter @Inject constructor() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val attendancePercentage = preferencesRepository.attendancePercentage
private enum class ViewType(val id: Int) {
HEADER(1),
ITEM(2)
@ -53,10 +48,7 @@ class AttendanceSummaryAdapter @Inject constructor(
}
private fun bindHeaderViewHolder(binding: ScrollableHeaderAttendanceSummaryBinding) {
binding.attendanceSummaryScrollableHeaderPercentage.text = formatPercentage(
attendancePercentage ?:
items.calculatePercentage()
)
binding.attendanceSummaryScrollableHeaderPercentage.text = formatPercentage(items.calculatePercentage())
}
private fun bindItemViewHolder(binding: ItemAttendanceSummaryBinding, position: Int) {
@ -68,8 +60,8 @@ class AttendanceSummaryAdapter @Inject constructor(
else -> item.month.getFormattedName()
}
attendanceSummaryPercentage.text = when (position) {
-1 -> formatPercentage(attendancePercentage ?: item.calculatePercentage())
else -> formatPercentage(attendancePercentage ?: item.calculatePercentage())
-1 -> formatPercentage(items.calculatePercentage())
else -> formatPercentage(item.calculatePercentage())
}
attendanceSummaryPresent.text = item.presence.toString()

View File

@ -5,7 +5,6 @@ import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class AuthPresenter @Inject constructor(
@ -27,12 +26,8 @@ class AuthPresenter @Inject constructor(
private fun loadName() {
presenterScope.launch {
runCatching {
studentRepository.getCurrentStudent(false)
.studentName
.replace(" ", "\u00A0")
}
.onSuccess { view?.showDescriptionWithName(it) }
runCatching { studentRepository.getCurrentStudent(false) }
.onSuccess { view?.showDescriptionWithName(it.studentName) }
.onFailure { errorHandler.dispatch(it) }
}
}
@ -62,9 +57,8 @@ class AuthPresenter @Inject constructor(
val semester = semesterRepository.getCurrentSemester(student)
val isSuccess = studentRepository.authorizePermission(student, semester, pesel)
Timber.d("Auth succeed: $isSuccess")
if (isSuccess) {
studentRepository.refreshStudentAfterAuthorize(student, semester)
studentRepository.refreshStudentName(student, semester)
}
isSuccess
}
@ -74,7 +68,6 @@ class AuthPresenter @Inject constructor(
view?.showContent(true)
}
.onSuccess {
Timber.d("Auth fully succeed: $it")
if (it) {
view?.showSuccess(true)
view?.showContent(false)

View File

@ -59,7 +59,7 @@ class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
webView = this
with(settings) {
javaScriptEnabled = true
userAgentString = wulkanowySdkFactory.createBase().userAgent
userAgentString = wulkanowySdkFactory.create().userAgent
}
webViewClient = object : WebViewClient() {

View File

@ -30,7 +30,6 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment
import io.github.wulkanowy.ui.modules.panicmode.PanicModeFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.capitalise
@ -126,7 +125,6 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
mainActivity.pushView(ConferenceFragment.newInstance())
}
onAdminMessageClickListener = presenter::onAdminMessageSelected
onPanicButtonClickListener = presenter::onPanicButtonClicked
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
@ -210,11 +208,7 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
binding = binding.dashboardErrorAdminMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
onPanicButtonClickListener = presenter::onPanicButtonClicked,
).bind(
item = adminMessageItem.adminMessage,
showPanicButton = true,
)
).bind(adminMessageItem.adminMessage)
}
}
@ -242,10 +236,6 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
requireContext().openInternetBrowser(url)
}
override fun openPanicWebView(url: String) {
(requireActivity() as MainActivity).pushView(PanicModeFragment.newInstance(url))
}
override fun onDestroyView() {
dashboardAdapter.clearTimers()
presenter.onDetachView()

View File

@ -147,17 +147,6 @@ sealed class DashboardItem(val type: Type) {
EXAMS,
CONFERENCES,
}
enum class HiddenAttendanceTile {
UNEXCUSED_ABSENCE,
EXEMPTION,
EXCUSED_LATENESS,
UNEXCUSED_LATENESS,
PRESENT,
DELETED,
EXCUSED_ABSENCE,
UNKNOWN,
}
}
fun DashboardItem.Tile.toDashboardItemType() = when (this) {

View File

@ -11,7 +11,6 @@ import io.github.wulkanowy.data.errorOrNull
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
import io.github.wulkanowy.data.repositories.ConferenceRepository
import io.github.wulkanowy.data.repositories.ExamRepository
@ -24,7 +23,6 @@ import io.github.wulkanowy.data.repositories.SchoolAnnouncementRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase
import io.github.wulkanowy.ui.base.BasePresenter
@ -46,7 +44,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber
import java.time.Instant
import java.time.LocalDate
@ -285,26 +282,8 @@ class DashboardPresenter @Inject constructor(
url?.let { view?.openInternetBrowser(it) }
}
fun onPanicButtonClicked() {
resourceFlow { studentRepository.getCurrentStudent() }
.onResourceError { errorHandler.dispatch(it) }
.onResourceSuccess {
val baseUrl = it.scrapperBaseUrl.toHttpUrl()
val urlToOpen = baseUrl.newBuilder()
.host("uonetplus${it.scrapperDomainSuffix}.${baseUrl.host}")
.addPathSegment(it.symbol)
.build()
.toString()
view?.openPanicWebView(urlToOpen)
}
.launch("panic_button")
}
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flow {
val attendancePercentage = preferencesRepository.attendancePercentage
val selectedTiles = selectedDashboardTiles
val flowSuccess = flowOf(Resource.Success(null))
@ -357,7 +336,7 @@ class DashboardPresenter @Inject constructor(
} else null
},
attendancePercentage = DashboardItem.HorizontalGroup.Cell(
data = attendancePercentage ?: attendanceResource.dataOrNull?.calculatePercentage(),
data = attendanceResource.dataOrNull?.calculatePercentage(),
error = attendanceResource.errorOrNull != null,
isLoading = attendanceResource is Resource.Loading,
),

View File

@ -31,6 +31,4 @@ interface DashboardView : BaseView {
fun openNotificationsCenterView()
fun openInternetBrowser(url: String)
fun openPanicWebView(url: String)
}

View File

@ -59,8 +59,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
var onAdminMessageClickListener: (String?) -> Unit = {}
var onPanicButtonClickListener: () -> Unit = {}
var onAdminMessageDismissClickListener: (AdminMessage) -> Unit = {}
val items = mutableListOf<DashboardItem>()
@ -88,46 +86,35 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
DashboardItem.Type.ACCOUNT.ordinal -> AccountViewHolder(
ItemDashboardAccountBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.HORIZONTAL_GROUP.ordinal -> HorizontalGroupViewHolder(
ItemDashboardHorizontalGroupBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.GRADES.ordinal -> GradesViewHolder(
ItemDashboardGradesBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.LESSONS.ordinal -> LessonsViewHolder(
ItemDashboardLessonsBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.HOMEWORK.ordinal -> HomeworkViewHolder(
ItemDashboardHomeworkBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.ANNOUNCEMENTS.ordinal -> AnnouncementsViewHolder(
ItemDashboardAnnouncementsBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.EXAMS.ordinal -> ExamsViewHolder(
ItemDashboardExamsBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.CONFERENCES.ordinal -> ConferencesViewHolder(
ItemDashboardConferencesBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder(
ItemDashboardAdminMessageBinding.inflate(inflater, parent, false),
onAdminMessageDismissClickListener = onAdminMessageDismissClickListener,
onAdminMessageClickListener = onAdminMessageClickListener,
onPanicButtonClickListener = onPanicButtonClickListener,
)
DashboardItem.Type.ADS.ordinal -> AdsViewHolder(
ItemDashboardAdsBinding.inflate(inflater, parent, false)
)
else -> throw IllegalArgumentException()
}
}
@ -142,11 +129,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position)
is ExamsViewHolder -> bindExamsViewHolder(holder, position)
is ConferencesViewHolder -> bindConferencesViewHolder(holder, position)
is AdminMessageViewHolder -> holder.bind(
(items[position] as DashboardItem.AdminMessages).adminMessage,
showPanicButton = true
)
is AdminMessageViewHolder -> holder.bind((items[position] as DashboardItem.AdminMessages).adminMessage)
is AdsViewHolder -> bindAdsViewHolder(holder, position)
}
}
@ -257,15 +240,12 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
attendancePercentage == null || attendancePercentage == .0 -> {
root.context.getThemeAttrColor(R.attr.colorOnSurface)
}
attendancePercentage <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> {
root.context.getThemeAttrColor(R.attr.colorPrimary)
}
attendancePercentage <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> {
root.context.getThemeAttrColor(R.attr.colorTimetableChange)
}
else -> root.context.getThemeAttrColor(R.attr.colorOnSurface)
}
val attendanceString = if (attendancePercentage == null || attendancePercentage == .0) {
@ -356,28 +336,24 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
}
tomorrowTimetable.isNotEmpty() -> {
dateToNavigate = tomorrowDate
updateLessonView(item, tomorrowTimetable, binding)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
}
currentDayHeader != null && currentDayHeader.content.isNotBlank() -> {
dateToNavigate = currentDate
updateLessonView(item, emptyList(), binding, currentDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
}
tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> {
dateToNavigate = tomorrowDate
updateLessonView(item, emptyList(), binding, tomorrowDayHeader)
binding.dashboardLessonsItemTitleTomorrow.isVisible = true
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
}
else -> {
dateToNavigate = currentDate
updateLessonView(item, emptyList(), binding)
@ -485,7 +461,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
firstTitleText =
context.getString(R.string.dashboard_timetable_first_lesson_title_moment)
}
minutesToStartLesson < 240 -> {
firstTitleAndValueTextColor =
context.getThemeAttrColor(R.attr.colorOnSurface)
@ -493,7 +468,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
firstTitleText =
context.getString(R.string.dashboard_timetable_first_lesson_title_soon)
}
else -> {
firstTitleAndValueTextColor =
context.getThemeAttrColor(R.attr.colorOnSurface)

View File

@ -13,10 +13,9 @@ class AdminMessageViewHolder(
private val binding: ItemDashboardAdminMessageBinding,
private val onAdminMessageDismissClickListener: (AdminMessage) -> Unit,
private val onAdminMessageClickListener: (String?) -> Unit,
private val onPanicButtonClickListener: () -> Unit,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: AdminMessage?, showPanicButton: Boolean = false) {
fun bind(item: AdminMessage?) {
item ?: return
val context = binding.root.context
@ -49,14 +48,10 @@ class AdminMessageViewHolder(
dashboardAdminMessageItemClose.setOnClickListener {
onAdminMessageDismissClickListener(item)
}
dashboardPanicSection.root.isVisible = showPanicButton
dashboardPanicSection.dashboardPanicButton.setOnClickListener {
onPanicButtonClickListener()
}
dashboardAdminMessage.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url ->
dashboardAdminMessage.setOnClickListener { onAdminMessageClickListener(url) }
root.setOnClickListener { onAdminMessageClickListener(url) }
}
}
}

View File

@ -26,7 +26,5 @@ private fun generateSummary(subject: String, predicted: String, final: String) =
proposedPoints = "",
finalPoints = "",
pointsSum = "",
average = .0,
pointsSumAllYear = null,
averageAllYear = null,
average = .0
)

View File

@ -19,6 +19,6 @@ val debugSchoolAnnouncementItems = listOf(
private fun generateAnnouncement(subject: String, content: String) = SchoolAnnouncement(
subject = subject,
content = content,
studentId = 0,
userLoginId = 0,
date = LocalDate.now()
)

View File

@ -6,6 +6,6 @@ enum class GradeAverageMode(val value: String) {
BOTH_SEMESTERS("both_semesters");
companion object {
fun getByValue(value: String) = entries.firstOrNull { it.value == value } ?: ONE_SEMESTER
fun getByValue(value: String) = values().firstOrNull { it.value == value } ?: ONE_SEMESTER
}
}

View File

@ -266,9 +266,7 @@ class GradeAverageProvider @Inject constructor(
proposedPoints = "",
finalPoints = "",
pointsSum = "",
pointsSumAllYear = null,
average = .0,
averageAllYear = null,
average = .0
)
}
@ -296,15 +294,13 @@ class GradeAverageProvider @Inject constructor(
proposedPoints = "",
finalPoints = "",
pointsSum = "",
pointsSumAllYear = null,
average = when {
calcAverage -> details
.updateModifiers(student, params)
.calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
else -> .0
},
averageAllYear = null,
}
)
}
}

View File

@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
@ -30,6 +31,14 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
@Inject
lateinit var presenter: GradePresenter
private val pagerAdapter by lazy {
BaseFragmentPagerAdapter(
fragmentManager = childFragmentManager,
pagesCount = 3,
lifecycle = lifecycle,
)
}
private var semesterSwitchMenu: MenuItem? = null
companion object {
@ -43,8 +52,6 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override val currentPageIndex get() = binding.gradeViewPager.currentItem
private var pagerAdapter: BaseFragmentPagerAdapter? = null
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -64,26 +71,13 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
override fun initView() {
with(binding) {
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
}
override fun initTabs(pageCount: Int) {
pagerAdapter = BaseFragmentPagerAdapter(
lifecycle = lifecycle,
pagesCount = pageCount,
fragmentManager = childFragmentManager
)
with(binding.gradeViewPager) {
adapter = pagerAdapter
offscreenPageLimit = 3
setOnSelectPageListener(presenter::onPageSelected)
}
with(pagerAdapter!!) {
with(pagerAdapter) {
containerId = binding.gradeViewPager.id
titleFactory = {
when (it) {
@ -105,6 +99,11 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
binding.gradeTabLayout.elevation = requireContext().dpToPx(4f)
with(binding) {
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -170,20 +169,19 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
override fun notifyChildLoadData(index: Int, semesterId: Int, forceRefresh: Boolean) {
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)
?.onParentLoadData(semesterId, forceRefresh)
}
override fun notifyChildParentReselected(index: Int) {
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentReselected()
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentReselected()
}
override fun notifyChildSemesterChange(index: Int) {
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester()
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester()
}
override fun onDestroyView() {
pagerAdapter = null
presenter.onDetachView()
super.onDestroyView()
}

View File

@ -22,8 +22,11 @@ class GradePresenter @Inject constructor(
) : BasePresenter<GradeView>(errorHandler, studentRepository) {
private var selectedIndex = 0
private var schoolYear = 0
private var availableSemesters = emptyList<Semester>()
private var semesters = emptyList<Semester>()
private val loadedSemesterId = mutableMapOf<Int, Int>()
private lateinit var lastError: Throwable
@ -37,7 +40,7 @@ class GradePresenter @Inject constructor(
}
fun onCreateMenu() {
if (availableSemesters.isEmpty()) view?.showSemesterSwitch(false)
if (semesters.isEmpty()) view?.showSemesterSwitch(false)
}
fun onViewReselected() {
@ -46,8 +49,8 @@ class GradePresenter @Inject constructor(
}
fun onSemesterSwitch(): Boolean {
if (availableSemesters.isNotEmpty()) {
view?.showSemesterDialog(selectedIndex - 1, availableSemesters.take(2))
if (semesters.isNotEmpty()) {
view?.showSemesterDialog(selectedIndex - 1, semesters.take(2))
}
return true
}
@ -80,7 +83,7 @@ class GradePresenter @Inject constructor(
}
fun onPageSelected(index: Int) {
if (availableSemesters.isNotEmpty()) loadChild(index)
if (semesters.isNotEmpty()) loadChild(index)
}
fun onRetry() {
@ -98,24 +101,16 @@ class GradePresenter @Inject constructor(
private fun loadData() {
resourceFlow {
val student = studentRepository.getCurrentStudent()
val semesters = semesterRepository.getSemesters(student, refreshOnNoCurrent = true)
student to semesters
semesterRepository.getSemesters(student, refreshOnNoCurrent = true)
}
.logResourceStatus("load grade data")
.onResourceData { (student, semesters) ->
val currentSemester = semesters.getCurrentOrLast()
selectedIndex =
if (selectedIndex == 0) currentSemester.semesterName else selectedIndex
schoolYear = currentSemester.schoolYear
availableSemesters = semesters.filter { semester ->
semester.diaryId == currentSemester.diaryId
}
.onResourceData {
val current = it.getCurrentOrLast()
selectedIndex = if (selectedIndex == 0) current.semesterName else selectedIndex
schoolYear = current.schoolYear
semesters = it.filter { semester -> semester.diaryId == current.diaryId }
view?.setCurrentSemesterName(current.semesterName, schoolYear)
view?.run {
initTabs(if (student.isEduOne == true) 2 else 3)
setCurrentSemesterName(currentSemester.semesterName, schoolYear)
Timber.i("Loading grade data: Attempt load index $currentPageIndex")
loadChild(currentPageIndex)
showErrorView(false)
@ -136,10 +131,10 @@ class GradePresenter @Inject constructor(
}
private fun loadChild(index: Int, forceRefresh: Boolean = false) {
Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${availableSemesters.joinToString { it.semesterName.toString() }}")
Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${semesters.joinToString { it.semesterName.toString() }}")
val newSelectedSemesterId = try {
availableSemesters.first { it.semesterName == selectedIndex }.semesterId
semesters.first { it.semesterName == selectedIndex }.semesterId
} catch (e: NoSuchElementException) {
Timber.e(e, "Selected semester no exists")
return

View File

@ -9,8 +9,6 @@ interface GradeView : BaseView {
fun initView()
fun initTabs(pageCount: Int)
fun showContent(show: Boolean)
fun showProgress(show: Boolean)

View File

@ -96,11 +96,9 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
ViewType.HEADER.id -> HeaderViewHolder(
HeaderGradeDetailsBinding.inflate(inflater, parent, false)
)
ViewType.ITEM.id -> ItemViewHolder(
ItemGradeDetailsBinding.inflate(inflater, parent, false)
)
else -> throw IllegalStateException()
}
}
@ -112,7 +110,6 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
header = items[position].value as GradeDetailsHeader,
position = position
)
is ItemViewHolder -> bindItemViewHolder(
holder = holder,
grade = items[position].value as Grade
@ -136,10 +133,6 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
maxLines = if (expandedPositions[headerPosition]) 2 else 1
}
gradeHeaderAverage.text = formatAverage(header.average, root.context.resources)
with(gradeHeaderAverageAllYear) {
isVisible = header.averageAllYear != null && header.averageAllYear != .0
text = formatAverageAllYear(header.averageAllYear, root.context.resources)
}
gradeHeaderPointsSum.text =
context.getString(R.string.grade_points_sum, header.pointsSum)
gradeHeaderPointsSum.isVisible = !header.pointsSum.isNullOrEmpty()
@ -240,13 +233,6 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
resources.getString(R.string.grade_average, average)
}
private fun formatAverageAllYear(average: Double?, resources: Resources) =
if (average == null || average == .0) {
resources.getString(R.string.grade_no_average)
} else {
resources.getString(R.string.grade_average_year, average)
}
private class HeaderViewHolder(val binding: HeaderGradeDetailsBinding) :
RecyclerView.ViewHolder(binding.root)

View File

@ -13,7 +13,6 @@ data class GradeDetailsItem(
data class GradeDetailsHeader(
val subject: String,
val average: Double?,
val averageAllYear: Double?,
val pointsSum: String?,
val grades: List<GradeDetailsItem>
) {

View File

@ -226,9 +226,8 @@ class GradeDetailsPresenter @Inject constructor(
GradeDetailsHeader(
subject = gradeSubject.subject,
average = gradeSubject.average,
averageAllYear = gradeSubject.summary.averageAllYear,
pointsSum = gradeSubject.points,
grades = subItems,
grades = subItems
).apply {
newGrades = gradeSubject.grades.filter { grade -> !grade.isRead }.size
}, ViewType.HEADER

View File

@ -3,7 +3,6 @@ package io.github.wulkanowy.ui.modules.grade.summary
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
@ -66,55 +65,37 @@ class GradeSummaryAdapter @Inject constructor(
val gradeSummaries = items
.filter { it.gradeDescriptive == null }
.map { it.gradeSummary }
val isSecondSemester = items.any { item ->
item.gradeSummary.let { it.averageAllYear != null && it.averageAllYear != .0 }
}
val context = binding.root.context
val finalItemsCount = gradeSummaries.count { isGradeValid(it.finalGrade) }
val calculatedSemesterItemsCount = gradeSummaries.count { value -> value.average != 0.0 }
val calculatedAnnualItemsCount =
gradeSummaries.count { value -> value.averageAllYear != 0.0 }
val calculatedItemsCount = gradeSummaries.count { value -> value.average != 0.0 }
val allItemsCount = gradeSummaries.count { !it.subject.equals("zachowanie", true) }
val finalAverage = gradeSummaries.calcFinalAverage(
plusModifier = preferencesRepository.gradePlusModifier,
minusModifier = preferencesRepository.gradeMinusModifier,
preferencesRepository.gradePlusModifier,
preferencesRepository.gradeMinusModifier
)
val calculatedSemesterAverage = gradeSummaries.filter { value -> value.average != 0.0 }
val calculatedAverage = gradeSummaries.filter { value -> value.average != 0.0 }
.map { values -> values.average }
.reversed() // fix average precision
.average()
.let { if (it.isNaN()) 0.0 else it }
val calculatedAnnualAverage = gradeSummaries.filter { value -> value.averageAllYear != 0.0 }
.mapNotNull { values -> values.averageAllYear }
.reversed() // fix average precision
.average()
.let { if (it.isNaN()) 0.0 else it }
with(binding) {
gradeSummaryScrollableHeaderCalculated.text = formatAverage(calculatedSemesterAverage)
gradeSummaryScrollableHeaderCalculatedAnnual.text =
formatAverage(calculatedAnnualAverage)
gradeSummaryScrollableHeaderFinal.text = formatAverage(finalAverage)
gradeSummaryScrollableHeaderFinalSubjectCount.text = context.getString(
R.string.grade_summary_from_subjects,
finalItemsCount,
allItemsCount
)
gradeSummaryScrollableHeaderCalculated.text = formatAverage(calculatedAverage)
gradeSummaryScrollableHeaderFinalSubjectCount.text =
context.getString(
R.string.grade_summary_from_subjects,
finalItemsCount,
allItemsCount
)
gradeSummaryScrollableHeaderCalculatedSubjectCount.text = context.getString(
R.string.grade_summary_from_subjects,
calculatedSemesterItemsCount,
calculatedItemsCount,
allItemsCount
)
gradeSummaryScrollableHeaderCalculatedSubjectCountAnnual.text = context.getString(
R.string.grade_summary_from_subjects,
calculatedAnnualItemsCount,
allItemsCount
)
gradeSummaryScrollableHeaderCalculatedAnnualContainer.isVisible = isSecondSemester
gradeSummaryCalculatedAverageHelp.setOnClickListener { onCalculatedHelpClickListener() }
gradeSummaryCalculatedAverageHelpAnnual.setOnClickListener { onCalculatedHelpClickListener() }
gradeSummaryFinalAverageHelp.setOnClickListener { onFinalHelpClickListener() }
}
}
@ -126,12 +107,7 @@ class GradeSummaryAdapter @Inject constructor(
with(binding) {
gradeSummaryItemTitle.text = gradeSummary.subject
gradeSummaryItemPoints.text = gradeSummary.pointsSum
gradeSummaryItemAverage.text = formatAverage(gradeSummary.average, "")
gradeSummaryItemAverageAllYear.text = gradeSummary.averageAllYear?.let {
formatAverage(it, "")
}
gradeSummaryItemPredicted.text =
"${gradeSummary.predictedGrade} ${gradeSummary.proposedPoints}".trim()
gradeSummaryItemFinal.text =
@ -140,12 +116,6 @@ class GradeSummaryAdapter @Inject constructor(
root.context.getString(R.string.all_no_data)
}
gradeSummaryItemAverageContainer.isVisible = gradeSummary.average != .0
gradeSummaryItemAverageDivider.isVisible = gradeSummaryItemAverageContainer.isVisible
gradeSummaryItemAverageAllYearContainer.isGone =
gradeSummary.averageAllYear == null || gradeSummary.averageAllYear == .0
gradeSummaryItemAverageAllYearDivider.isGone =
gradeSummaryItemAverageAllYearContainer.isGone
gradeSummaryItemFinalDivider.isVisible = gradeDescriptive == null
gradeSummaryItemPredictedDivider.isVisible = gradeDescriptive == null
gradeSummaryItemPointsDivider.isVisible = gradeDescriptive == null
@ -153,7 +123,6 @@ class GradeSummaryAdapter @Inject constructor(
gradeSummaryItemFinalContainer.isVisible = gradeDescriptive == null
gradeSummaryItemDescriptiveContainer.isVisible = gradeDescriptive != null
gradeSummaryItemPointsContainer.isVisible = gradeSummary.pointsSum.isNotBlank()
gradeSummaryItemPointsDivider.isVisible = gradeSummaryItemPointsContainer.isVisible
}
}

View File

@ -98,9 +98,7 @@ class HomeworkAddDialog : BaseDialogFragment<DialogHomeworkAddBinding>(), Homewo
rangeEnd = LocalDate.now().lastSchoolDayInSchoolYear,
onDateSelected = {
date = it
if (isAdded) {
binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString())
}
binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString())
}
)
}

View File

@ -118,6 +118,5 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
override fun onResume() {
super.onResume()
inAppUpdateHelper.onResume()
presenter.updateSdkMappings()
}
}

View File

@ -1,15 +1,12 @@
package io.github.wulkanowy.ui.modules.login
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class LoginPresenter @Inject constructor(
private val wulkanowyRepository: WulkanowyRepository,
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter<LoginView>(errorHandler, studentRepository) {
@ -19,11 +16,4 @@ class LoginPresenter @Inject constructor(
view.initView()
Timber.i("Login view was initialized")
}
fun updateSdkMappings() {
presenterScope.launch {
runCatching { wulkanowyRepository.fetchMapping() }
.onFailure { Timber.e(it) }
}
}
}

View File

@ -238,7 +238,6 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
binding = binding.loginFormMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
onPanicButtonClickListener = {},
).bind(message)
binding.loginFormMessage.root.isVisible = message != null
}

Some files were not shown because too many files have changed in this diff Show More