Compare commits

..

6 Commits

Author SHA1 Message Date
Faierbel
2bae532f6d Merge branch 'develop' into bugfix/classid-zero 2024-05-05 10:46:47 +02:00
Faierbel
157e04b239 Add clearing all tables dependent on classId 2024-04-21 04:01:21 +02:00
Faierbel
2a0ac7f91e Add test for StudentDao 2024-04-14 17:48:44 +02:00
Faierbel
d7b1a08098 Merge branch 'develop' into bugfix/classid-zero 2024-04-14 16:32:24 +02:00
Faierbel
985be92a4d Fix tests 2024-04-14 02:57:05 +02:00
Faierbel
6616a313e2 Fix checking classId condition when is eduOne 2024-04-14 02:48:26 +02:00
127 changed files with 1220 additions and 4131 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 161
versionName "2.6.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.25d
updatePriority = 1
enabled.set(false)
}
@ -187,17 +187,16 @@ 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.6.0'
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-splashscreen:1.0.1'
@ -205,7 +204,6 @@ dependencies {
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.7.0"
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"

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,9 +42,9 @@
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"

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,21 +1,13 @@
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
@ -24,24 +16,18 @@ 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 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 +36,14 @@ class WulkanowySdkFactory @Inject constructor(
addInterceptor(chuckerInterceptor, network = true)
}
fun createBase() = 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()
}
}
}
}
fun create() = sdk
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 {
private fun buildSdk(student: Student, semester: Semester?, isStudentEduOne: Boolean): Sdk {
return create().apply {
email = student.email
password = student.password

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

@ -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

@ -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

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.School
import io.github.wulkanowy.data.db.entities.Student
import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton
@ -11,5 +12,16 @@ import javax.inject.Singleton
interface SchoolDao : BaseDao<School> {
@Query("SELECT * FROM School WHERE student_id = :studentId AND class_id = :classId")
fun load(studentId: Int, classId: Int): Flow<School?>
fun loadWithClassId(studentId: Int, classId: Int): Flow<School?>
@Query("SELECT * FROM School WHERE student_id = :studentId")
fun loadNoClassId(studentId: Int): Flow<School?>
fun load(student: Student): Flow<School?> {
return if (student.isEduOne == true) {
loadNoClassId(student.studentId)
} else {
loadWithClassId(student.studentId, student.classId)
}
}
}

View File

@ -5,6 +5,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import javax.inject.Singleton
@Singleton
@ -14,6 +15,17 @@ 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)")
suspend fun loadAll(studentId: Int, classId: Int): List<Semester>
@Query("SELECT * FROM Semesters WHERE (student_id = :studentId AND class_id = :classId)")
suspend fun loadAllWithClassId(studentId: Int, classId: Int): List<Semester>
@Query("SELECT * FROM Semesters WHERE student_id = :studentId")
suspend fun loadAllNoClassId(studentId: Int): List<Semester>
suspend fun loadAll(student: Student): List<Semester> {
return if (student.isEduOne == true) {
loadAllNoClassId(student.studentId)
} else {
loadAllWithClassId(student.studentId, student.classId)
}
}
}

View File

@ -47,13 +47,9 @@ 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")
abstract suspend fun loadStudentWithSemestersById(id: Long): Map<Student, List<Semester>>
@Query("UPDATE Students SET is_current = 1 WHERE id = :id")
abstract suspend fun updateCurrent(id: Long)

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Teacher
import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton
@ -11,5 +12,16 @@ import javax.inject.Singleton
interface TeacherDao : BaseDao<Teacher> {
@Query("SELECT * FROM Teachers WHERE student_id = :studentId AND class_id = :classId")
fun loadAll(studentId: Int, classId: Int): Flow<List<Teacher>>
fun loadAllWithClassId(studentId: Int, classId: Int): Flow<List<Teacher>>
@Query("SELECT * FROM Teachers WHERE student_id = :studentId")
fun loadAllNoClassId(studentId: Int): Flow<List<Teacher>>
fun loadAll(student: Student): Flow<List<Teacher>> {
return if (student.isEduOne == true) {
loadAllNoClassId(student.studentId)
} else {
loadAllWithClassId(student.studentId, student.classId)
}
}
}

View File

@ -6,6 +6,15 @@ 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")
db.execSQL("DROP TABLE IF EXISTS `Semesters`")
db.execSQL("DROP TABLE IF EXISTS `School`")
db.execSQL("DROP TABLE IF EXISTS `Teachers`")
db.execSQL("CREATE TABLE IF NOT EXISTS `Semesters` (`student_id` INTEGER NOT NULL, `diary_id` INTEGER NOT NULL, `kindergarten_diary_id` INTEGER NOT NULL DEFAULT 0, `diary_name` TEXT NOT NULL, `school_year` INTEGER NOT NULL, `semester_id` INTEGER NOT NULL, `semester_name` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `unit_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `is_current` INTEGER NOT NULL)")
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_Semesters_student_id_diary_id_kindergarten_diary_id_semester_id` ON `Semesters` (`student_id`, `diary_id`, `kindergarten_diary_id`, `semester_id`)")
db.execSQL("CREATE TABLE IF NOT EXISTS `School` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `address` TEXT NOT NULL, `contact` TEXT NOT NULL, `headmaster` TEXT NOT NULL, `pedagogue` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)")
db.execSQL("CREATE TABLE IF NOT EXISTS `Teachers` (`student_id` INTEGER NOT NULL, `class_id` INTEGER NOT NULL, `subject` TEXT NOT NULL, `name` TEXT NOT NULL, `short_name` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)")
db.execSQL("UPDATE Students SET is_edu_one = NULL")
}
}

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

@ -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

@ -0,0 +1,4 @@
package io.github.wulkanowy.data.exceptions
class NoSuchStudentException(id: Long) :
Exception("There is no student with id $id in database")

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

@ -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,10 +7,8 @@ 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
@ -26,7 +24,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 +36,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(
@ -305,60 +301,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 +310,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 +375,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

@ -36,7 +36,7 @@ class SchoolRepository @Inject constructor(
)
it == null || forceRefresh || isExpired
},
query = { schoolDb.load(semester.studentId, semester.classId) },
query = { schoolDb.load(student) },
fetch = {
wulkanowySdkFactory.create(student, semester)
.getSchool()

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

@ -27,11 +27,11 @@ class SemesterRepository @Inject constructor(
forceRefresh: Boolean = false,
refreshOnNoCurrent: Boolean = false
) = withContext(dispatchers.io) {
val semesters = semesterDb.loadAll(student.studentId, student.classId)
val semesters = semesterDb.loadAll(student)
if (isShouldFetch(student, semesters, forceRefresh, refreshOnNoCurrent)) {
refreshSemesters(student)
semesterDb.loadAll(student.studentId, student.classId)
semesterDb.loadAll(student)
} else semesters
}
@ -69,7 +69,7 @@ class SemesterRepository @Inject constructor(
return
}
val old = semesterDb.loadAll(student.studentId, student.classId)
val old = semesterDb.loadAll(student)
semesterDb.removeOldAndSaveNew(
oldItems = old uniqueSubtract new,
newItems = new uniqueSubtract old,

View File

@ -12,6 +12,7 @@ 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.exceptions.NoSuchStudentException
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.mappers.mapToPojo
import io.github.wulkanowy.data.pojos.RegisterUser
@ -65,7 +66,8 @@ class StudentRepository @Inject constructor(
.mapToPojo(password)
.also { it.logErrors() }
suspend fun getSavedStudents(decryptPass: Boolean = true): List<StudentWithSemesters> {
@Deprecated("Semesters are not synced within this method and students with empty semesters are not returned")
suspend fun getSavedStudentsWithSemesters(decryptPass: Boolean = true): List<StudentWithSemesters> {
return studentDb.loadStudentsWithSemesters().map { (student, semesters) ->
StudentWithSemesters(
student = student.apply {
@ -80,22 +82,25 @@ class StudentRepository @Inject constructor(
}
}
suspend fun getSavedStudentById(id: Long, decryptPass: Boolean = true): StudentWithSemesters? =
studentDb.loadStudentWithSemestersById(id).let { res ->
StudentWithSemesters(
student = res.keys.firstOrNull() ?: return null,
semesters = res.values.first(),
)
}.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) {
suspend fun getSavedStudents(decryptPass: Boolean = true): List<Student> {
val students = studentDb.loadAll()
if (!decryptPass) return students
return students.map { student ->
if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) {
return@map student
}
student.apply {
password = withContext(dispatchers.io) {
scrambler.decrypt(student.password)
}
}
}
}
suspend fun getStudentById(id: Long, decryptPass: Boolean = true): Student {
val student = studentDb.loadById(id) ?: throw NoCurrentStudentException()
val student = studentDb.loadById(id) ?: throw NoSuchStudentException(id)
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.HEBE) {
student.password = withContext(dispatchers.io) {
@ -123,7 +128,7 @@ class StudentRepository @Inject constructor(
return
}
val currentStudentSemesters = semesterDb.loadAll(student.studentId, student.classId)
val currentStudentSemesters = semesterDb.loadAll(student)
if (currentStudentSemesters.isEmpty()) {
Timber.d("Check isAuthorized: apply empty semesters workaround")
semesterDb.insertSemesters(
@ -181,8 +186,8 @@ class StudentRepository @Inject constructor(
}
}
suspend fun switchStudent(studentWithSemesters: StudentWithSemesters) {
studentDb.switchCurrent(studentWithSemesters.student.id)
suspend fun switchStudent(student: Student) {
studentDb.switchCurrent(student.id)
}
suspend fun logoutStudent(student: Student) = studentDb.delete(student)
@ -190,8 +195,8 @@ class StudentRepository @Inject constructor(
suspend fun updateStudentNickAndAvatar(studentNickAndAvatar: StudentNickAndAvatar) =
studentDb.update(studentNickAndAvatar)
suspend fun isOneUniqueStudent() = getSavedStudents(false)
.distinctBy { it.student.studentName }.size == 1
suspend fun isOneUniqueStudent() = studentDb.loadAll()
.distinctBy { it.studentName }.size == 1
suspend fun authorizePermission(student: Student, semester: Semester, pesel: String) =
wulkanowySdkFactory.create(student, semester)
@ -199,7 +204,7 @@ class StudentRepository @Inject constructor(
suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) {
val wulkanowySdk = wulkanowySdkFactory.create(student, semester)
val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() }
val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Can't find student with id ${student.studentId}") }
.getOrNull() ?: return
@ -209,7 +214,7 @@ class StudentRepository @Inject constructor(
studentDb.update(studentName)
semesterDb.removeOldAndSaveNew(
oldItems = semesterDb.loadAll(student.studentId, semester.classId),
oldItems = semesterDb.loadAll(student),
newItems = newCurrentApiStudent.semesters.mapToEntities(newCurrentApiStudent.studentId)
)
}

View File

@ -35,7 +35,7 @@ class TeacherRepository @Inject constructor(
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, semester))
it.isEmpty() || forceRefresh || isExpired
},
query = { teacherDb.loadAll(semester.studentId, semester.classId) },
query = { teacherDb.loadAll(student) },
fetch = {
wulkanowySdkFactory.create(student, semester)
.getTeachers()

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

@ -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

@ -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,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

@ -33,7 +33,7 @@ class AccountPresenter @Inject constructor(
}
private fun loadData() {
resourceFlow { studentRepository.getSavedStudents(false) }
resourceFlow { studentRepository.getSavedStudentsWithSemesters(false) }
.logResourceStatus("load account data")
.onResourceSuccess { view?.updateData(createAccountItems(it)) }
.onResourceError(errorHandler::dispatch)

View File

@ -1,9 +1,15 @@
package io.github.wulkanowy.ui.modules.account.accountdetails
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceLoading
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
@ -14,6 +20,7 @@ import javax.inject.Inject
class AccountDetailsPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val semeRepository: SemesterRepository,
private val syncManager: SyncManager
) : BasePresenter<AccountDetailsView>(errorHandler, studentRepository) {
@ -46,7 +53,12 @@ class AccountDetailsPresenter @Inject constructor(
}
private fun loadData() {
resourceFlow { studentRepository.getSavedStudentById(studentId ?: -1) }
resourceFlow {
val student = studentRepository.getStudentById(studentId ?: -1)
val semesters = semeRepository.getSemesters(student)
StudentWithSemesters(student, semesters)
}
.logResourceStatus("loading account details view")
.onResourceLoading {
view?.run {
@ -85,7 +97,7 @@ class AccountDetailsPresenter @Inject constructor(
Timber.i("Select student ${studentWithSemesters!!.student.id}")
resourceFlow { studentRepository.switchStudent(studentWithSemesters!!) }
resourceFlow { studentRepository.switchStudent(studentWithSemesters!!.student) }
.logResourceStatus("change student")
.onResourceSuccess { view?.recreateMainView() }
.onResourceNotLoading { view?.popViewToMain() }
@ -122,10 +134,12 @@ class AccountDetailsPresenter @Inject constructor(
syncManager.stopSyncWorker()
openClearLoginView()
}
studentWithSemesters?.student?.isCurrent == true -> {
Timber.i("Logout result: Logout student and switch to another")
recreateMainView()
}
else -> {
Timber.i("Logout result: Logout student")
recreateMainView()

View File

@ -1,8 +1,12 @@
package io.github.wulkanowy.ui.modules.account.accountquick
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.account.AccountItem
@ -40,7 +44,7 @@ class AccountQuickPresenter @Inject constructor(
return
}
resourceFlow { studentRepository.switchStudent(studentWithSemesters) }
resourceFlow { studentRepository.switchStudent(studentWithSemesters.student) }
.logResourceStatus("change student")
.onResourceSuccess { view?.recreateMainView() }
.onResourceNotLoading { view?.popView() }

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

@ -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

@ -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

@ -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
}

View File

@ -118,7 +118,6 @@ class LoginStudentSelectFragment :
binding = binding.loginStudentSelectAdminMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
onPanicButtonClickListener = {},
).bind(adminMessage)
binding.loginStudentSelectAdminMessage.root.isVisible = adminMessage != null
}

View File

@ -80,7 +80,7 @@ class LoginStudentSelectPresenter @Inject constructor(
private fun loadData() {
resetSelectedState()
resourceFlow { studentRepository.getSavedStudents(false) }.onEach {
resourceFlow { studentRepository.getSavedStudentsWithSemesters(false) }.onEach {
students = it.dataOrNull.orEmpty()
when (it) {
is Resource.Loading -> Timber.d("Login student select students load started")

View File

@ -188,7 +188,6 @@ class LoginSymbolFragment :
binding = binding.loginSymbolAdminMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
onPanicButtonClickListener = {},
).bind(adminMessage)
binding.loginSymbolAdminMessage.root.isVisible = adminMessage != null
}

View File

@ -35,7 +35,7 @@ class LuckyNumberWidgetConfigurePresenter @Inject constructor(
}
private fun loadData() {
resourceFlow { studentRepository.getSavedStudents(false) }.onEach {
resourceFlow { studentRepository.getSavedStudentsWithSemesters(false) }.onEach {
when (it) {
is Resource.Loading -> Timber.d("Lucky number widget configure students data load")
is Resource.Success -> {

View File

@ -132,7 +132,7 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
private fun getLuckyNumber(studentId: Long, appWidgetId: Int) = runBlocking {
try {
val students = studentRepository.getSavedStudents()
val student = students.singleOrNull { it.student.id == studentId }?.student
val student = students.singleOrNull { it.id == studentId }
val currentStudent = when {
student != null -> student
studentId != 0L && studentRepository.isCurrentStudentSet() -> {

View File

@ -138,7 +138,6 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
override fun onResume() {
super.onResume()
inAppUpdateHelper.onResume()
presenter.updateSdkMappings()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {

View File

@ -6,7 +6,6 @@ import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
@ -30,7 +29,6 @@ class MainPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val preferencesRepository: PreferencesRepository,
private val wulkanowyRepository: WulkanowyRepository,
private val syncManager: SyncManager,
private val analytics: AnalyticsHelper,
private val json: Json,
@ -87,7 +85,7 @@ class MainPresenter @Inject constructor(
return
}
resourceFlow { studentRepository.getSavedStudents(false) }
resourceFlow { studentRepository.getSavedStudentsWithSemesters(false) }
.logResourceStatus("load student avatar")
.onResourceSuccess {
studentsWitSemesters = it
@ -201,11 +199,4 @@ class MainPresenter @Inject constructor(
.onFailure { errorHandler.dispatch(it) }
}
}
fun updateSdkMappings() {
presenterScope.launch {
runCatching { wulkanowyRepository.fetchMapping() }
.onFailure { Timber.e(it) }
}
}
}

View File

@ -27,7 +27,6 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
import io.github.wulkanowy.ui.modules.panicmode.PanicModeFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
@ -133,7 +132,6 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
)
messageTabErrorRetry.setOnClickListener { presenter.onRetry() }
messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() }
messageTabPanicSection.dashboardPanicButton.setOnClickListener { presenter.onPanicButtonClicked() }
}
setFragmentResultListener(requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!) { _, bundle ->
@ -285,10 +283,6 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
)
}
override fun openPanicWebView(url: String) {
(requireActivity() as MainActivity).pushView(PanicModeFragment.newInstance(url))
}
override fun hideKeyboard() {
activity?.hideSoftInput()
}

View File

@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.xdrop.fuzzywuzzy.FuzzySearch
import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber
import javax.inject.Inject
import kotlin.math.pow
@ -430,20 +429,4 @@ class MessageTabPresenter @Inject constructor(
+ dateRatio.toDouble().pow(2) * 2
).toInt()
}
fun onPanicButtonClicked() {
resourceFlow { studentRepository.getCurrentStudent() }
.onResourceError { errorHandler.dispatch(it) }
.onResourceSuccess {
val baseUrl = it.scrapperBaseUrl.toHttpUrl()
val urlToOpen = baseUrl.newBuilder()
.host("uonetplus${it.scrapperDomainSuffix}-wiadomosciplus.${baseUrl.host}")
.addPathSegment(it.symbol)
.build()
.toString()
view?.openPanicWebView(urlToOpen)
}
.launch("panic_button")
}
}

View File

@ -50,6 +50,4 @@ interface MessageTabView : BaseView {
fun showRecyclerBottomPadding(show: Boolean)
fun showMailboxChooser(mailboxes: List<Mailbox>)
fun openPanicWebView(url: String)
}

View File

@ -12,8 +12,6 @@ class MoreAdapter @Inject constructor() : RecyclerView.Adapter<MoreAdapter.ItemV
var onClickListener: (moreItem: MoreItem) -> Unit = {}
var onLongClickListener: (moreItem: MoreItem) -> Unit = {}
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemViewHolder(
@ -29,10 +27,6 @@ class MoreAdapter @Inject constructor() : RecyclerView.Adapter<MoreAdapter.ItemV
moreItemImage.setImageResource(item.icon)
root.setOnClickListener { onClickListener(item) }
root.setOnLongClickListener {
onLongClickListener(item)
true
}
}
}

View File

@ -5,7 +5,6 @@ import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentMoreBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.Destination
@ -24,9 +23,6 @@ class MoreFragment : BaseFragment<FragmentMoreBinding>(R.layout.fragment_more),
@Inject
lateinit var moreAdapter: MoreAdapter
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object {
fun newInstance() = MoreFragment()
}
@ -77,9 +73,4 @@ class MoreFragment : BaseFragment<FragmentMoreBinding>(R.layout.fragment_more),
presenter.onDetachView()
super.onDestroyView()
}
override fun restartApp() {
startActivity(MainActivity.getStartIntent(requireContext()))
requireActivity().finishAffinity()
}
}

View File

@ -12,6 +12,4 @@ interface MoreView : BaseView {
fun popView(depth: Int)
fun openView(destination: Destination)
fun restartApp()
}

View File

@ -3,7 +3,6 @@ package io.github.wulkanowy.ui.modules.note
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.repositories.NoteRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
@ -18,7 +17,6 @@ class NotePresenter @Inject constructor(
studentRepository: StudentRepository,
private val noteRepository: NoteRepository,
private val semesterRepository: SemesterRepository,
private val preferencesRepository: PreferencesRepository,
private val analytics: AnalyticsHelper
) : BasePresenter<NoteView>(errorHandler, studentRepository) {
@ -50,19 +48,6 @@ class NotePresenter @Inject constructor(
}
private fun loadData(forceRefresh: Boolean = false) {
if (!preferencesRepository.showNotes) {
view?.run {
enableSwipe(false)
showEmpty(false)
showContent(false)
showErrorView(false)
showProgress(false)
showEmpty(true)
}
return
}
flatResourceFlow {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)

View File

@ -1,99 +0,0 @@
package io.github.wulkanowy.ui.modules.panicmode
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.addCallback
import androidx.core.os.bundleOf
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.databinding.FragmentPanicModeBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import io.github.wulkanowy.utils.openInternetBrowser
import javax.inject.Inject
@AndroidEntryPoint
class PanicModeFragment : BaseFragment<FragmentPanicModeBinding>(R.layout.fragment_panic_mode),
MainView.TitledView {
@Inject
lateinit var wulkanowySdkFactory: WulkanowySdkFactory
@Inject
lateinit var webkitCookieManagerProxy: WebkitCookieManagerProxy
private var webView: WebView? = null
override val titleStringId: Int get() = R.string.panic_mode_title
companion object {
private const val PANIC_URL = "panic_mode_url"
fun newInstance(url: String?): PanicModeFragment {
return PanicModeFragment().apply {
arguments = bundleOf(PANIC_URL to url)
}
}
}
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentPanicModeBinding.bind(view)
binding.panicModeRefresh.setOnClickListener {
binding.panicModeWebview.loadUrl(
binding.panicModeWebview.url ?: arguments?.getString(PANIC_URL).orEmpty()
)
}
binding.panicModeBack.setOnClickListener { binding.panicModeWebview.goBack() }
binding.panicModeHome.setOnClickListener {
binding.panicModeWebview.loadUrl(
arguments?.getString(PANIC_URL).orEmpty()
)
}
binding.panicModeForward.setOnClickListener { binding.panicModeWebview.goForward() }
binding.panicModeShare.setOnClickListener {
requireContext().openInternetBrowser(
binding.panicModeWebview.url.toString(),
)
}
val onBackPressedCallback = requireActivity().onBackPressedDispatcher
.addCallback(viewLifecycleOwner) {
binding.panicModeWebview.goBack()
}
with(binding.panicModeWebview) {
webView = this
with(settings) {
javaScriptEnabled = true
userAgentString = wulkanowySdkFactory.createBase().userAgent
}
webViewClient = object : WebViewClient() {
override fun doUpdateVisitedHistory(
view: WebView?,
url: String?,
isReload: Boolean
) {
binding.panicModeBack.isEnabled = binding.panicModeWebview.canGoBack()
binding.panicModeForward.isEnabled = binding.panicModeWebview.canGoForward()
onBackPressedCallback.isEnabled = binding.panicModeWebview.canGoBack()
}
}
loadUrl(arguments?.getString(PANIC_URL).orEmpty())
}
}
override fun onDestroy() {
webkitCookieManagerProxy.webkitCookieManager?.flush()
webView?.destroy()
super.onDestroy()
}
}

View File

@ -1,22 +1,13 @@
package io.github.wulkanowy.ui.modules.settings
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceScreen
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.main.MainView
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class SettingsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener,
MainView.TitledView, SettingsView {
class SettingsFragment : PreferenceFragmentCompat(), MainView.TitledView, SettingsView {
companion object {
@ -25,26 +16,11 @@ class SettingsFragment : PreferenceFragmentCompat(),
override val titleStringId get() = R.string.settings_title
@Inject
lateinit var preferencesRepository: PreferencesRepository
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.scheme_preferences, rootKey)
val prefScreen: PreferenceScreen? = findPreference("settings_preferences")
val prefDeveloper: Preference? = findPreference("mod_settings")
if (!preferencesRepository.developerMode && prefScreen != null && prefDeveloper != null) {
prefScreen.removePreference(prefDeveloper)
}
Timber.i("Settings view was initialized")
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, rootKey: String?) {
setPreferencesFromResource(R.xml.scheme_preferences, rootKey)
}
override fun showError(text: String, error: Throwable) {}
override fun showMessage(text: String) {}

View File

@ -1,147 +0,0 @@
package io.github.wulkanowy.ui.modules.settings.mod_settings
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.preference.EditTextPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.ErrorDialog
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AppInfo
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class ModSettingsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener,
MainView.TitledView, ModSettingsView {
@Inject
lateinit var presenter: ModSettingsPresenter
@Inject
lateinit var appInfo: AppInfo
@Inject
lateinit var preferencesRepository: PreferencesRepository
override val titleStringId get() = R.string.pref_mod_settings_title
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.scheme_preferences_mod_settings, rootKey)
val attendancePercentagePreference: EditTextPreference? = findPreference("attendance_percentage")
attendancePercentagePreference?.setOnBindEditTextListener { editText ->
editText.inputType = android.text.InputType.TYPE_CLASS_NUMBER or android.text.InputType.TYPE_NUMBER_FLAG_DECIMAL
editText.filters = arrayOf(
android.text.InputFilter { source, _, _, dest, _, _ ->
if (source == "." && dest.isEmpty()) {
return@InputFilter "0."
}
val input = dest.toString() + source.toString()
if (input == "100.00") {
return@InputFilter null
}
val inputVal = input.toFloatOrNull()
if (inputVal != null && inputVal >= 0 && inputVal <= 100) {
null
} else {
""
}
}
)
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
presenter.onSharedPreferenceChanged(key)
}
override fun showError(text: String, error: Throwable) {
(activity as? BaseActivity<*, *>)?.showError(text, error)
}
override fun showMessage(text: String) {
(activity as? BaseActivity<*, *>)?.showMessage(text)
}
override fun showExpiredCredentialsDialog() {
(activity as? BaseActivity<*, *>)?.showExpiredCredentialsDialog()
}
override fun onCaptchaVerificationRequired(url: String?) {
(activity as? BaseActivity<*, *>)?.onCaptchaVerificationRequired(url)
}
override fun showDecryptionFailedDialog() {
(activity as? BaseActivity<*, *>)?.showDecryptionFailedDialog()
}
override fun showChangePasswordSnackbar(redirectUrl: String) {
(activity as? BaseActivity<*, *>)?.showChangePasswordSnackbar(redirectUrl)
}
override fun openClearLoginView() {
(activity as? BaseActivity<*, *>)?.openClearLoginView()
}
override fun showErrorDetailsDialog(error: Throwable) {
ErrorDialog.newInstance(error).show(childFragmentManager, error.toString())
}
override fun showAuthDialog() {
(activity as? BaseActivity<*, *>)?.showAuthDialog()
}
override fun onResume() {
super.onResume()
preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
super.onPause()
preferenceScreen.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
}
override fun showAttendanceSettings(items: List<DashboardItem.HiddenAttendanceTile>) {
val entries = requireContext().resources.getStringArray(R.array.mod_settings_attendance_entries)
val values = requireContext().resources.getStringArray(R.array.mod_settings_attendance_values)
val selectedItemsState = values.map { value -> items.any { it.name == value } }
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.attendance_title)
.setMultiChoiceItems(entries, selectedItemsState.toBooleanArray()) { _, _, _ -> }
.setPositiveButton(android.R.string.ok) { dialog, _ ->
val selectedState = (dialog as AlertDialog).listView.checkedItemPositions
val selectedValues = values
.filterIndexed { index, _ -> selectedState[index] }
.map { DashboardItem.HiddenAttendanceTile.valueOf(it) }
Timber.i("Selected attendance to hide: $selectedValues")
presenter.onAttendanceSettingsSelected(selectedValues)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
override fun restartApp() {
startActivity(MainActivity.getStartIntent(requireContext()))
requireActivity().finishAffinity()
}
}

View File

@ -1,40 +0,0 @@
package io.github.wulkanowy.ui.modules.settings.mod_settings
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.utils.AnalyticsHelper
import timber.log.Timber
import javax.inject.Inject
class ModSettingsPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val analytics: AnalyticsHelper,
private val preferencesRepository: PreferencesRepository
) : BasePresenter<ModSettingsView>(errorHandler, studentRepository) {
override fun onAttachView(view: ModSettingsView) {
super.onAttachView(view)
Timber.i("Mod settings view was initialized")
}
fun onSharedPreferenceChanged(key: String?) {
key ?: return
Timber.i("Change mod settings $key")
analytics.logEvent("setting_changed", "name" to key)
}
fun onHiddenGradesSelected(selectedItems: List<String>) {
preferencesRepository.hiddenGrades = selectedItems
view?.restartApp()
}
fun onAttendanceSettingsSelected(selectedValues: List<DashboardItem.HiddenAttendanceTile>) {
preferencesRepository.hiddenAttendanceItems = selectedValues
view?.restartApp()
}
}

View File

@ -1,9 +0,0 @@
package io.github.wulkanowy.ui.modules.settings.mod_settings
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
interface ModSettingsView : BaseView {
fun restartApp()
fun showAttendanceSettings(items: List<DashboardItem.HiddenAttendanceTile>)
}

View File

@ -42,22 +42,24 @@ class TimetableWidgetConfigurePresenter @Inject constructor(
}
private fun loadData() {
resourceFlow { studentRepository.getSavedStudents(false) }.onEach {
when (it) {
is Resource.Loading -> Timber.d("Timetable widget configure students data load")
is Resource.Success -> {
val selectedStudentId = appWidgetId?.let { id ->
sharedPref.getLong(getStudentWidgetKey(id), 0)
} ?: -1
when {
it.data.isEmpty() -> view?.openLoginView()
it.data.size == 1 && !isFromProvider -> onItemSelect(it.data.single().student)
else -> view?.updateData(it.data, selectedStudentId)
resourceFlow { studentRepository.getSavedStudentsWithSemesters(false) }
.onEach {
when (it) {
is Resource.Loading -> Timber.d("Timetable widget configure students data load")
is Resource.Success -> {
val selectedStudentId = appWidgetId?.let { id ->
sharedPref.getLong(getStudentWidgetKey(id), 0)
} ?: -1
when {
it.data.isEmpty() -> view?.openLoginView()
it.data.size == 1 && !isFromProvider -> onItemSelect(it.data.single().student)
else -> view?.updateData(it.data, selectedStudentId)
}
}
is Resource.Error -> errorHandler.dispatch(it.error)
}
is Resource.Error -> errorHandler.dispatch(it.error)
}
}.launch()
}.launch()
}
private fun registerStudent(student: Student?) {

View File

@ -95,7 +95,7 @@ class TimetableWidgetFactory(
private suspend fun getStudent(studentId: Long): Student? {
val students = studentRepository.getSavedStudents()
return students.singleOrNull { it.student.id == studentId }?.student
return students.singleOrNull { it.id == studentId }
}
private suspend fun getLessons(

View File

@ -2,7 +2,11 @@ package io.github.wulkanowy.ui.modules.timetablewidget
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetManager.*
import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_DELETED
import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS
import android.appwidget.AppWidgetManager.INVALID_APPWIDGET_ID
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@ -22,7 +26,14 @@ import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.widgets.TimetableWidgetService
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
import io.github.wulkanowy.utils.*
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.PendingIntentCompat
import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.nextSchoolDay
import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.previousSchoolDay
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -244,7 +255,7 @@ class TimetableWidgetProvider : BroadcastReceiver() {
private suspend fun getStudent(studentId: Long, appWidgetId: Int) = try {
val students = studentRepository.getSavedStudents(false)
val student = students.singleOrNull { it.student.id == studentId }?.student
val student = students.singleOrNull { it.id == studentId }
when {
student != null -> student
studentId != 0L && studentRepository.isCurrentStudentSet() -> {
@ -263,7 +274,10 @@ class TimetableWidgetProvider : BroadcastReceiver() {
}
private fun setupAccountView(
context: Context, student: Student, remoteViews: RemoteViews, widgetId: Int
context: Context,
student: Student,
remoteViews: RemoteViews,
widgetId: Int
) {
val accountInitials = getAccountInitials(student.nickOrName)
val accountPickerPendingIntent = createAccountPickerPendingIntent(context, widgetId)

View File

@ -30,10 +30,6 @@ fun getRefreshKey(name: String, mailbox: Mailbox?, folder: MessageFolder): Strin
return "${name}_${mailbox?.globalKey ?: "all"}_${folder.id}"
}
fun getRefreshKey(name: String): String {
return name
}
class AutoRefreshHelper @Inject constructor(
@ApplicationContext private val context: Context,
private val sharedPref: SharedPrefProvider

View File

@ -1,6 +1,8 @@
Wersja 2.6.13
Wersja 2.6.1
— dodaliśmy tryb awaryjny (no w sensie taka przeglądarka z dziennikiem w apce, ale nie trzeba się ręcznie logować)
— naprawiliśmy ładowania ucznia na tle klasy i lekcji zrealizowanych
— dodaliśmy kalkulator frekwencji
— dodaliśmy wyświetlanie lekcji dodatkowych w planie lekcji
— ulepszyliśmy wyjaśnienie na ekranie z miejscem na wpisanie numeru PESEL
— naprawiliśmy rzadkie sytuacje, gdy plan lekcji nakładał się na informację o jego braku
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -60,16 +60,6 @@
tools:ignore="UseCompoundDrawables"
tools:visibility="invisible">
<include
android:id="@+id/message_tab_panic_section"
layout="@layout/item_dashboard_panic_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="16dp"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/dashboard_error_admin_message" />
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"

View File

@ -1,69 +0,0 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.modules.panicmode.PanicModeFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorControlHighlight">
<com.google.android.material.button.MaterialButton
android:id="@+id/panic_mode_share"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
app:icon="@drawable/ic_share"
app:iconTint="?colorOnSurface" />
<com.google.android.material.button.MaterialButton
android:id="@+id/panic_mode_home"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
app:icon="@drawable/ic_all_home"
app:iconTint="?colorOnSurface" />
<com.google.android.material.button.MaterialButton
android:id="@+id/panic_mode_refresh"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:contentDescription="@string/logviewer_refresh"
app:icon="@drawable/ic_refresh"
app:iconTint="?colorOnSurface" />
<com.google.android.material.button.MaterialButton
android:id="@+id/panic_mode_back"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
app:icon="@drawable/ic_chevron_left"
app:iconTint="?colorOnSurface" />
<com.google.android.material.button.MaterialButton
android:id="@+id/panic_mode_forward"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
app:icon="@drawable/ic_chevron_right"
app:iconTint="?colorOnSurface" />
</LinearLayout>
<WebView
android:id="@+id/panic_mode_webview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>

View File

@ -1,105 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="6dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/dashboard_admin_message"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/dashboard_admin_message_item_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="6dp">
android:layout_height="match_parent">
<ImageView
android:id="@+id/dashboard_admin_message_item_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_error"
app:layout_constraintBottom_toBottomOf="@id/dashboard_admin_message_item_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/dashboard_admin_message_item_title"
tools:ignore="ContentDescription"
tools:tint="@android:color/black" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/dashboard_admin_message_item_content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/dashboard_admin_message_item_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/dashboard_admin_message_item_close"
app:layout_constraintStart_toEndOf="@id/dashboard_admin_message_item_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem" />
<ImageView
android:id="@+id/dashboard_admin_message_item_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_error"
app:layout_constraintBottom_toBottomOf="@id/dashboard_admin_message_item_title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/dashboard_admin_message_item_title"
tools:ignore="ContentDescription"
tools:tint="@android:color/black" />
<ImageView
android:id="@+id/dashboard_admin_message_item_close"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="4dp"
android:padding="12dp"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_close"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/dashboard_admin_message_item_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/dashboard_admin_message_item_close"
app:layout_constraintStart_toEndOf="@id/dashboard_admin_message_item_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/dashboard_admin_message_item_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dashboard_admin_message_item_title"
app:layout_constraintVertical_bias="0"
app:lineHeight="20dp"
tools:maxLines="5"
tools:text="@tools:sample/lorem/random" />
<ImageView
android:id="@+id/dashboard_admin_message_item_close"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="4dp"
android:background="?selectableItemBackgroundBorderless"
android:padding="12dp"
android:src="@drawable/ic_close"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/dashboard_admin_message_item_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/dashboard_admin_message_item_title"
app:layout_constraintVertical_bias="0"
app:lineHeight="20dp"
tools:maxLines="5"
tools:text="@tools:sample/lorem/random" />
<com.google.android.material.button.MaterialButton
android:id="@+id/dashboard_admin_message_item_dismiss"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:text="@android:string/ok"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/dashboard_admin_message_item_description"
app:layout_constraintVertical_bias="0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<include
android:id="@+id/dashboard_panic_section"
layout="@layout/item_dashboard_panic_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/dashboard_error_admin_message" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/dashboard_admin_message_item_dismiss"
style="@style/Widget.Material3.Button.TextButton.Dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:text="@android:string/ok"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/dashboard_admin_message_item_description"
app:layout_constraintVertical_bias="0" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -1,28 +0,0 @@
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="6dp">
<LinearLayout
android:layout_margin="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Aplikacja nie działa?"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.button.MaterialButton
android:id="@+id/dashboard_panic_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="Otwórz stronę dziennika" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pref_mod_settings_title">Skrytá nastavení</string>
<string name="pref_hidden_settings_attendance_percentage">Procento docházky</string>
<string name="pref_hidden_settings_hidden_grades">Skrýt známky</string>
<string name="pref_mod_settings_hidden_attendance_items">Skryté položky docházky</string>
<string name="pref_mod_settings_show_notes">Poznámky k pořadu</string>
<string-array name="mod_settings_attendance_entries">
<item>Neomluvená absence</item>
<item>Výjimka</item>
<item>Ospravedlněné zpoždění</item>
<item>Neomluvené zpoždění</item>
<item>Přítomnost</item>
<item>Smazáno</item>
<item>Ospravedlněná absence</item>
<item>Neznámá</item>
</string-array>
<string name="pref_mod_settings_developer_mode">Režim pro vývojáře (přístup na tuto stránku)</string>
<string name="pref_mod_settings_developer_mode_summary">Po deaktivaci tohoto nastavení již nebudete mít přístup na tuto stránku, ale nastavení se bude nadále používat.</string>
</resources>

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@ -893,5 +893,4 @@
<string name="message_unmute">Zrušit ztlumení</string>
<string name="message_mute_success">Ztlumili jste tohoto uživatele</string>
<string name="message_unmute_success">Zrušili jste ztlumení tohoto uživatele</string>
<string name="pref_mod_settings_other_title">Jiné</string>
</resources>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="pref_mod_settings_title">Hidden settings</string>
<string name="pref_hidden_settings_attendance_percentage">Attendance percentage</string>
<string name="pref_hidden_settings_hidden_grades">Hide grades</string>
<string name="pref_mod_settings_hidden_attendance_items">Hidden attendance items</string>
<string name="pref_mod_settings_show_notes">Show notes</string>
<string-array name="mod_settings_attendance_entries">
<item>Unexcused absence</item>
<item>Exemption</item>
<item>Excused lateness</item>
<item>Unexcused lateness</item>
<item>Present</item>
<item>Deleted</item>
<item>Excused absence</item>
<item>Unknown</item>
</string-array>
<string name="pref_mod_settings_developer_mode">Developer mode (access to this page)</string>
<string name="pref_mod_settings_developer_mode_summary">After disabling this setting, you will not be able to access this page anymore, but the settings will still be applied.</string>
</resources>

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